Merge branch 'Voice'

This commit is contained in:
ygg2 2024-10-26 18:47:49 -04:00
commit efae437b70
35 changed files with 3401 additions and 2721 deletions

View file

@ -1,6 +1,6 @@
import{ getBulkInfo }from"./login.js";
class Voice{
class AVoice{
audioCtx: AudioContext;
info: { wave: string | Function; freq: number };
playing: boolean;
@ -95,7 +95,7 @@ class Voice{
static noises(noise: string): void{
switch(noise){
case"three": {
const voicy = new Voice("sin", 800);
const voicy = new AVoice("sin", 800);
voicy.play();
setTimeout(_=>{
voicy.freq = 1000;
@ -109,7 +109,7 @@ class Voice{
break;
}
case"zip": {
const voicy = new Voice((t: number, freq: number)=>{
const voicy = new AVoice((t: number, freq: number)=>{
return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq);
}, 700);
voicy.play();
@ -119,7 +119,7 @@ class Voice{
break;
}
case"square": {
const voicy = new Voice("square", 600, 0.4);
const voicy = new AVoice("square", 600, 0.4);
voicy.play();
setTimeout(_=>{
voicy.freq = 800;
@ -133,7 +133,7 @@ class Voice{
break;
}
case"beep": {
const voicy = new Voice("sin", 800);
const voicy = new AVoice("sin", 800);
voicy.play();
setTimeout(_=>{
voicy.stop();
@ -146,6 +146,38 @@ class Voice{
}, 150);
break;
}
case "join":{
const voicy = new AVoice("triangle", 600,.1);
voicy.play();
setTimeout(_=>{
voicy.freq=800;
}, 75);
setTimeout(_=>{
voicy.freq=1000;
}, 150);
setTimeout(_=>{
voicy.stop();
}, 200);
break;
}
case "leave":{
const voicy = new AVoice("triangle", 850,.5);
voicy.play();
setTimeout(_=>{
voicy.freq=700;
}, 100);
setTimeout(_=>{
voicy.stop();
voicy.freq=400;
}, 180);
setTimeout(_=>{
voicy.play();
}, 200);
setTimeout(_=>{
voicy.stop();
}, 250);
break;
}
}
}
static get sounds(){
@ -161,4 +193,4 @@ class Voice{
return userinfos.preferences.notisound;
}
}
export{ Voice };
export{ AVoice as AVoice };

View file

@ -1,6 +1,6 @@
"use strict";
import{ Message }from"./message.js";
import{ Voice }from"./audio.js";
import{ AVoice }from"./audio.js";
import{ Contextmenu }from"./contextmenu.js";
import{ Dialog }from"./dialog.js";
import{ Guild }from"./guild.js";
@ -10,16 +10,11 @@ import{ Settings }from"./settings.js";
import{ Role, RoleList }from"./role.js";
import{ InfiniteScroller }from"./infiniteScroller.js";
import{ SnowFlake }from"./snowflake.js";
import{
channeljson,
embedjson,
messageCreateJson,
messagejson,
readyjson,
startTypingjson,
}from"./jsontypes.js";
import{channeljson,embedjson,messageCreateJson,messagejson,readyjson,startTypingjson}from"./jsontypes.js";
import{ MarkDown }from"./markdown.js";
import{ Member }from"./member.js";
import { Voice } from "./voice.js";
import { User } from "./user.js";
declare global {
interface NotificationOptions {
@ -55,6 +50,8 @@ class Channel extends SnowFlake{
idToPrev: Map<string, string> = new Map();
idToNext: Map<string, string> = new Map();
messages: Map<string, Message> = new Map();
voice?:Voice;
bitrate:number=128000;
static setupcontextmenu(){
this.contextmenu.addbutton("Copy channel id", function(this: Channel){
navigator.clipboard.writeText(this.id);
@ -123,13 +120,14 @@ class Channel extends SnowFlake{
const div = document.createElement("div");
div.classList.add("invitediv");
const text = document.createElement("span");
text.classList.add("ellipsis");
div.append(text);
let uses = 0;
let expires = 1800;
const copycontainer = document.createElement("div");
copycontainer.classList.add("copycontainer");
const copy = document.createElement("span");
copy.classList.add("copybutton", "svgtheme", "svg-copy");
copy.classList.add("copybutton", "svgicon", "svg-copy");
copycontainer.append(copy);
copycontainer.onclick = _=>{
if(text.textContent){
@ -336,6 +334,10 @@ class Channel extends SnowFlake{
}
this.setUpInfiniteScroller();
this.perminfo ??= {};
if(this.type===2&&this.localuser.voiceFactory){
this.voice=this.localuser.voiceFactory.makeVoice(this.guild.id,this.id,{bitrate:this.bitrate});
this.setUpVoice();
}
}
get perminfo(){
return this.guild.perminfo.channels[this.id];
@ -457,6 +459,7 @@ class Channel extends SnowFlake{
get visable(){
return this.hasPermission("VIEW_CHANNEL");
}
voiceUsers=new WeakRef(document.createElement("div"));
createguildHTML(admin = false): HTMLDivElement{
const div = document.createElement("div");
this.html = new WeakRef(div);
@ -487,18 +490,18 @@ class Channel extends SnowFlake{
const decdiv = document.createElement("div");
const decoration = document.createElement("span");
decoration.classList.add("svgtheme", "collapse-icon", "svg-category");
decoration.classList.add("svgicon", "collapse-icon", "svg-category");
decdiv.appendChild(decoration);
const myhtml = document.createElement("p2");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
decdiv.appendChild(myhtml);
caps.appendChild(decdiv);
const childrendiv = document.createElement("div");
if(admin){
const addchannel = document.createElement("span");
addchannel.textContent = "+";
addchannel.classList.add("addchannel");
addchannel.classList.add("addchannel","svgicon","svg-plus");
caps.appendChild(addchannel);
addchannel.onclick = _=>{
this.guild.createchannels(this.createChannel.bind(this));
@ -506,8 +509,8 @@ class Channel extends SnowFlake{
this.coatDropDiv(decdiv, childrendiv);
}
div.appendChild(caps);
caps.classList.add("capsflex");
decdiv.classList.add("channeleffects");
caps.classList.add("flexltr","capsflex");
decdiv.classList.add("flexltr","channeleffects");
decdiv.classList.add("channel");
Channel.contextmenu.bindContextmenu(decdiv, this,undefined);
@ -552,32 +555,84 @@ class Channel extends SnowFlake{
}
// @ts-ignore I dont wanna deal with this
div.all = this;
const button = document.createElement("button");
button.classList.add("channelbutton");
div.append(button);
const myhtml = document.createElement("span");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
if(this.type === 0){
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-channel");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-channel");
}else if(this.type === 2){
//
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-voice");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-voice");
}else if(this.type === 5){
//
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-announce");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-announce");
}else{
console.log(this.type);
}
div.appendChild(myhtml);
div.onclick = _=>{
button.appendChild(myhtml);
button.onclick = _=>{
this.getHTML();
const toggle = document.getElementById("maintoggle") as HTMLInputElement;
toggle.checked = true;
};
if(this.type===2){
const voiceUsers=document.createElement("div");
div.append(voiceUsers);
this.voiceUsers=new WeakRef(voiceUsers);
this.updateVoiceUsers();
}
}
return div;
}
async setUpVoice(){
if(!this.voice) return;
this.voice.onMemberChange=async (memb,joined)=>{
console.log(memb,joined);
if(typeof memb!=="string"){
await Member.new(memb,this.guild);
}
this.updateVoiceUsers();
if(this.voice===this.localuser.currentVoice){
AVoice.noises("join");
}
}
}
async updateVoiceUsers(){
const voiceUsers=this.voiceUsers.deref();
if(!voiceUsers||!this.voice) return;
console.warn(this.voice.userids)
const html=(await Promise.all(this.voice.userids.entries().toArray().map(async _=>{
const user=await User.resolve(_[0],this.localuser);
console.log(user);
const member=await Member.resolveMember(user,this.guild);
const array=[member,_[1]] as [Member, typeof _[1]];
return array;
}))).flatMap(([member,_obj])=>{
if(!member){
console.warn("This is weird, member doesn't exist :P");
return [];
}
const div=document.createElement("div");
div.classList.add("voiceuser");
const span=document.createElement("span");
span.textContent=member.name;
div.append(span);
return div;
});
voiceUsers.innerHTML="";
voiceUsers.append(...html);
}
get myhtml(){
if(this.html){
return this.html.deref();
@ -764,6 +819,7 @@ class Channel extends SnowFlake{
}
makereplybox(){
const replybox = document.getElementById("replybox") as HTMLElement;
const typebox = document.getElementById("typebox") as HTMLElement;
if(this.replyingto){
replybox.innerHTML = "";
const span = document.createElement("span");
@ -776,14 +832,16 @@ class Channel extends SnowFlake{
replybox.classList.add("hideReplyBox");
this.replyingto = null;
replybox.innerHTML = "";
typebox.classList.remove("typeboxreplying");
};
replybox.classList.remove("hideReplyBox");
X.textContent = "⦻";
X.classList.add("cancelReply");
X.classList.add("cancelReply","svgicon","svg-x");
replybox.append(span);
replybox.append(X);
typebox.classList.add("typeboxreplying");
}else{
replybox.classList.add("hideReplyBox");
typebox.classList.remove("typeboxreplying");
}
}
async getmessage(id: string): Promise<Message>{
@ -840,6 +898,9 @@ class Channel extends SnowFlake{
loading.classList.add("loading");
this.rendertyping();
this.localuser.getSidePannel();
if(this.voice){
this.localuser.joinVoice(this);
}
await this.putmessages();
await prom;
if(id !== Channel.genid){
@ -972,12 +1033,7 @@ class Channel extends SnowFlake{
return;
}
await fetch(
this.info.api +
"/channels/" +
this.id +
"/messages?limit=100&after=" +
id,
{
this.info.api + "/channels/" +this.id +"/messages?limit=100&after=" +id,{
headers: this.headers,
}
)
@ -1326,15 +1382,11 @@ class Channel extends SnowFlake{
}
notititle(message: Message): string{
return(
message.author.username +
" > " +
this.guild.properties.name +
" > " +
this.name
message.author.username + " > " + this.guild.properties.name + " > " + this.name
);
}
notify(message: Message, deep = 0){
Voice.noises(Voice.getNotificationSound());
AVoice.noises(AVoice.getNotificationSound());
if(!("Notification" in window)){
}else if(Notification.permission === "granted"){
let noticontent: string | undefined | null = message.content.textContent;

View file

@ -2,12 +2,12 @@ class Contextmenu<x, y>{
static currentmenu: HTMLElement | "";
name: string;
buttons: [
string,
(this: x, arg: y, e: MouseEvent) => void,
string | null,
(this: x, arg: y) => boolean,
(this: x, arg: y) => boolean,
string
string|(()=>string),
(this: x, arg: y, e: MouseEvent) => void,
string | null,
(this: x, arg: y) => boolean,
(this: x, arg: y) => boolean,
string
][];
div!: HTMLDivElement;
static setup(){
@ -27,7 +27,7 @@ class Contextmenu<x, y>{
this.buttons = [];
}
addbutton(
text: string,
text: string|(()=>string),
onclick: (this: x, arg: y, e: MouseEvent) => void,
img: null | string = null,
shown: (this: x, arg: y) => boolean = _=>true,
@ -58,7 +58,11 @@ class Contextmenu<x, y>{
const intext = document.createElement("button");
intext.disabled = !thing[4].bind(addinfo).call(addinfo, other);
intext.classList.add("contextbutton");
intext.textContent = thing[0];
if(thing[0] instanceof Function){
intext.textContent = thing[0]();
}else{
intext.textContent = thing[0];
}
console.log(thing);
if(thing[5] === "button" || thing[5] === "submenu"){
intext.onclick = thing[1].bind(addinfo, other);
@ -86,6 +90,13 @@ class Contextmenu<x, y>{
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
obj.addEventListener("touchstart",(event: TouchEvent)=>{
if(event.touches.length > 1){
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other);
}
});
return func;
}
static keepOnScreen(obj: HTMLElement){

View file

@ -11,13 +11,7 @@ type dialogjson =
| ["title", string]
| ["radio", string, string[], (this: unknown, e: string) => unknown, number]
| ["html", HTMLElement]
| [
"select",
string,
string[],
(this: HTMLSelectElement, e: Event) => unknown,
number
]
| ["select", string, string[], (this: HTMLSelectElement, e: Event) => unknown, number]
| ["tabs", [string, dialogjson][]];
class Dialog{
layout: dialogjson;
@ -197,11 +191,17 @@ class Dialog{
case"select": {
const div = document.createElement("div");
const label = document.createElement("label");
const selectSpan = document.createElement("span");
selectSpan.classList.add("selectspan");
const select = document.createElement("select");
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
label.textContent = array[1];
selectSpan.append(select);
selectSpan.append(selectArrow);
div.append(label);
div.appendChild(select);
div.appendChild(selectSpan);
for(const thing of array[2]){
const option = document.createElement("option");
option.textContent = thing;

View file

@ -153,8 +153,9 @@ class Group extends Channel{
const div = document.createElement("div");
Group.contextmenu.bindContextmenu(div, this,undefined);
this.html = new WeakRef(div);
div.classList.add("channeleffects");
div.classList.add("flexltr","liststyle");
const myhtml = document.createElement("span");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
div.appendChild(this.user.buildpfp());
div.appendChild(myhtml);

View file

@ -157,13 +157,11 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1];
if(this.json?.footer?.text){
const span = document.createElement("span");
span.textContent = this.json.footer.text;
span.classList.add("spaceright");
footer.append(span);
}
if(this.json?.footer && this.json?.timestamp){
const span = document.createElement("span");
span.textContent = "•";
span.classList.add("spaceright");
span.textContent = " • ";
footer.append(span);
}
if(this.json?.timestamp){
@ -288,7 +286,7 @@ json.guild;
guild as invitejson["guild"] & { info: { cdn: string } }
);
const iconrow = document.createElement("div");
iconrow.classList.add("flexltr", "flexstart");
iconrow.classList.add("flexltr");
iconrow.append(icon);
{
const guildinfo = document.createElement("div");

View file

@ -143,7 +143,7 @@ class Emoji{
title.classList.add("emojiTitle");
menu.append(title);
const selection = document.createElement("div");
selection.classList.add("flexltr", "dontshrink", "emojirow");
selection.classList.add("flexltr", "emojirow");
const body = document.createElement("div");
body.classList.add("emojiBody");

View file

@ -83,7 +83,9 @@ class File{
div.append(contained);
const controls = document.createElement("div");
const garbage = document.createElement("button");
garbage.textContent = "🗑";
const icon = document.createElement("span");
icon.classList.add("svgicon","svg-delete");
garbage.append(icon);
garbage.onclick = _=>{
div.remove();
files.splice(files.indexOf(file), 1);

View file

@ -11,28 +11,29 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<body class="Dark-theme" style="overflow-y: scroll;">
<body class="no-theme" style="overflow-y: scroll;">
<div id="titleDiv">
<img src="/logo.svg" width="40">
<h1 id="pageTitle">Jank Client</h1>
<a href="https://sb-jankclient.vanillaminigames.net/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat"
class="TitleButtons">
<h1>Spacebar Guild</h1>
Spacebar Guild
</a>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
<h1>Github</h1>
Github
</a>
<a href="/channels/@me" class="TitleButtons">
Open Client
</a>
</div>
<div class="flexttb">
<div id="homePage">
<div class="flexttb pagehead">
<h1>Welcome to Jank Client</h1>
</div>
<h1 class="pagehead">Welcome to Jank Client</h1>
<div class="pagebox">
<p>Jank Client is a spacebar compatible client seeking to be as good as it can be with many features including:
</p>
<p>Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:</p>
<ul>
<li>Direct Messaging</li>
<li>Reactions support</li>
@ -44,16 +45,16 @@
</ul>
</div>
<div class="pagebox">
<h2>Spacebar compatible Instances:</h2>
<h2>Spacebar-Compatible Instances:</h2>
<div id="instancebox">
</div>
</div>
<div class="pagebox">
<h2>Contribute to Jank Client</h2>
<p>We always appreciate some help, wether that be in the form of bug reports, or code, or even just pointing out
<p>We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out
some typos.</p><br>
</a><a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
<h1>Github</h1>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
Github
</a>
</div>
</div>

View file

@ -37,11 +37,11 @@ login?: string;
div.append(img);
}
const statbox = document.createElement("div");
statbox.classList.add("flexttb");
statbox.classList.add("flexttb","flexgrow");
{
const textbox = document.createElement("div");
textbox.classList.add("flexttb", "instatancetextbox");
textbox.classList.add("flexttb", "instancetextbox");
const title = document.createElement("h2");
title.innerText = instance.name;
if(instance.online !== undefined){

96
src/webpage/i18n.ts Normal file
View file

@ -0,0 +1,96 @@
type translation={
[key:string]:string|{[key:string]:string}
};
let res:()=>unknown=()=>{};
class I18n{
static lang:string;
static translations:{[key:string]:string}[]=[];
static done=new Promise<void>((res2,_reject)=>{
res=res2;
});
static async create(json:translation|string,lang:string){
if(typeof json === "string"){
json=await (await fetch(json)).json() as translation;
}
const translations:{[key:string]:string}[]=[];
let translation=json[lang];
if(!translation){
translation=json[lang[0]+lang[1]];
if(!translation){
console.error(lang+" does not exist in the translations");
translation=json["en"];
lang="en";
}
}
translations.push(await this.toTranslation(translation,lang));
if(lang!=="en"){
translations.push(await this.toTranslation(json["en"],"en"))
}
this.lang=lang;
this.translations=translations;
res();
}
static getTranslation(msg:string,...params:string[]):string{
let str:string|undefined;
for(const json of this.translations){
str=json[msg];
if(str){
break;
}
}
if(str){
return this.fillInBlanks(str,params);
}else{
throw new Error(msg+" not found")
}
}
static fillInBlanks(msg:string,params:string[]):string{
//thanks to geotale for the regex
msg=msg.replace(/{{(.+?)}}/g,
(str, match:string) => {
const [op,strsSplit]=this.fillInBlanks(match,params).split(":");
const [first,...strs]=strsSplit.split("|");
switch(op.toUpperCase()){
case "PLURAL":{
const numb=Number(first);
if(numb===0){
return strs[strs.length-1];
}
return strs[Math.min(strs.length-1,numb-1)];
}
case "GENDER":{
if(first==="male"){
return strs[0];
}else if(first==="female"){
return strs[1];
}else if(first==="neutral"){
if(strs[2]){
return strs[2];
}else{
return strs[0];
}
}
}
}
return str;
}
);
msg=msg.replace(/\$\d+/g,(str, match:string) => {
const number=Number(match);
if(params[number-1]){
return params[number-1];
}else{
return str;
}
});
return msg;
}
private static async toTranslation(trans:string|{[key:string]:string},lang:string):Promise<{[key:string]:string}>{
if(typeof trans==='string'){
return this.toTranslation((await (await fetch(trans)).json() as translation)[lang],lang);
}else{
return trans;
}
}
}
export{I18n};

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="#c9c9c9" stroke="red" stroke-linecap="round" stroke-width="24" d="M90 168V12M12 90h156"/></svg>

After

Width:  |  Height:  |  Size: 169 B

1
src/webpage/icons/x.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="red" d="M90 0A90 90 0 0 0 0 90a90 90 0 0 0 90 90 90 90 0 0 0 90-90A90 90 0 0 0 90 0zM61.7 49.7a12 12 0 0 1 8.5 3.5L90 73l19.8-19.8a12 12 0 0 1 8.5-3.5 12 12 0 0 1 8.5 3.5 12 12 0 0 1 0 17L107 90l19.8 19.8a12 12 0 0 1 0 17 12 12 0 0 1-17 0L90 107l-19.8 19.8a12 12 0 0 1-17 0 12 12 0 0 1 0-17L73 90 53.2 70.2a12 12 0 0 1 0-17 12 12 0 0 1 8.5-3.5z"/></svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -11,15 +11,15 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme,#loading{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme,#loading{background:#9397bd;}}</style>
<link rel="manifest" href="/manifest.json">
</head>
<body class="Dark-theme">
<body class="no-theme">
<script src="/index.js" type="module"></script>
<div id="loading" class="loading">
<div id="centerdiv">
<div class="centeritem">
<img src="/logo.svg" style="width:3in;height:3in;">
<h1>Jank Client is loading</h1>
<h2 id="load-desc">This shouldn't take long</h2>
@ -27,50 +27,62 @@
</div>
</div>
<div class="flexltr" id="page">
<div id="neunence">
<div id="servers"></div>
</div>
<div id="servers"></div>
<div class="flexttb channelflex">
<div class="servertd" id="servertd">
<h2 id="serverName">Server Name</h2>
<div class="flexltr header" id="servertd">
<h2 id="serverName" class="ellipsis">Server Name</h2>
</div>
<div id="channels"></div>
<div class="flexltr" id="userdock">
<div class="flexltr" id="userinfo">
<img id="userpfp" class="pfp">
<div class="userflex">
<p id="username">USERNAME</p>
<p id="status">STATUS</p>
</div>
<div class="flexttb">
<div class="flexltr" id="VoiceBox">
<span id="VoiceStatus"></span>
</div>
<div class="flexltr" id="userdock">
<div class="flexltr" id="userinfo">
<img id="userpfp" class="pfp">
<div id="user-actions">
<span id="settings" class="svgtheme svg-settings"></span>
<div class="flexttb userflex">
<p id="username">USERNAME</p>
<p id="status">STATUS</p>
</div>
</div>
<div id="user-actions">
<span id="settings" class="svgicon svg-settings"></span>
</div>
</div>
</div>
</div>
<div class="flexttb messageflex">
<div class="servertd channelnamediv">
<span id="mobileback" hidden></span>
<span id="channelname">Channel name</span>
<span id="channelTopic" hidden>Channel topic</span>
<div class="flexttb flexgrow" id="mainarea">
<div class="flexltr header channelnamediv">
<label for="maintoggle" id="maintoggleicon">
<span class="svgicon svg-category"></span>
</label>
<input type="checkbox" id="maintoggle">
<span class="flexltr">
<span id="channelname">Channel name</span>
<span id="channelTopic" class="ellipsis" hidden>Channel topic</span>
</span>
<label for="memberlisttoggle" id="memberlisttoggleicon">
<span class="svgicon svg-channel"></span>
</label>
<input type="checkbox" id="memberlisttoggle" checked>
</div>
<div class="flexltr">
<div class="flexttb">
<div id="channelw">
<div class="flexltr flexgrow">
<div class="flexttb flexgrow">
<div id="channelw" class="flexltr">
<div id="loadingdiv">
</div>
</div>
<div id="pasteimage"></div>
<div id="pasteimage" class="flexltr"></div>
<div id="replybox" class="hideReplyBox"></div>
<div id="typediv">
<div id="realbox">
<div id="typebox" contentEditable="true"></div>
</div>
<div id="typing" class="hidden">
<div id="typing" class="hidden flexltr">
<p id="typingtext">typing</p>
<div class="loading-indicator">
<div class="flexltr loading-indicator">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>

View file

@ -22,7 +22,7 @@ import{ File }from"./file.js";
function showAccountSwitcher(): void{
const table = document.createElement("div");
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
for(const user of Object.values(users.users)){
const specialUser = user as Specialuser;
@ -235,25 +235,12 @@ import{ File }from"./file.js";
userSettings;
if(mobile){
const channelWrapper = document.getElementById(
"channelw"
) as HTMLDivElement;
const channelWrapper = document.getElementById("channelw") as HTMLDivElement;
channelWrapper.onclick = ()=>{
(
document.getElementById("channels")!.parentNode as HTMLElement
).classList.add("collapse");
document.getElementById("servertd")!.classList.add("collapse");
document.getElementById("servers")!.classList.add("collapse");
};
const mobileBack = document.getElementById("mobileback") as HTMLDivElement;
mobileBack.textContent = "#";
mobileBack.onclick = ()=>{
(
document.getElementById("channels")!.parentNode as HTMLElement
).classList.remove("collapse");
document.getElementById("servertd")!.classList.remove("collapse");
document.getElementById("servers")!.classList.remove("collapse");
const toggle = document.getElementById("maintoggle") as HTMLInputElement;
toggle.checked = true;
};
const memberListToggle = document.getElementById("memberlisttoggle") as HTMLInputElement;
memberListToggle.checked = false;
}
})();

View file

@ -39,7 +39,7 @@ offset: number
}
const scroll = document.createElement("div");
scroll.classList.add("flexttb", "scroller");
scroll.classList.add("scroller");
this.div = scroll;
this.div.addEventListener("scroll", ()=>{

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -10,14 +11,17 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div>
<div id="invitebody">
<div id="inviteimg"></div>
<h1 id="invitename">Server Name</h1>
<p id="invitedescription">Someone invited you to Server Name</p>
<button id="AcceptInvite">Accept Invite</button>
<body class="no-theme">
<div>
<div id="invitebody">
<div id="inviteimg"></div>
<h1 id="invitename">Server Name</h1>
<p id="invitedescription">Someone invited you to Server Name</p>
<button id="AcceptInvite">Accept Invite</button>
</div>
</div>
</div>
<script type="module" src="/invite.js"></script>
</body>
<script type="module" src="/invite.js"></script>
</body>
</html>

View file

@ -136,7 +136,7 @@ document.getElementById("inviteimg")!.append(div);
}
table.append(td);
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
console.log(table);
document.body.append(table);
}

View file

@ -408,79 +408,109 @@ type wsjson =
| "MESSAGE_REACTION_REMOVE_EMOJI";
}
| {
op: 0;
t: "GUILD_MEMBERS_CHUNK";
d: memberChunk;
s: number;
op: 0;
t: "GUILD_MEMBERS_CHUNK";
d: memberChunk;
s: number;
}
| {
op: 0;
d: {
id: string;
guild_id?: string;
channel_id: string;
};
s: number;
t: "MESSAGE_DELETE";
op: 0;
d: {
id: string;
guild_id?: string;
channel_id: string;
};
s: number;
t: "MESSAGE_DELETE";
}
| {
op: 0;
d: {
guild_id?: string;
channel_id: string;
} & messagejson;
s: number;
t: "MESSAGE_UPDATE";
op: 0;
d: {
guild_id?: string;
channel_id: string;
} & messagejson;
s: number;
t: "MESSAGE_UPDATE";
}
| messageCreateJson
| readyjson
| {
op: 11;
s: undefined;
d: {};
op: 11;
s: undefined;
d: {};
}
| {
op: 10;
s: undefined;
d: {
heartbeat_interval: number;
};
op: 10;
s: undefined;
d: {
heartbeat_interval: number;
};
}
| {
op: 0;
t: "MESSAGE_REACTION_ADD";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id?: string;
emoji: emojijson;
member?: memberjson;
};
s: number;
op: 0;
t: "MESSAGE_REACTION_ADD";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id?: string;
emoji: emojijson;
member?: memberjson;
};
s: number;
}
| {
op: 0;
t: "MESSAGE_REACTION_REMOVE";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id: string;
emoji: emojijson;
};
s: 3;
}|memberlistupdatejson;
type memberChunk = {
guild_id: string;
nonce: string;
members: memberjson[];
presences: presencejson[];
chunk_index: number;
chunk_count: number;
not_found: string[];
};
op: 0;
t: "MESSAGE_REACTION_REMOVE";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id: string;
emoji: emojijson;
};
s: 3;
}|memberlistupdatejson|voiceupdate|voiceserverupdate;
type memberChunk = {
guild_id: string;
nonce: string;
members: memberjson[];
presences: presencejson[];
chunk_index: number;
chunk_count: number;
not_found: string[];
};
type voiceupdate={
op: 0,
t: "VOICE_STATE_UPDATE",
d: {
guild_id: string,
channel_id: string,
user_id: string,
member: memberjson,
session_id: string,
token: string,
deaf: boolean,
mute: boolean,
self_deaf: boolean,
self_mute: boolean,
self_video: boolean,
suppress: boolean
},
s: number
};
type voiceserverupdate={
op: 0,
t: "VOICE_SERVER_UPDATE",
d: {
token: string,
guild_id: string,
endpoint: string
},
s: 6
};
type memberlistupdatejson={
op: 0,
s: number,
@ -511,7 +541,73 @@ type memberlistupdatejson={
count: number,
id: string
}[]
}
}
type webRTCSocket= {
op: 8,
d: {
heartbeat_interval: number
}
}|{
op:6,
d:{t: number}
}|{
op: 2,
d: {
ssrc: number,
"streams": {
type: "video",//probally more options, but idk
rid: string,
quality: number,
ssrc: number,
rtx_ssrc:number
}[],
ip: number,
port: number,
"modes": [],//no clue
"experiments": []//no clue
}
}|sdpback|opRTC12|{
op: 5,
d: {
user_id: string,
speaking: 0,
ssrc: 940464811
}
};
type sdpback={
op: 4,
d: {
audioCodec: string,
videoCodec: string,
media_session_id: string,
sdp: string
}
};
type opRTC12={
op: 12,
d: {
user_id: string,
audio_ssrc: number,
video_ssrc: number,
streams: [
{
type: "video",
rid: "100",
ssrc: number,
active: boolean,
quality: 100,
rtx_ssrc: number,
max_bitrate: 2500000,
max_framerate: number,
max_resolution: {
type: "fixed",
width: number,
height: number
}
}
]
}
}
export{
readyjson,
@ -532,5 +628,10 @@ export{
messageCreateJson,
memberChunk,
invitejson,
memberlistupdatejson
memberlistupdatejson,
voiceupdate,
voiceserverupdate,
webRTCSocket,
sdpback,
opRTC12
};

View file

@ -1,35 +1,23 @@
import{ Guild }from"./guild.js";
import{ Channel }from"./channel.js";
import{ Direct }from"./direct.js";
import{ Voice }from"./audio.js";
import{ AVoice }from"./audio.js";
import{ User }from"./user.js";
import{ Dialog }from"./dialog.js";
import{ getapiurls, getBulkInfo, setTheme, Specialuser }from"./login.js";
import{
channeljson,
guildjson,
mainuserjson,
memberjson,
memberlistupdatejson,
messageCreateJson,
presencejson,
readyjson,
startTypingjson,
wsjson,
}from"./jsontypes.js";
import{channeljson,guildjson,mainuserjson,memberjson,memberlistupdatejson,messageCreateJson,presencejson,readyjson,startTypingjson,wsjson,}from"./jsontypes.js";
import{ Member }from"./member.js";
import{ Form, FormError, Options, Settings }from"./settings.js";
import{ MarkDown }from"./markdown.js";
import { Bot } from "./bot.js";
import { Role } from "./role.js";
import { VoiceFactory } from "./voice.js";
import { I18n } from "./i18n.js";
const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]);
class Localuser{
badges: Map<
string,
{ id: string; description: string; icon: string; link: string }
> = new Map();
badges: Map<string,{ id: string; description: string; icon: string; link: string }> = new Map();
lastSequence: number | null = null;
token!: string;
userinfo!: Specialuser;
@ -52,6 +40,7 @@ class Localuser{
errorBackoff = 0;
channelids: Map<string, Channel> = new Map();
readonly userMap: Map<string, User> = new Map();
voiceFactory?:VoiceFactory;
instancePing = {
name: "Unknown",
};
@ -76,14 +65,19 @@ class Localuser{
"Content-type": "application/json; charset=UTF-8",
Authorization: this.userinfo.token,
};
I18n.create("/translations/en.json","en")
}
gottenReady(ready: readyjson): void{
async gottenReady(ready: readyjson): Promise<void>{
await I18n.done;
this.initialized = true;
this.ready = ready;
this.guilds = [];
this.guildids = new Map();
this.user = new User(ready.d.user, this);
this.user.setstatus("online");
this.voiceFactory=new VoiceFactory({id:this.user.id});
this.handleVoice();
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
this.userinfo.username = this.user.username;
this.userinfo.pfpsrc = this.user.getpfpsrc();
@ -92,10 +86,11 @@ class Localuser{
this.lookingguild = undefined;
this.guildhtml = new Map();
const members: { [key: string]: memberjson } = {};
for(const thing of ready.d.merged_members){
members[thing[0].guild_id] = thing[0];
if(ready.d.merged_members){
for(const thing of ready.d.merged_members){
members[thing[0].guild_id] = thing[0];
}
}
for(const thing of ready.d.guilds){
const temp = new Guild(thing, this, members[thing.id]);
this.guilds.push(temp);
@ -127,6 +122,7 @@ class Localuser{
this.pingEndpoint();
this.userinfo.updateLocal();
}
outoffocus(): void{
const servers = document.getElementById("servers") as HTMLDivElement;
@ -205,10 +201,10 @@ class Localuser{
try{
const temp = JSON.parse(build);
build = "";
await this.handleEvent(temp);
if(temp.op === 0 && temp.t === "READY"){
returny();
}
await this.handleEvent(temp);
}catch{}
}
})();
@ -236,9 +232,9 @@ class Localuser{
if(
!(
array[len - 1] === 255 &&
array[len - 2] === 255 &&
array[len - 3] === 0 &&
array[len - 4] === 0
array[len - 2] === 255 &&
array[len - 3] === 0 &&
array[len - 4] === 0
)
){
return;
@ -249,10 +245,11 @@ class Localuser{
}else{
temp = JSON.parse(event.data);
}
await this.handleEvent(temp as readyjson);
if(temp.op === 0 && temp.t === "READY"){
returny();
}
await this.handleEvent(temp as readyjson);
}catch(e){
console.error(e);
}finally{
@ -372,7 +369,7 @@ class Localuser{
break;
}
case"READY":
this.gottenReady(temp as readyjson);
await this.gottenReady(temp as readyjson);
break;
case"MESSAGE_UPDATE": {
temp.d.guild_id ??= "@me";
@ -486,8 +483,20 @@ class Localuser{
this.memberListUpdate(temp)
break;
}
case "VOICE_STATE_UPDATE":
if(this.voiceFactory){
this.voiceFactory.voiceStateUpdate(temp)
}
break;
case "VOICE_SERVER_UPDATE":
if(this.voiceFactory){
this.voiceFactory.voiceServerUpdate(temp)
}
break;
}
}else if(temp.op === 10){
if(!this.ws)return;
console.log("heartbeat down");
@ -501,6 +510,30 @@ class Localuser{
}, this.heartbeat_interval);
}
}
get currentVoice(){
return this.voiceFactory?.currentVoice;
}
async joinVoice(channel:Channel){
if(!this.voiceFactory) return;
if(!this.ws) return;
this.ws.send(JSON.stringify(this.voiceFactory.joinVoice(channel.id,channel.guild.id)));
return undefined;
}
changeVCStatus(status:string){
const statuselm=document.getElementById("VoiceStatus");
if(!statuselm) throw new Error("Missing status element");
statuselm.textContent=status;
}
handleVoice(){
if(this.voiceFactory){
this.voiceFactory.onJoin=voice=>{
voice.onSatusChange=status=>{
this.changeVCStatus(status);
}
}
}
}
heartbeat_interval: number = 0;
updateChannel(json: channeljson): void{
const guild = this.guildids.get(json.guild_id);
@ -603,11 +636,12 @@ class Localuser{
const memberdiv=document.createElement("div");
const pfp=await member.user.buildstatuspfp();
const username=document.createElement("span");
username.classList.add("ellipsis");
username.textContent=member.name;
member.bind(username)
member.user.bind(memberdiv,member.guild,false);
memberdiv.append(pfp,username);
memberdiv.classList.add("flexltr");
memberdiv.classList.add("flexltr","liststyle");
membershtml.append(memberdiv);
}
category.append(membershtml);
@ -722,7 +756,7 @@ class Localuser{
const div = document.createElement("div");
div.classList.add("home", "servericon");
home.classList.add("svgtheme", "svgicon", "svg-home");
home.classList.add("svgicon", "svg-home");
home.all = this.guildids.get("@me");
(this.guildids.get("@me") as Guild).html = outdiv;
const unread = document.createElement("div");
@ -760,19 +794,17 @@ class Localuser{
br.id = "bottomseparator";
const div = document.createElement("div");
div.textContent = "+";
const plus = document.createElement("span");
plus.classList.add("svgicon", "svg-plus");
div.classList.add("home", "servericon");
div.appendChild(plus);
serverlist.appendChild(div);
div.onclick = _=>{
this.createGuild();
};
const guilddsdiv = document.createElement("div");
const guildDiscoveryContainer = document.createElement("span");
guildDiscoveryContainer.classList.add(
"svgtheme",
"svgicon",
"svg-explore"
);
guildDiscoveryContainer.classList.add("svgicon", "svg-explore");
guilddsdiv.classList.add("home", "servericon");
guilddsdiv.appendChild(guildDiscoveryContainer);
serverlist.appendChild(guilddsdiv);
@ -838,7 +870,7 @@ class Localuser{
["title", "Create a guild"],
[
"fileupload",
"Icon:",
"Icon: ",
function(event: Event){
const target = event.target as HTMLInputElement;
if(!target.files)return;
@ -861,7 +893,7 @@ class Localuser{
[
"button",
"",
"submit",
"Submit",
()=>{
this.makeGuild(fields).then(_=>{
if(_.message){
@ -889,7 +921,7 @@ class Localuser{
}
async guildDiscovery(){
const content = document.createElement("div");
content.classList.add("guildy");
content.classList.add("flexttb","guildy");
content.textContent = "Loading...";
const full = new Dialog(["html", content]);
full.show();
@ -1104,7 +1136,7 @@ class Localuser{
});
let changed = false;
const pronounbox = settingsLeft.addTextInput(
"Pronouns",
"Pronouns:",
_=>{
if(newpronouns || newbio || changed){
this.updateProfile({
@ -1136,7 +1168,7 @@ class Localuser{
color = "transparent";
}
const colorPicker = settingsLeft.addColorInput(
"Profile color",
"Profile color:",
_=>{},
{ initColor: color }
);
@ -1149,9 +1181,9 @@ class Localuser{
});
}
{
const tas = settings.addButton("Themes & sounds");
const tas = settings.addButton("Themes & Sounds");
{
const themes = ["Dark", "WHITE", "Light"];
const themes = ["Dark", "WHITE", "Light", "Dark-Accent"];
tas.addSelect(
"Theme:",
_=>{
@ -1167,18 +1199,18 @@ class Localuser{
);
}
{
const sounds = Voice.sounds;
const sounds = AVoice.sounds;
tas
.addSelect(
"Notification sound:",
_=>{
Voice.setNotificationSound(sounds[_]);
AVoice.setNotificationSound(sounds[_]);
},
sounds,
{ defaultIndex: sounds.indexOf(Voice.getNotificationSound()) }
{ defaultIndex: sounds.indexOf(AVoice.getNotificationSound()) }
)
.watchForChange(_=>{
Voice.noises(sounds[_]);
AVoice.noises(sounds[_]);
});
}
@ -1427,9 +1459,9 @@ class Localuser{
}
);
form.addTextInput("Name", "name", { required: true });
form.addTextInput("Name:", "name", { required: true });
form.addSelect(
"Team",
"Team:",
"team_id",
["Personal", ...teams.map((team: { name: string })=>team.name)],
{
@ -1545,7 +1577,7 @@ class Localuser{
});
form.addTextInput("Bot username:","username",{initText:bot.username});
form.addFileInput("Bot avatar:","avatar");
form.addButtonInput("Reset Token:","Reset",async ()=>{
form.addButtonInput("","Reset Token",async ()=>{
if(!confirm("Are you sure you want to reset the bot token? Your bot will stop working until you update it.")){
return;
}
@ -1585,7 +1617,7 @@ class Localuser{
this.userinfo.updateLocal();
}
});
form.addButtonInput("","Advanced bot settings",()=>{
form.addButtonInput("","Advanced Bot Settings",()=>{
const token=this.botTokens.get(appId);
if(token){
const botc=new Bot(bot,token,this);
@ -1758,16 +1790,8 @@ class Localuser{
this.pageTitle("Loading...");
}
pageTitle(channelName = "", guildName = ""){
(document.getElementById("channelname") as HTMLSpanElement).textContent =
channelName;
(
document.getElementsByTagName("title")[0] as HTMLTitleElement
).textContent =
channelName +
(guildName ? " | " + guildName : "") +
" | " +
this.instancePing.name +
" | Jank Client";
(document.getElementById("channelname") as HTMLSpanElement).textContent = channelName;
(document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent = channelName + (guildName ? " | " + guildName : "") + " | " + this.instancePing.name +" | Jank Client";
}
async instanceStats(){
const res = await fetch(this.info.api + "/policies/stats", {

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -12,51 +13,49 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div id="logindiv">
<h1>Login</h1>
<br >
<form id="form" submit="check(e)">
<label for="instance"><b>Instance:</b></label
><br >
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
id="instancein"
required
><br ><br >
<body class="no-theme">
<div id="logindiv">
<h1>Login</h1>
<form id="form" submit="check(e)">
<label for="instance"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
required
>
<label for="uname"><b>Email:</b></label
><br >
<input
type="text"
placeholder="Enter email address"
name="uname"
id="uname"
required
><br ><br >
<label for="uname"><b>Email:</b></label>
<input
type="text"
placeholder="Enter email address"
name="uname"
id="uname"
required
>
<label for="psw"><b>Password:</b></label
><br >
<input
type="password"
placeholder="Enter Password"
name="psw"
id="psw"
required
><br ><br ><br ><br >
<p class="wrongred" id="wrong"></p>
<label for="psw"><b>Password:</b></label>
<input
type="password"
placeholder="Enter Password"
name="psw"
id="psw"
required
>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<button type="submit">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script>
</body>
<div id="h-captcha"></div>
<button type="submit">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script>
</body>
</html>

View file

@ -95,7 +95,7 @@ function setDefaults(){
userinfos.users = {};
}
if(userinfos.accent_color === undefined){
userinfos.accent_color = "#242443";
userinfos.accent_color = "#3096f7";
}
document.documentElement.style.setProperty(
"--accent-color",
@ -116,12 +116,12 @@ function setDefaults(){
setDefaults();
class Specialuser{
serverurls: {
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
};
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
};
email: string;
token: string;
loggedin;

View file

@ -765,12 +765,12 @@ txt[j + 1] === undefined)
}else{
const full: Dialog = new Dialog([
"vdiv",
["title", "You're leaving spacebar"],
["title", "You're leaving Spacebar"],
[
"text",
"You're going to " +
Url.host +
" are you sure you want to go there?",
". Are you sure you want to go there?",
],
[
"hdiv",

View file

@ -11,6 +11,8 @@ import{ SnowFlake }from"./snowflake.js";
import{ memberjson, messagejson }from"./jsontypes.js";
import{ Emoji }from"./emoji.js";
import{ Dialog }from"./dialog.js";
import{ mobile }from"./login.js";
import { I18n } from "./i18n.js";
class Message extends SnowFlake{
static contextmenu = new Contextmenu<Message, undefined>("message menu");
@ -44,9 +46,7 @@ class Message extends SnowFlake{
return this.weakdiv?.deref();
}
//*/
div:
| (HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })
| undefined;
div:(HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })| undefined;
member: Member | undefined;
reactions!: messagejson["reactions"];
static setup(){
@ -56,13 +56,13 @@ class Message extends SnowFlake{
Message.setupcmenu();
}
static setupcmenu(){
Message.contextmenu.addbutton("Copy raw text", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"copyrawtext"), function(this: Message){
navigator.clipboard.writeText(this.content.rawString);
});
Message.contextmenu.addbutton("Reply", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"reply"), function(this: Message){
this.channel.setReplying(this);
});
Message.contextmenu.addbutton("Copy message id", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"copymessageid"), function(this: Message){
navigator.clipboard.writeText(this.id);
});
Message.contextmenu.addsubmenu(
@ -404,22 +404,19 @@ class Message extends SnowFlake{
}
if(this.message_reference){
const replyline = document.createElement("div");
const line = document.createElement("hr");
const minipfp = document.createElement("img");
minipfp.classList.add("replypfp");
replyline.appendChild(line);
replyline.appendChild(minipfp);
const username = document.createElement("span");
replyline.appendChild(username);
const reply = document.createElement("div");
username.classList.add("username");
reply.classList.add("replytext");
reply.classList.add("replytext","ellipsis");
replyline.appendChild(reply);
const line2 = document.createElement("hr");
replyline.appendChild(line2);
line2.classList.add("reply");
line.classList.add("startreply");
replyline.classList.add("replyflex");
replyline.classList.add("flexltr","replyflex");
// TODO: Fix this
this.channel.getmessage(this.message_reference.message_id).then(message=>{
if(message.author.relationshipType === 2){
@ -442,7 +439,6 @@ class Message extends SnowFlake{
div.appendChild(build);
if({ 0: true, 19: true }[this.type] || this.attachments.length !== 0){
const pfpRow = document.createElement("div");
pfpRow.classList.add("flexltr");
let pfpparent, current;
if(premessage != null){
pfpparent ??= premessage;
@ -466,10 +462,7 @@ class Message extends SnowFlake{
pfpRow.classList.add("pfprow");
build.appendChild(pfpRow);
const text = document.createElement("div");
text.classList.add("flexttb");
const texttxt = document.createElement("div");
texttxt.classList.add("commentrow", "flexttb");
text.appendChild(texttxt);
text.classList.add("commentrow", "flexttb");
if(combine){
const username = document.createElement("span");
username.classList.add("username");
@ -477,7 +470,6 @@ class Message extends SnowFlake{
div.classList.add("topMessage");
username.textContent = this.author.username;
const userwrap = document.createElement("div");
userwrap.classList.add("flexltr");
userwrap.appendChild(username);
if(this.author.bot){
const username = document.createElement("span");
@ -490,7 +482,7 @@ class Message extends SnowFlake{
time.classList.add("timestamp");
userwrap.appendChild(time);
texttxt.appendChild(userwrap);
text.appendChild(userwrap);
}else{
div.classList.remove("topMessage");
}
@ -499,13 +491,13 @@ class Message extends SnowFlake{
const messagedwrap = document.createElement("div");
messagedwrap.classList.add("flexttb");
messagedwrap.appendChild(messaged);
texttxt.appendChild(messagedwrap);
text.appendChild(messagedwrap);
build.appendChild(text);
if(this.attachments.length){
console.log(this.attachments);
const attach = document.createElement("div");
attach.classList.add("flexltr");
attach.classList.add("flexltr","attachments");
for(const thing of this.attachments){
attach.appendChild(thing.getHTML());
}
@ -513,7 +505,6 @@ class Message extends SnowFlake{
}
if(this.embeds.length){
const embeds = document.createElement("div");
embeds.classList.add("flexltr");
for(const thing of this.embeds){
embeds.appendChild(thing.generateHTML());
}
@ -522,27 +513,23 @@ class Message extends SnowFlake{
//
}else if(this.type === 7){
const text = document.createElement("div");
text.classList.add("flexttb");
const texttxt = document.createElement("div");
text.appendChild(texttxt);
build.appendChild(text);
texttxt.classList.add("flexltr");
const messaged = document.createElement("span");
div.txt = messaged;
messaged.textContent = "welcome: ";
texttxt.appendChild(messaged);
text.appendChild(messaged);
const username = document.createElement("span");
username.textContent = this.author.username;
//this.author.profileclick(username);
this.author.bind(username, this.guild);
texttxt.appendChild(username);
text.appendChild(username);
username.classList.add("username");
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
texttxt.append(time);
text.append(time);
div.classList.add("topMessage");
}
const reactions = document.createElement("div");
@ -557,6 +544,7 @@ class Message extends SnowFlake{
if(this.div){
let buttons: HTMLDivElement | undefined;
this.div.onmouseenter = _=>{
if(mobile)return;
if(buttons){
buttons.remove();
buttons = undefined;
@ -565,9 +553,9 @@ class Message extends SnowFlake{
buttons = document.createElement("div");
buttons.classList.add("messageButtons", "flexltr");
if(this.channel.hasPermission("SEND_MESSAGES")){
const container = document.createElement("div");
const container = document.createElement("button");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-reply", "svgicon");
reply.classList.add("svg-reply", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _=>{
@ -575,9 +563,9 @@ class Message extends SnowFlake{
};
}
if(this.author === this.localuser.user){
const container = document.createElement("div");
const container = document.createElement("button");
const edit = document.createElement("span");
edit.classList.add("svgtheme", "svg-edit", "svgicon");
edit.classList.add("svg-edit", "svgicon");
container.append(edit);
buttons.append(container);
container.onclick = _=>{
@ -585,9 +573,9 @@ class Message extends SnowFlake{
};
}
if(this.canDelete()){
const container = document.createElement("div");
const container = document.createElement("button");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-delete", "svgicon");
reply.classList.add("svg-delete", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _=>{
@ -596,25 +584,28 @@ class Message extends SnowFlake{
return;
}
const diaolog = new Dialog([
"hdiv",
["title", "are you sure you want to delete this?"],
"vdiv",
["title", "Are you sure you want to delete this?"],
[
"button",
"",
"yes",
()=>{
this.delete();
diaolog.hide();
},
],
[
"button",
"",
"no",
()=>{
diaolog.hide();
},
],
"hdiv",
[
"button",
"",
"Yes",
()=>{
this.delete();
diaolog.hide();
},
],
[
"button",
"",
"No",
()=>{
diaolog.hide();
},
],
]
]);
diaolog.show();
};

View file

@ -102,18 +102,24 @@ type botjsonfetch={
}
}
const dialog=document.createElement("dialog");
dialog.classList.add("accountSwitcher");
dialog.classList.add("flexttb","accountSwitcher");
const h1=document.createElement("h1");
dialog.append(h1);
h1.textContent="Invite to server:";
const select=document.createElement("select");
const selectSpan=document.createElement("span");
selectSpan.classList.add("selectspan");
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
for(const guild of guilds){
const option=document.createElement("option");
option.textContent=guild.name;
option.value=guild.id;
select.append(option);
}
dialog.append(select);
selectSpan.append(select);
selectSpan.append(selectArrow);
dialog.append(selectSpan);
const button=document.createElement("button");
button.textContent="Invite";
dialog.append(button);
@ -193,7 +199,7 @@ type botjsonfetch={
}
table.append(td);
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
console.log(table);
document.body.append(table);
}
@ -221,7 +227,7 @@ type botjsonfetch={
const int = Number((BigInt(json.bot.id) >> 22n) % 6n);
pfp.src=`${urls.cdn}/embed/avatars/${int}.png`;
}
const perms=document.getElementById("permsions") as HTMLDivElement;
const perms=document.getElementById("permissions") as HTMLDivElement;
if(perms&&permstr){
const permisions=new Permissions(permstr)

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -9,15 +10,18 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div>
<div id="invitebody">
<img id="inviteimg" class="pfp"/>
<h1 id="invitename">Bot Name</h1>
<p id="invitedescription">Add Bot</p>
<div id="permsions"><h1>This will allow the bot to:</h1></div>
<button id="AcceptInvite">Add to server</button>
<body class="no-theme">
<div>
<div id="invitebody">
<img id="inviteimg" class="pfp"/>
<h1 id="invitename">Bot Name</h1>
<p id="invitedescription">Add Bot</p>
<div id="permissions"><h1>This will allow the bot to:</h1></div>
<button id="AcceptInvite">Add to server</button>
</div>
</div>
</div>
<script type="module" src="/oauth2/auth.js"></script>
</body>
<script type="module" src="/oauth2/auth.js"></script>
</body>
</html>

View file

@ -1,5 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -10,53 +10,55 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div id="logindiv">
<h1>Create an account</h1><br>
<form id="register" submit="registertry(e)">
<div>
<label for="instance"><b>Instance:</b></label><br>
<p id="verify"></p>
<input type="search" list="instances" placeholder="Instance URL" id="instancein" name="instance" value=""
id="instancein" required>
</div>
<div>
<label for="uname"><b>Email:</b></label><br>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required>
</div>
<body class="no-theme">
<div id="logindiv">
<h1>Create an account</h1>
<form id="register" submit="registertry(e)">
<div>
<label for="instance"><b>Instance:</b></label>
<p id="verify"></p>
<input type="search" list="instances" placeholder="Instance URL" id="instancein" name="instance" value="" required>
</div>
<div>
<label for="uname"><b>Email:</b></label>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required>
</div>
<div>
<label for="uname"><b>Username:</b></label><br>
<input type="text" placeholder="Enter Username" name="username" id="username" required>
</div>
<div>
<label for="psw"><b>Password:</b></label><br>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
</div>
<div>
<label for="uname"><b>Username:</b></label>
<input type="text" placeholder="Enter Username" name="username" id="username" required>
</div>
<div>
<label for="psw"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
</div>
<div>
<label for="psw2"><b>Enter password again:</b></label><br>
<input type="password" placeholder="Enter Password Again" name="psw2" id="psw2" required>
</div>
<div>
<label for="psw2"><b>Enter password again:</b></label>
<input type="password" placeholder="Enter Password Again" name="psw2" id="psw2" required>
</div>
<div>
<label for="date"><b>Date of birth:</b></label><br>
<input type="date" id="date" name="date">
</div>
<div>
<label for="date"><b>Date of birth:</b></label>
<input type="date" id="date" name="date">
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS">
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS">
</div>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha">
<p class="wrongred" id="wrong"></p>
<div id="h-captcha">
</div>
<button type="submit" class="dontgrow">Create account</button>
</form>
<a href="/login.html" id="switch">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script>
</body>
</div>
<button type="submit" class="dontgrow">Create account</button>
</form>
<a href="/login.html" id="switch">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script>
</body>
</html>

View file

@ -4,7 +4,7 @@ interface OptionsElement<x> {
submit: () => void;
readonly watchForChange: (func: (arg1: x) => void) => void;
value: x;
}
}
//future me stuff
class Buttons implements OptionsElement<unknown>{
readonly name: string;
@ -31,7 +31,7 @@ class Buttons implements OptionsElement<unknown>{
const htmlarea = document.createElement("div");
htmlarea.classList.add("flexgrow");
const buttonTable = document.createElement("div");
buttonTable.classList.add("flexttb", "settingbuttons");
buttonTable.classList.add("settingbuttons");
for(const thing of this.buttons){
const button = document.createElement("button");
button.classList.add("SettingsButton");
@ -244,9 +244,12 @@ class ButtonInput implements OptionsElement<void>{
}
generateHTML(): HTMLDivElement{
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
if(this.label){
const span = document.createElement("span");
span.classList.add("inlinelabel");
span.textContent = this.label;
div.append(span);
}
const button = document.createElement("button");
button.textContent = this.textContent;
button.onclick = this.onClickEvent.bind(this);
@ -338,6 +341,8 @@ class SelectInput implements OptionsElement<number>{
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const selectSpan = document.createElement("span");
selectSpan.classList.add("selectspan");
const select = document.createElement("select");
select.onchange = this.onChange.bind(this);
@ -348,7 +353,11 @@ class SelectInput implements OptionsElement<number>{
}
this.select = new WeakRef(select);
select.selectedIndex = this.index;
div.append(select);
selectSpan.append(select);
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
selectSpan.append(selectArrow);
div.append(selectSpan);
return div;
}
private onChange(){
@ -438,11 +447,13 @@ class FileInput implements OptionsElement<FileList | null>{
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const innerDiv = document.createElement("div");
innerDiv.classList.add("flexltr","fileinputdiv");
const input = document.createElement("input");
input.type = "file";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
innerDiv.append(input);
if(this.clear){
const button = document.createElement("button");
button.textContent = "Clear";
@ -453,8 +464,9 @@ class FileInput implements OptionsElement<FileList | null>{
this.value = null;
this.owner.changed();
};
div.append(button);
innerDiv.append(button);
}
div.append(innerDiv);
return div;
}
onChange(){
@ -719,7 +731,7 @@ class Options implements OptionsElement<void>{
title: WeakRef<HTMLElement> = new WeakRef(document.createElement("h2"));
generateHTML(): HTMLElement{
const div = document.createElement("div");
div.classList.add("titlediv");
div.classList.add("flexttb","titlediv");
const title = document.createElement("h2");
title.textContent = this.name;
div.append(title);
@ -1199,7 +1211,7 @@ class Settings extends Buttons{
}
show(){
const background = document.createElement("div");
background.classList.add("background");
background.classList.add("flexttb","menu","background");
const title = document.createElement("h2");
title.textContent = this.name;
@ -1209,8 +1221,7 @@ class Settings extends Buttons{
background.append(this.generateHTML());
const exit = document.createElement("span");
exit.textContent = "✖";
exit.classList.add("exitsettings");
exit.classList.add("exitsettings","svgicon","svg-x");
background.append(exit);
exit.onclick = _=>{
this.hide();

File diff suppressed because it is too large Load diff

View file

@ -1,178 +1,176 @@
:root {
--servertd-height: 0px;
/* Default value */
--red: red;
--green: green;
--yellow:yellow;
--accent-color:#242443;
--font: "acumin-pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
--black: #000000;
--red: #ff5555;
--yellow: #ffc159;
--green: #1c907b;
}
.Dark-theme { /* thanks to TomatoCake for the updated CSS vars and such*/
/* Themes. See themes.txt */
.Dark-theme {
color-scheme: dark;
--primary-text: #FFF;
--primary-bg: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
--black: #000;
--shadow: #000;
--focus: #8888ff;
--message-bg-hover: color-mix(in srgb, #191919 85%, var(--accent-color));
--typing-bg: #161616;
--timestamp-color: #a2a2a2;
--code-bg: color-mix(in srgb, #121212 95%, var(--accent-color));
--user-info-bg: color-mix(in srgb,#383838 85%, var(--accent-color));
--user-dock-bg: color-mix(in srgb,#111111 90%, var(--accent-color));
--channels-bg: color-mix(in srgb, #2a2a2a 90%, var(--accent-color));
--channel-hover: color-mix(in srgb, #121212 70%, var(--accent-color));
--blank-bg: #323232;
--light-border: #92929B;
--settings-hover: color-mix(in srgb, #000000 95%, var(--accent-color) 5%);
--quote-bg: #7a798e;
--button-bg: color-mix(in srgb, #191919 85%, var(--accent-color));
--button-hover: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
--textarea-bg: color-mix(in srgb, #484848 80%, var(--accent-color));
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #797979;
--profile-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
--profile-info-bg: color-mix(in srgb, #121212 90%, var(--accent-color));
--server-border: color-mix(in srgb, #000000 80%, var(--accent-color));
--channel-name-bg: color-mix(in srgb, #2a2a2a 80%, var(--accent-color));
--server-name-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
--reply-border: #474b76;
--reply-bg: #0b0d20;
--reply-text: #acacac;
--spoiler-hover: #111111;
--spoiler-open-bg: #1e1e1e;
--unknown-file-bg: #141316;
--unknown-file-border: #474555;
--login-border: #131315;
--loading-bg: #22232c;
--dialog-bg: #33363d;
--dialog-border: #1c1b31;
--scrollbar-track: #34313c;
--scrollbar-thumb: #201f29;
--scrollbar-thumb-hover: #16161f;
--markdown-timestamp: #2f2f33;
--embed: color-mix(in srgb, #131313 90%, var(--accent-color));
--link: #8888ff;
--discovery-bg: #37373b;
--message-jump:#7678b0;
--icon-color:white;
--server-list: color-mix(in srgb, #1d1d1d 90%, var(--accent-color));
--primary-bg: #303339;
--primary-hover: #272b31;
--primary-text: #dfdfdf;
--primary-text-soft: #adb8b9;
--secondary-bg: #16191b;
--secondary-hover: #252b2c;
--servers-bg: #141718;
--channels-bg: #25282b;
--channel-selected: #3c4046;
--typebox-bg: #3a3e45;
--button-bg: #4e5457;
--button-hover: #6b7174;
--spoiler-bg: #000000;
--link: #5ca9ed;
--primary-text-prominent: #efefef;
--dock-bg: #1b1e20;
--card-bg: #000000;
}
.WHITE-theme {
color-scheme: light;
--primary-text: #000;
--primary-bg: #FFF;
--black: #FFF;
--red: #dd6c6c;
--green: #639d63;
--shadow: #777;
--focus: #47bbff;
--message-bg-hover: #dedee2;
--typing-bg: #dad8d8;
--timestamp-color: #929297;
--code-bg: #cbcbcc;
--user-info-bg: #b0abc2;
--user-dock-bg: #b2b2c4;
--channels-bg: #cbcbd8;
--channel-hover: #b8b5cc;
--blank-bg: #ceccdd;
--light-border: #96959e;
--settings-hover: #b5b1bb;
--quote-bg: #7a798e;
--button-bg: #b7b7cc;
--button-hover: #FFF;
--textarea-bg: #b1b6ce;
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #6a6a6d;
--profile-bg: #cacad8;
--profile-info-bg: #bbbbce;
--server-border: #bebed3;
--channel-name-bg: #c0c0d4;
--server-name-bg: #a3a3b5;
--reply-border: #b1b2bd;
--reply-bg: #d4d6e9;
--reply-text: #2e2e30;
--spoiler-hover: #b9b9b9;
--spoiler-open-bg: #dadada;
--unknown-file-bg: #bdbdbd;
--unknown-file-border: #adadad;
--login-border: #c3c0e0;
--loading-bg: #b5b7cc;
--dialog-bg: #c1c8d6;
--dialog-border: #b9b7db;
--scrollbar-track: #d5d1e2;
--scrollbar-thumb: #b0afc0;
--scrollbar-thumb-hover: #a5a5b8;
--markdown-timestamp: #c8c8da;
--embed: #f2f3f5;
--link: #3333ee;
--discovery-bg: #c6c6d8;
--message-jump:#ccceff;
--icon-color:black;
--primary-bg: #fefefe;
--primary-hover: #f6f6f9;
--primary-text: #4b4b59;
--primary-text-soft: #656575;
--secondary-bg: #e0e0ea;
--secondary-hover: #d0d0dd;
--servers-bg: #b4b4ca;
--channels-bg: #eaeaf0;
--channel-selected: #c7c7d9;
--typebox-bg: #ededf4;
--button-bg: #cacad8;
--button-hover: #b3b3c4;
--spoiler-bg: #dadada;
--link: #056cd9;
--black: #4b4b59;
--green: #68d79d;
--primary-text-prominent: #08080d;
--secondary-text: #3c3c46;
--secondary-text-soft: #4c4c5a;
--dock-bg: #d1d1df;
--dock-hover: #b8b8d0;
}
.Light-theme {
color-scheme: light;
--primary-text: #000;
--primary-bg: #8e90c3;
--black: #fff;
--shadow: #000;
--focus: #5e50c5;
--primary-bg: #aaafce;
--primary-hover: #b1b6d4;
--primary-text: #060415;
--primary-text-soft: #424268;
--message-bg-hover: #5757b5;
--typing-bg: #d4d6e9;
--profile-bg: #8075bf;
--profile-info-bg: #8075bf;
--timestamp-color: #000000;
--code-bg: #a89adf;
--info-bg: #6060a3;
--user-info-bg: #796f9a;
--user-dock-bg: #83839d;
--channels-bg: #c2c2d1;
--channel-hover: #726e88;
--blank-bg: #5e50c5;
--light-border: #000000;
--settings-hover: #b5b1bb;
--quote-bg: #7a798e;
--button-bg: #5757b5;
--button-hover: #8e90c3;
--textarea-bg: #abb1cd;
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #202020;
--channel-name-bg: #c0c0d4;
--server-name-bg: #a3a3b5;
--secondary-bg: #9397bd;
--secondary-hover: #9ea5cc;
--server-border: #aaaac4;
--server-hover: #7f7fa8;
--servers-bg: #7a7aaa;
--channels-bg: #babdd2;
--channel-selected: #9c9fbf;
--typebox-bg: #bac0df;
--reply-border: #474b76;
--reply-bg: #d4d6e9;
--reply-text: #38383d;
--button-bg: #babdd2;
--button-hover: #9c9fbf;
--spoiler-bg: #34333a;
--link: #283c8b;
--spoiler-hover: #34333a;
--spoiler-open-bg: #767587;
--unknown-file-bg: #bdbdbd;
--unknown-file-border: #adadad;
--login-border: #c3c0e0;
--loading-bg: #b5b7cc;
--dialog-bg: #c1c8d6;
--dialog-border: #b9b7db;
--scrollbar-track: #d2cedf;
--scrollbar-thumb: #bdbcca;
--scrollbar-thumb-hover: #a7a7be;
--markdown-timestamp: #c8c8da;
--embed: #cdccd1;
--link: #5566cc;
--discovery-bg: #c6c6d8;
--message-jump:#ccceff;
--icon-color:black;
--black: #434392;
--red: #ca304d;
--secondary-text-soft: #211f2e;
--blank-bg: #494985;
--spoiler-text: #e4e6ed;
}
.Dark-Accent-theme {
color-scheme: dark;
--primary-bg: color-mix(in srgb, #3f3f3f 65%, var(--accent-color));
--primary-hover: color-mix(in srgb, #373737 68%, var(--accent-color));
--primary-text: #ebebeb;
--primary-text-soft: #ebebebb8;
--secondary-bg: color-mix(in srgb, #222222 72%, var(--accent-color));
--secondary-hover: color-mix(in srgb, #222222 65%, var(--accent-color));
--servers-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
--channels-bg: color-mix(in srgb, #292929 68%, var(--accent-color));
--channel-selected: color-mix(in srgb, #555555 65%, var(--accent-color));
--typebox-bg: color-mix(in srgb, #666666 60%, var(--accent-color));
--button-bg: color-mix(in srgb, #777777 56%, var(--accent-color));
--button-hover: color-mix(in srgb, #585858 58%, var(--accent-color));
--spoiler: color-mix(in srgb, #101010 72%, var(--accent-color));
--link: color-mix(in srgb, #99ccff 75%, var(--accent-color));
--black: color-mix(in srgb, #000000 90%, var(--accent-color));
--icon: color-mix(in srgb, #ffffff, var(--accent-color));
--dock-bg: color-mix(in srgb, #171717 68%, var(--accent-color));
--spoiler-hover: color-mix(in srgb, #111111 80%, var(--accent-color));
--card-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
}
/* Optional Variables */
body {
--primary-text-prominent: var(--primary-text);
--secondary-text: var(--primary-text);
--secondary-text-soft: var(--primary-text-soft);
--text-input-bg: var(--secondary-bg);
--button-text: var(--primary-text);
--button-disabled-text: color-mix(in srgb, var(--button-text), transparent);
--icon: var(--accent-color);
--focus: var(--accent-color);
--shadow: color-mix(in srgb, var(--black) 30%, transparent);
--scrollbar: var(--primary-text-soft);
--scrollbar-track: var(--primary-hover);
--blank-bg: var(--channels-bg);
--divider: color-mix(in srgb, var(--primary-text), transparent);
--channels-header-bg: var(--channels-bg);
--channel-hover: color-mix(in srgb, var(--channel-selected) 60%, transparent);
--dock-bg: var(--secondary-bg);
--dock-hover: var(--secondary-hover);
--user-info-bg: var(--dock-bg);
--user-info-text: var(--secondary-text);
--main-header-bg: transparent;
--message-jump-bg: color-mix(in srgb, var(--accent-color) 20%, transparent);
--code-bg: var(--secondary-bg);
--code-text: var(--secondary-text);
--spoiler-text: var(--primary-text);
--spoiler-hover: color-mix(in srgb, var(--spoiler-bg), var(--primary-text-soft) 10%);
--quote-line: color-mix(in srgb, var(--primary-text-soft), transparent);
--reply-line: color-mix(in srgb, var(--primary-text-soft) 20%, transparent);
--reply-text: var(--primary-text-soft);
--reply-highlight: var(--accent-color);
--mention: color-mix(in srgb, var(--accent-color) 80%, transparent);
--mention-highlight: var(--yellow);
--reaction-bg: var(--secondary-bg);
--reaction-reacted-bg: var(--secondary-hover);
--filename: var(--link);
--embed-bg: var(--secondary-bg);
--sidebar-bg: var(--channels-bg);
--sidebar-hover: var(--channel-hover);
--card-bg: var(--primary-bg);
--role-bg: var(--primary-bg);
--role-text: var(--primary-text);
--settings-bg: var(--primary-bg);
--settings-header-bg: var(--main-header-bg);
--settings-panel-bg: var(--channels-bg);
--settings-panel-selected: var(--channel-selected);
--settings-panel-hover: color-mix(in srgb, var(--settings-panel-selected), transparent);
--loading-bg: var(--secondary-bg);
--loading-text: var(--secondary-text);
}

View file

@ -0,0 +1,16 @@
{
"@metadata": {
"authors": [
"MathMan05"
],
"last-updated": "2024/15/24",
"locale": "en",
"comment":"Don't know how often I'll update this top part lol"
},
"en": {
"reply": "Reply",
"copyrawtext":"Copy raw text",
"copymessageid":"Copy message id"
},
"ru": "./ru.json"
}

View file

@ -0,0 +1,12 @@
{
"@metadata": {
"authors": [
],
"last-updated": "2024/15/24",
"locale": "ru",
"comment":"I need some help with this :P"
},
"ru": {
}
}

View file

@ -245,7 +245,7 @@ class User extends SnowFlake{
async buildstatuspfp(): Promise<HTMLDivElement>{
const div = document.createElement("div");
div.style.position = "relative";
div.classList.add("pfpDiv")
const pfp = this.buildpfp();
div.append(pfp);
const status = document.createElement("div");
@ -301,7 +301,7 @@ class User extends SnowFlake{
localuser.info.api.toString() + "/users/" + id + "/profile",
{ headers: localuser.headers }
).then(res=>res.json());
return new User(json, localuser);
return new User(json.user, localuser);
}
changepfp(update: string | null): void{
@ -399,7 +399,7 @@ class User extends SnowFlake{
div.classList.add("profile", "flexttb");
}else{
this.setstatus("online");
div.classList.add("hypoprofile", "flexttb");
div.classList.add("hypoprofile", "profile", "flexttb");
}
const badgediv = document.createElement("div");
badgediv.classList.add("badges");
@ -426,7 +426,7 @@ class User extends SnowFlake{
const pfp = await this.buildstatuspfp();
div.appendChild(pfp);
const userbody = document.createElement("div");
userbody.classList.add("infosection");
userbody.classList.add("flexttb","infosection");
div.appendChild(userbody);
const usernamehtml = document.createElement("h2");
usernamehtml.textContent = this.username;
@ -450,7 +450,7 @@ class User extends SnowFlake{
Member.resolveMember(this, guild).then(member=>{
if(!member)return;
const roles = document.createElement("div");
roles.classList.add("rolesbox");
roles.classList.add("flexltr","rolesbox");
for(const role of member.roles){
const roleDiv = document.createElement("div");
roleDiv.classList.add("rolediv");

652
src/webpage/voice.ts Normal file
View file

@ -0,0 +1,652 @@
import { memberjson, sdpback, voiceserverupdate, voiceupdate, webRTCSocket } from "./jsontypes.js";
class VoiceFactory{
settings:{id:string};
constructor(usersettings:VoiceFactory["settings"]){
this.settings=usersettings;
}
voices=new Map<string,Map<string,Voice>>();
voiceChannels=new Map<string,Voice>();
currentVoice?:Voice;
guildUrlMap=new Map<string,{url?:string,geturl:Promise<void>,gotUrl:()=>void}>();
makeVoice(guildid:string,channelId:string,settings:Voice["settings"]){
let guild=this.voices.get(guildid);
if(!guild){
this.setUpGuild(guildid);
guild=new Map();
this.voices.set(guildid,guild);
}
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);
this.voiceChannels.set(channelId,voice);
guild.set(channelId,voice);
return voice;
}
onJoin=(_voice:Voice)=>{};
onLeave=(_voice:Voice)=>{};
joinVoice(channelId:string,guildId:string){
if(this.currentVoice){
this.currentVoice.leave();
}
const voice=this.voiceChannels.get(channelId);
if(!voice) throw new Error(`Voice ${channelId} does not exist`);
voice.join();
this.currentVoice=voice;
this.onJoin(voice);
return {
d:{
guild_id: guildId,
channel_id: channelId,
self_mute: true,//todo
self_deaf: false,//todo
self_video: false,//What is this? I have some guesses
flags: 2//?????
},
op:4
}
}
userMap=new Map<string,Voice>();
voiceStateUpdate(update:voiceupdate){
const prev=this.userMap.get(update.d.user_id);
console.log(prev,this.userMap);
if(prev){
prev.disconnect(update.d.user_id);
this.onLeave(prev);
}
const voice=this.voiceChannels.get(update.d.channel_id);
if(voice){
this.userMap.set(update.d.user_id,voice);
voice.voiceupdate(update);
}
}
private setUpGuild(id:string){
const obj:{url?:string,geturl?:Promise<void>,gotUrl?:()=>void}={};
obj.geturl=new Promise<void>(res=>{obj.gotUrl=res});
this.guildUrlMap.set(id,obj as {geturl:Promise<void>,gotUrl:()=>void});
}
voiceServerUpdate(update:voiceserverupdate){
const obj=this.guildUrlMap.get(update.d.guild_id);
if(!obj) return;
obj.url=update.d.endpoint;
obj.gotUrl();
}
}
class Voice{
private pstatus:string="not connected";
public onSatusChange:(e:string)=>unknown=()=>{};
set status(e:string){
this.pstatus=e;
this.onSatusChange(e);
}
get status(){
return this.pstatus;
}
readonly userid:string;
settings:{bitrate:number};
urlobj:{url?:string,geturl:Promise<void>,gotUrl:()=>void};
constructor(userid:string,settings:Voice["settings"],urlobj:Voice["urlobj"]){
this.userid=userid;
this.settings=settings;
this.urlobj=urlobj;
}
pc?:RTCPeerConnection;
ws?:WebSocket;
timeout:number=30000;
interval:NodeJS.Timeout=0 as unknown as NodeJS.Timeout;
time:number=0;
seq:number=0;
sendAlive(){
if(this.ws){
this.ws.send(JSON.stringify({ op: 3,d:10}));
}
}
readonly users= new Map<number,string>();
readonly speakingMap= new Map<string,number>();
onSpeakingChange=(_userid:string,_speaking:number)=>{};
disconnect(userid:string){
console.warn(userid);
if(userid===this.userid){
this.leave();
}
const ssrc=this.speakingMap.get(userid);
if(ssrc){
this.users.delete(ssrc);
for(const thing of this.ssrcMap){
if(thing[1]===ssrc){
this.ssrcMap.delete(thing[0]);
}
}
}
this.speakingMap.delete(userid);
this.userids.delete(userid);
console.log(this.userids,userid);
//there's more for sure, but this is "good enough" for now
this.onMemberChange(userid,false);
}
packet(message:MessageEvent){
const data=message.data
if(typeof data === "string"){
const json:webRTCSocket = JSON.parse(data);
switch(json.op){
case 2:
this.startWebRTC();
break;
case 4:
this.continueWebRTC(json);
break;
case 5:
this.speakingMap.set(json.d.user_id,json.d.speaking);
this.onSpeakingChange(json.d.user_id,json.d.speaking);
break;
case 6:
this.time=json.d.t;
setTimeout(this.sendAlive.bind(this), this.timeout);
break;
case 8:
this.timeout=json.d.heartbeat_interval;
setTimeout(this.sendAlive.bind(this), 1000);
break;
case 12:
this.figureRecivers();
if(!this.users.has(json.d.audio_ssrc)){
console.log("redo 12!");
this.makeOp12();
}
this.users.set(json.d.audio_ssrc,json.d.user_id);
break;
}
}
}
offer?:string;
cleanServerSDP(sdp:string):string{
const pc=this.pc;
if(!pc) throw new Error("pc isn't defined")
const ld=pc.localDescription;
if(!ld) throw new Error("localDescription isn't defined");
const parsed = Voice.parsesdp(ld.sdp);
const group=parsed.atr.get("group");
if(!group) throw new Error("group isn't in sdp");
const [_,...bundles]=(group.entries().next().value as [string, string])[0].split(" ");
bundles[bundles.length-1]=bundles[bundles.length-1].replace("\r","");
console.log(bundles);
if(!this.offer) throw new Error("Offer is missing :P");
let cline=sdp.split("\n").find(line=>line.startsWith("c="));
if(!cline) throw new Error("c line wasn't found");
const parsed1=Voice.parsesdp(sdp).medias[0];
//const parsed2=Voice.parsesdp(this.offer);
const rtcport=(parsed1.atr.get("rtcp") as Set<string>).values().next().value as string;
const ICE_UFRAG=(parsed1.atr.get("ice-ufrag") as Set<string>).values().next().value as string;
const ICE_PWD=(parsed1.atr.get("ice-pwd") as Set<string>).values().next().value as string;
const FINGERPRINT=(parsed1.atr.get("fingerprint") as Set<string>).values().next().value as string;
const candidate=(parsed1.atr.get("candidate") as Set<string>).values().next().value as string;
let build=`v=0\r
o=- 1420070400000 0 IN IP4 127.0.0.1\r
s=-\r
t=0 0\r
a=msid-semantic: WMS *\r
a=group:BUNDLE ${bundles.join(" ")}\r`
let i=0;
for(const grouping of parsed.medias){
let mode="recvonly";
for(const _ of this.senders){
if(i<2){
mode="sendrecv";
}
}
if(grouping.media==="audio"){
build+=`
m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r
${cline}\r
a=rtpmap:111 opus/48000/2\r
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r
a=rtcp:${rtcport}\r
a=rtcp-fb:111 transport-cc\r
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
a=setup:passive\r
a=mid:${bundles[i]}\r
a=maxptime:60\r
a=${mode}\r
a=ice-ufrag:${ICE_UFRAG}\r
a=ice-pwd:${ICE_PWD}\r
a=fingerprint:${FINGERPRINT}\r
a=candidate:${candidate}\r
a=rtcp-mux\r`
}else{
build+=`
m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r
${cline}\r
a=rtpmap:102 H264/90000\r
a=rtpmap:103 rtx/90000\r
a=fmtp:102 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r
a=fmtp:103 apt=102\r
a=rtcp:${rtcport}\r
a=rtcp-fb:102 ccm fir\r
a=rtcp-fb:102 nack\r
a=rtcp-fb:102 nack pli\r
a=rtcp-fb:102 goog-remb\r
a=rtcp-fb:102 transport-cc\r
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time/r/n
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r
a=extmap:13 urn:3gpp:video-orientation\r
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay/r/na=setup:passive/r/n
a=mid:${bundles[i]}\r
a=${mode}\r
a=ice-ufrag:${ICE_UFRAG}\r
a=ice-pwd:${ICE_PWD}\r
a=fingerprint:${FINGERPRINT}\r
a=candidate:${candidate}\r
a=rtcp-mux\r`;
}
i++
}
build+="\n";
return build;
}
counter?:string;
negotationneeded(){
if(this.pc&&this.offer){
const pc=this.pc;
pc.addEventListener("negotiationneeded", async ()=>{
this.offer=(await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
})).sdp;
await pc.setLocalDescription({sdp:this.offer});
if(!this.counter) throw new Error("counter isn't defined");
const counter=this.counter;
const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter),type:"answer"};
console.log(remote);
await pc.setRemoteDescription(remote);
const senders=this.senders.difference(this.ssrcMap);
for(const sender of senders){
for(const thing of (await sender.getStats() as Map<string, any>)){
if(thing[1].ssrc){
this.ssrcMap.set(sender,thing[1].ssrc);
this.makeOp12(sender);
}
}
}
console.log(this.ssrcMap);
});
}
}
async makeOp12(sender:RTCRtpSender|undefined|[RTCRtpSender,number]=(this.ssrcMap.entries().next().value)){
if(!sender) throw new Error("sender doesn't exist");
if(sender instanceof Array){
sender=sender[0];
}
if(this.ws){
this.ws.send(JSON.stringify({
op: 12,
d: {
audio_ssrc: this.ssrcMap.get(sender),
video_ssrc: 0,
rtx_ssrc: 0,
streams: [
{
type: "video",
rid: "100",
ssrc: 0,//TODO
active: false,
quality: 100,
rtx_ssrc: 0,//TODO
max_bitrate: 2500000,//TODO
max_framerate: 0,//TODO
max_resolution: {
type: "fixed",
width: 0,//TODO
height: 0//TODO
}
}
]
}
}));
this.status="Sending audio streams";
}
}
senders:Set<RTCRtpSender>=new Set();
recivers=new Set<RTCRtpReceiver>();
ssrcMap:Map<RTCRtpSender,number>=new Map();
speaking=false;
async setupMic(audioStream:MediaStream){
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(audioStream);
analyser.smoothingTimeConstant = 0;
analyser.fftSize = 32;
microphone.connect(analyser);
const array=new Float32Array(1);
const interval=setInterval(()=>{
if(!this.ws){
clearInterval(interval);
}
analyser.getFloatFrequencyData(array);
const value=array[0]+65;
if(value<0){
if(this.speaking){
this.speaking=false;
this.sendSpeaking();
console.log("not speaking")
}
}else if(!this.speaking){
console.log("speaking");
this.speaking=true;
this.sendSpeaking();
}
},500);
}
async sendSpeaking(){
if(!this.ws) return;
const pair=this.ssrcMap.entries().next().value;
if(!pair) return
this.ws.send(JSON.stringify({
op:5,
d:{
speaking:+this.speaking,
delay:5,//not sure
ssrc:pair[1]
}
}))
}
async continueWebRTC(data:sdpback){
if(this.pc&&this.offer){
const pc=this.pc;
this.negotationneeded();
this.status="Starting Audio streams";
const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} );
for (const track of audioStream.getAudioTracks()){
//Add track
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:"recvonly",
streams:[],
sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}]
});
}
for(let i=0;i<10;i++){
pc.addTransceiver("video",{
direction:"recvonly",
streams:[],
sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}]
});
}
this.counter=data.d.sdp;
pc.ontrack = async (e) => {
this.status="Done";
if(e.track.kind==="video"){
return;
}
const media=e.streams[0];
console.log("got audio:",e);
for(const track of media.getTracks()){
console.log(track);
}
const context= new AudioContext();
await context.resume();
const ss=context.createMediaStreamSource(media);
console.log(media);
ss.connect(context.destination);
new Audio().srcObject = media;//weird I know, but it's for chromium/webkit bug
this.recivers.add(e.receiver)
};
}else{
this.status="Connection failed";
}
}
reciverMap=new Map<number,RTCRtpReceiver>()
async figureRecivers(){
await new Promise(res=>setTimeout(res,500));
for(const reciver of this.recivers){
const stats=await reciver.getStats() as Map<string,any>;
for(const thing of (stats)){
if(thing[1].ssrc){
this.reciverMap.set(thing[1].ssrc,reciver)
}
}
}
console.log(this.reciverMap);
}
async startWebRTC(){
this.status="Making offer";
const pc = new RTCPeerConnection();
this.pc=pc;
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
this.status="Starting RTC connection";
const sdp=offer.sdp;
this.offer=sdp;
if(!sdp){
this.status="No SDP";
this.ws?.close();
return;
}
const parsed=Voice.parsesdp(sdp);
const video=new Map<string,[number,number]>();
const audio=new Map<string,number>();
let cur:[number,number]|undefined;
let i=0;
for(const thing of parsed.medias){
try{
if(thing.media==="video"){
const rtpmap=thing.atr.get("rtpmap");
if(!rtpmap) continue;
for(const codecpair of rtpmap){
const [port, codec]=codecpair.split(" ");
if(cur&&codec.split("/")[0]==="rtx"){
cur[1]=Number(port);
cur=undefined;
continue
}
if(video.has(codec.split("/")[0])) continue;
cur=[Number(port),-1];
video.set(codec.split("/")[0],cur);
}
}else if(thing.media==="audio"){
const rtpmap=thing.atr.get("rtpmap");
if(!rtpmap) continue;
for(const codecpair of rtpmap){
const [port, codec]=codecpair.split(" ");
if(audio.has(codec.split("/")[0])) { continue};
audio.set(codec.split("/")[0],Number(port));
}
}
}finally{
i++;
}
}
const codecs:{
name: string,
type: "video"|"audio",
priority: number,
payload_type: number,
rtx_payload_type: number|null
}[]=[];
const include=new Set<string>();
const audioAlloweds=new Map([["opus",{priority:1000,}]]);
for(const thing of audio){
if(audioAlloweds.has(thing[0])){
include.add(thing[0]);
codecs.push({
name:thing[0],
type:"audio",
priority:audioAlloweds.get(thing[0])?.priority as number,
payload_type:thing[1],
rtx_payload_type:null
});
}
}
const videoAlloweds=new Map([["H264",{priority:1000}],["VP8",{priority:2000}],["VP9",{priority:3000}]]);
for(const thing of video){
if(videoAlloweds.has(thing[0])){
include.add(thing[0]);
codecs.push({
name:thing[0],
type:"video",
priority:videoAlloweds.get(thing[0])?.priority as number,
payload_type:thing[1][0],
rtx_payload_type:thing[1][1]
});
}
}
let sendsdp="a=extmap-allow-mixed";
let first=true;
for(const media of parsed.medias){
for(const thing of first?["ice-ufrag","ice-pwd","ice-options","fingerprint","extmap","rtpmap"]:["extmap","rtpmap"]){
const thing2=media.atr.get(thing);
if(!thing2) continue;
for(const thing3 of thing2){
if(thing === "rtpmap"){
const name=thing3.split(" ")[1].split("/")[0];
if(include.has(name)){
include.delete(name);
}else{
continue;
}
}
sendsdp+=`\na=${thing}:${thing3}`;
}
}
first=false;
}
if(this.ws){
this.ws.send(JSON.stringify({
d:{
codecs,
protocol:"webrtc",
data:sendsdp,
sdp:sendsdp
},
op:1
}));
}
}
static parsesdp(sdp:string){
let currentA=new Map<string,Set<string>>();
const out:{version?:number,medias:{media:string,port:number,proto:string,ports:number[],atr:Map<string,Set<string>>}[],atr:Map<string,Set<string>>}={medias:[],atr:currentA};
for(const line of sdp.split("\n")){
const [code,setinfo]=line.split("=");
switch(code){
case "v":
out.version=Number(setinfo);
break;
case "o":
case "s":
case "t":
break;
case "m":
currentA=new Map();
const [media,port,proto,...ports]=setinfo.split(" ");
const portnums=ports.map(Number);
out.medias.push({media,port:Number(port),proto,ports:portnums,atr:currentA});
break;
case "a":
const [key, ...value] = setinfo.split(":");
if(!currentA.has(key)){
currentA.set(key,new Set());
}
currentA.get(key)?.add(value.join(":"));
break;
}
}
return out;
}
open=false;
async join(){
console.warn("Joining");
this.open=true
this.status="waiting for main WS";
}
onMemberChange=(_member:memberjson|string,_joined:boolean)=>{};
userids=new Map<string,{}>();
async voiceupdate(update:voiceupdate){
console.log("Update!");
this.userids.set(update.d.member.id,{deaf:update.d.deaf,muted:update.d.mute});
this.onMemberChange(update.d.member,true);
if(update.d.member.id===this.userid&&this.open){
if(!update) {
this.status="bad responce from WS";
return;
};
if(!this.urlobj.url){
this.status="waiting for Voice URL";
await this.urlobj.geturl;
if(!this.open){this.leave();return}
}
const ws=new WebSocket("ws://"+this.urlobj.url as string);
this.ws=ws;
ws.onclose=()=>{
this.leave();
}
this.status="waiting for WS to open";
ws.addEventListener("message",(m)=>{
this.packet(m);
})
await new Promise<void>(res=>{
ws.addEventListener("open",()=>{
res()
})
});
if(!this.ws){
this.leave();
return;
}
this.status="waiting for WS to authorize";
ws.send(JSON.stringify({
"op": 0,
"d": {
server_id: update.d.guild_id,
user_id: update.d.user_id,
session_id: update.d.session_id,
token: update.d.token,
video: false,
"streams": [
{
type: "video",
rid: "100",
quality: 100
}
]
}
}));
}
}
async leave(){
this.open=false;
this.status="Left voice chat";
if(this.ws){
this.ws.close();
this.ws=undefined;
}
if(this.pc){
this.pc.close();
this.pc=undefined;
}
}
}
export {Voice,VoiceFactory};