basic VC gui
This commit is contained in:
parent
49632b3bab
commit
ce6dc3ba5e
9 changed files with 308 additions and 31 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
1
src/webpage/icons/call.svg
Normal file
1
src/webpage/icons/call.svg
Normal 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 |
1
src/webpage/icons/hangup.svg
Normal file
1
src/webpage/icons/hangup.svg
Normal 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 |
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue