basic VC gui

This commit is contained in:
MathMan05 2025-05-15 15:45:09 -05:00
parent 49632b3bab
commit ce6dc3ba5e
9 changed files with 308 additions and 31 deletions

View file

@ -89,7 +89,8 @@
<input type="checkbox" id="memberlisttoggle" checked />
</div>
<div class="flexltr flexgrow">
<div class="flexttb flexgrow">
<div class="flexttb flexgrow" id="voiceArea"></div>
<div class="flexttb flexgrow" id="chatArea">
<div id="channelw" class="flexltr">
<div id="loadingdiv"></div>
</div>

View file

@ -729,6 +729,17 @@ class Channel extends SnowFlake {
if (typeof memb !== "string") {
await Member.new(memb, this.guild);
}
const users = this.usersDiv.deref();
if (users) {
const user = await this.localuser.getUser(typeof memb === "string" ? memb : memb.id);
if (joined) {
this.makeUserBox(user, users);
} else {
this.destUserBox(user);
}
}
this.updateVoiceUsers();
if (this.voice === this.localuser.currentVoice) {
AVoice.noises("join");
@ -1060,7 +1071,124 @@ class Channel extends SnowFlake {
if (!this.last_pin_timestamp && !this.lastpin) return false;
return this.last_pin_timestamp !== this.lastpin;
}
async getHTML(addstate = true, getMessages = true) {
boxMap = new Map<string, HTMLElement>();
destUserBox(user: User) {
const box = this.boxMap.get(user.id);
if (!box) return;
box.remove();
this.boxMap.delete(user.id);
}
async makeUserBox(user: User, users: HTMLElement) {
const memb = Member.resolveMember(user, this.guild);
const box = document.createElement("div");
this.boxMap.set(user.id, box);
if (user.accent_color != undefined) {
box.style.setProperty(
"--accent_color",
`#${user.accent_color.toString(16).padStart(6, "0")}`,
);
}
memb.then((_) => {
if (!_) return;
if (_.accent_color !== undefined) {
box.style.setProperty("--accent_color", `#${_.accent_color.toString(16).padStart(6, "0")}`);
}
});
box.append(user.buildpfp(this.guild));
const span = document.createElement("span");
span.textContent = user.name;
memb.then((_) => {
if (!_) return;
span.textContent = _.name;
});
span.classList.add("voiceUsername");
box.append(span);
users.append(box);
}
usersDiv = new WeakRef(document.createElement("div"));
async setUpVoiceArea() {
if (!this.voice) throw new Error("voice not found?");
const voiceArea = document.getElementById("voiceArea") as HTMLElement;
const buttonRow = document.createElement("div");
buttonRow.classList.add("flexltr", "buttonRow");
const updateMicIcon = () => {
mspan.classList.remove("svg-micmute", "svg-mic");
mspan.classList.add(this.localuser.mute ? "svg-micmute" : "svg-mic");
};
const mute = document.createElement("div");
const mspan = document.createElement("span");
mute.append(mspan);
updateMicIcon();
this.localuser.updateOtherMic = updateMicIcon;
mute.onclick = () => {
this.localuser.mute = !this.localuser.mute;
this.localuser.updateMic();
};
mute.classList.add("muteVoiceIcon");
const updateCallIcon = () => {
cspan.classList.remove("svg-call", "svg-hangup");
cspan.classList.add(this.voice?.open ? "svg-hangup" : "svg-call");
};
const call = document.createElement("div");
const cspan = document.createElement("span");
call.append(cspan);
updateCallIcon();
call.onclick = async () => {
if (this.voice?.userids.has(this.localuser.user.id)) {
this.voice.leave();
} else if (this.voice) {
await this.localuser.joinVoice(this);
}
updateCallIcon();
};
call.classList.add("callVoiceIcon");
buttonRow.append(mute, call);
const users = document.createElement("div");
users.classList.add("voiceUsers");
this.voice.userids.forEach(async (_, id) => {
const user = await this.localuser.getUser(id);
this.makeUserBox(user, users);
});
this.usersDiv = new WeakRef(users);
this.voice.onSpeakingChange = (id, speaking) => {
const box = this.boxMap.get(id);
if (!box) return;
if (speaking) {
box.classList.add("speaking");
} else {
box.classList.remove("speaking");
}
};
voiceArea.append(users, buttonRow);
}
async getHTML(addstate = true, getMessages: boolean | void = undefined) {
if (getMessages === undefined) {
getMessages = this.type !== 2 || !this.localuser.voiceAllowed;
}
const messages = document.getElementById("channelw") as HTMLDivElement;
const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer"));
for (const thing of messageContainers) {
thing.remove();
}
const chatArea = document.getElementById("chatArea") as HTMLElement;
const voiceArea = document.getElementById("voiceArea") as HTMLElement;
voiceArea.innerHTML = "";
if (getMessages) {
chatArea.style.removeProperty("display");
} else {
chatArea.style.setProperty("display", "none");
this.setUpVoiceArea();
}
const pinnedM = document.getElementById("pinnedMDiv");
if (pinnedM) {
if (this.unreadPins()) {
@ -1069,11 +1197,6 @@ class Channel extends SnowFlake {
pinnedM.classList.remove("unreadPin");
}
}
const ghostMessages = document.getElementById("ghostMessages") as HTMLElement;
ghostMessages.innerHTML = "";
for (const thing of this.fakeMessages) {
ghostMessages.append(thing[1]);
}
if (addstate) {
history.pushState([this.guild_id, this.id], "", "/channels/" + this.guild_id + "/" + this.id);
}
@ -1095,6 +1218,7 @@ class Channel extends SnowFlake {
if (this.guild !== this.localuser.lookingguild) {
this.guild.loadGuild();
}
if (this.localuser.channelfocus && this.localuser.channelfocus.myhtml) {
this.localuser.channelfocus.myhtml.classList.remove("viewChannel");
}
@ -1117,15 +1241,22 @@ class Channel extends SnowFlake {
return;
}
const prom = this.infinite.delete();
const ghostMessages = document.getElementById("ghostMessages") as HTMLElement;
ghostMessages.innerHTML = "";
for (const thing of this.fakeMessages) {
ghostMessages.append(thing[1]);
}
const loading = document.getElementById("loadingdiv") as HTMLDivElement;
Channel.regenLoadingMessages();
loading.classList.add("loading");
const prom = this.infinite.delete();
if (getMessages) {
const loading = document.getElementById("loadingdiv") as HTMLDivElement;
Channel.regenLoadingMessages();
loading.classList.add("loading");
}
this.rendertyping();
this.localuser.getSidePannel();
if (this.voice && this.localuser.voiceAllowed) {
this.localuser.joinVoice(this);
//this.localuser.joinVoice(this);
}
(document.getElementById("typebox") as HTMLDivElement).contentEditable = "" + this.canMessage;
(document.getElementById("upload") as HTMLElement).style.visibility = this.canMessage

View file

@ -54,6 +54,8 @@ class Direct extends Guild {
}
}
getHTML() {
const voiceArea = document.getElementById("voiceArea") as HTMLElement;
voiceArea.innerHTML = "";
const sideContainDiv = document.getElementById("sideContainDiv");
if (sideContainDiv) {
sideContainDiv.classList.remove("searchDiv");

View file

@ -0,0 +1 @@
<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;fill-rule:evenodd;stroke:#000;stroke-width:10;stroke-linecap:round;stroke-dasharray:none" d="M7.7 22.7a20.9 20.9 0 0 1 31.6-.6" transform="rotate(-133.7 24.2 26.6) scale(1.16048)"/><path style="fill:#1a1a1a;stroke:#000;stroke-width:6.99651;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" transform="rotate(-133.7)" d="M-21.9-3.1h9.1v7.2h-9.1zM-52.9-3.1h9.1v7.2h-9.1z"/></svg>

After

Width:  |  Height:  |  Size: 479 B

View file

@ -0,0 +1 @@
<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;fill-rule:evenodd;stroke:#000;stroke-width:10;stroke-linecap:round;stroke-dasharray:none" d="M7.7 22.7a20.9 20.9 0 0 1 31.6-.6" transform="translate(-3.4 -2) scale(1.16048)"/><path style="fill:#1a1a1a;stroke:#000;stroke-width:6.99651;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" d="M34.8 22.3h9.1v7.2h-9.1zM3.8 22.3h9.1v7.2H3.8z"/></svg>

After

Width:  |  Height:  |  Size: 444 B

View file

@ -249,9 +249,12 @@ class Localuser {
}
mute = true;
deaf = false;
updateMic() {
updateOtherMic = () => {};
updateMic(updateVoice: boolean = true) {
this.updateOtherMic();
const mic = document.getElementById("mic") as HTMLElement;
mic.classList.remove("svg-mic", "svg-micmute");
if (this.voiceFactory && updateVoice) this.voiceFactory.mute = this.mute;
if (this.mute) {
mic.classList.add("svg-micmute");
} else {
@ -271,7 +274,11 @@ class Localuser {
this.mdBox();
this.voiceFactory = new VoiceFactory({id: this.user.id});
this.voiceFactory = new VoiceFactory({id: this.user.id}, (g) => {
if (this.ws) {
this.ws.send(JSON.stringify(g));
}
});
this.handleVoice();
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
this.userinfo.username = this.user.username;
@ -757,6 +764,10 @@ class Localuser {
}
break;
case "VOICE_STATE_UPDATE":
if (this.user.id === temp.d.user_id) {
this.mute = temp.d.self_mute;
this.updateMic(false);
}
if (this.voiceFactory) {
this.voiceFactory.voiceStateUpdate(temp.d);
}

View file

@ -184,6 +184,69 @@ body {
position: relative;
width: 100%;
}
#voiceArea:empty {
display: none;
}
#voiceArea {
background: var(--black);
position: relative;
}
.voiceUsers {
padding: 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.speaking {
border: solid var(--green) 3px;
padding: 77px 137px !important;
}
.voiceUsers > * {
background: var(--accent_color, var(--primary-bg));
padding: 80px 140px;
width: fit-content;
border-radius: 8px;
position: relative;
box-sizing: border-box;
margin: 8px;
img {
width: 60px;
height: 60px;
cursor: unset;
}
}
.buttonRow > * {
margin-right: 6px;
width: 54px;
height: 54px;
background: var(--secondary-hover);
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
* {
width: 32px !important;
height: 32px !important;
background: var(--primary-text);
}
}
.buttonRow {
position: absolute;
bottom: 20px;
width: 100%;
display: flex;
justify-content: center;
}
.voiceUsername {
position: absolute;
bottom: 10px;
left: 14px;
background: var(--secondary-bg);
padding: 4px;
border-radius: 6px;
}
.flexgrow {
flex-grow: 1;
min-height: 0;
@ -415,6 +478,15 @@ textarea {
display: block;
mask-size: cover !important;
}
.svg-call {
mask: url(/icons/call.svg);
mask-size: contain !important;
}
.svg-hangup {
mask: url(/icons/hangup.svg);
mask-size: contain !important;
background: var(--red);
}
.svg-plainx {
mask: url(/icons/plainx.svg);
mask-size: contain !important;

View file

@ -499,14 +499,14 @@ class User extends SnowFlake {
this.bind(div, guild);
return div;
}
async buildstatuspfp(guild: Guild | void | Member | null): Promise<HTMLDivElement> {
buildstatuspfp(guild: Guild | void | Member | null): HTMLDivElement {
const div = document.createElement("div");
div.classList.add("pfpDiv");
const pfp = this.buildpfp(guild, div);
div.append(pfp);
const status = document.createElement("div");
status.classList.add("statusDiv");
switch (await this.getStatus()) {
switch (this.getStatus()) {
case "offline":
case "invisible":
status.classList.add("offlinestatus");
@ -785,7 +785,7 @@ class User extends SnowFlake {
badgediv.append(badge);
}
})();
const pfp = await this.buildstatuspfp(guild);
const pfp = this.buildstatuspfp(guild);
div.appendChild(pfp);
const userbody = document.createElement("div");
userbody.classList.add("flexttb", "infosection");

View file

@ -1,8 +1,13 @@
import {memberjson, sdpback, voiceserverupdate, voiceStatus, webRTCSocket} from "./jsontypes.js";
class VoiceFactory {
settings: {id: string};
constructor(usersettings: VoiceFactory["settings"]) {
handleGateway: (obj: Object) => void;
constructor(
usersettings: VoiceFactory["settings"],
handleGateway: VoiceFactory["handleGateway"],
) {
this.settings = usersettings;
this.handleGateway = handleGateway;
}
voices = new Map<string, Map<string, Voice>>();
voiceChannels = new Map<string, Voice>();
@ -20,19 +25,50 @@ class VoiceFactory {
}
const urlobj = this.guildUrlMap.get(guildid);
if (!urlobj) throw new Error("url Object doesn't exist (InternalError)");
const voice = new Voice(this.settings.id, settings, urlobj);
const voice = new Voice(this.settings.id, settings, urlobj, this);
this.voiceChannels.set(channelId, voice);
guild.set(channelId, voice);
return voice;
}
onJoin = (_voice: Voice) => {};
onLeave = (_voice: Voice) => {};
private imute = false;
get mute() {
return this.imute;
}
set mute(s) {
const changed = this.imute !== s;
this.imute = s;
if (this.currentVoice && changed) {
this.currentVoice.updateMute();
this.updateSelf();
}
}
updateSelf() {
if (this.currentVoice && this.currentVoice.open) {
this.handleGateway({
op: 4,
d: {
guild_id: this.curGuild,
channel_id: this.curChan,
self_mute: this.imute,
self_deaf: false,
self_video: false,
flags: 3,
},
});
}
}
curGuild?: string;
curChan?: string;
joinVoice(channelId: string, guildId: string, self_mute = false) {
const voice = this.voiceChannels.get(channelId);
this.mute = self_mute;
if (this.currentVoice && this.currentVoice.ws) {
this.currentVoice.leave();
}
this.curChan = channelId;
this.curGuild = guildId;
if (!voice) throw new Error(`Voice ${channelId} does not exist`);
voice.join();
this.currentVoice = voice;
@ -53,7 +89,7 @@ class VoiceFactory {
voiceStateUpdate(update: voiceStatus) {
const prev = this.userMap.get(update.user_id);
console.log(prev, this.userMap);
if (prev) {
if (prev && update.channel_id !== this.curChan) {
prev.disconnect(update.user_id);
this.onLeave(prev);
}
@ -93,10 +129,17 @@ class Voice {
readonly userid: string;
settings: {bitrate: number};
urlobj: {url?: string; token?: string; geturl: Promise<void>; gotUrl: () => void};
constructor(userid: string, settings: Voice["settings"], urlobj: Voice["urlobj"]) {
owner: VoiceFactory;
constructor(
userid: string,
settings: Voice["settings"],
urlobj: Voice["urlobj"],
owner: VoiceFactory,
) {
this.userid = userid;
this.settings = settings;
this.urlobj = urlobj;
this.owner = owner;
}
pc?: RTCPeerConnection;
ws?: WebSocket;
@ -425,6 +468,7 @@ a=rtcp-mux\r`;
if (!this.ws) return;
const pair = this.ssrcMap.entries().next().value;
if (!pair) return;
this.onSpeakingChange(this.userid, +this.speaking);
this.ws.send(
JSON.stringify({
op: 5,
@ -470,6 +514,12 @@ a=rtcp-mux\r`;
}
console.log(this.reciverMap);
}
updateMute() {
if (!this.micTrack) return;
this.micTrack.enabled = !this.owner.mute;
}
mic?: RTCRtpSender;
micTrack?: MediaStreamTrack;
async startWebRTC() {
this.status = "Making offer";
const pc = new RTCPeerConnection();
@ -497,14 +547,17 @@ a=rtcp-mux\r`;
console.log(this.recivers);
};
const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true});
for (const track of audioStream.getAudioTracks()) {
//Add track
const [track] = audioStream.getAudioTracks();
//Add track
this.setupMic(audioStream);
const sender = pc.addTrack(track);
this.mic = sender;
this.micTrack = track;
track.enabled = !this.owner.mute;
this.senders.add(sender);
console.log(sender);
this.setupMic(audioStream);
const sender = pc.addTrack(track);
this.senders.add(sender);
console.log(sender);
}
for (let i = 0; i < 10; i++) {
pc.addTransceiver("audio", {
direction: "inactive",
@ -697,9 +750,12 @@ a=rtcp-mux\r`;
userids = new Map<string, {}>();
async voiceupdate(update: voiceStatus) {
console.log("Update!");
if (!this.userids.has(update.user_id)) {
this.onMemberChange(update?.member || update.user_id, true);
}
this.userids.set(update.user_id, {deaf: update.deaf, muted: update.mute});
this.onMemberChange(update?.member || update.user_id, true);
if (update.user_id === this.userid && this.open) {
if (update.user_id === this.userid && this.open && !(this.status === "Done")) {
if (!update) {
this.status = "bad responce from WS";
return;
@ -757,6 +813,8 @@ a=rtcp-mux\r`;
console.warn("leave");
this.open = false;
this.status = "Left voice chat";
this.onMemberChange(this.userid, false);
this.userids.delete(this.userid);
if (this.ws) {
this.ws.close();
this.ws = undefined;