Merge branch 'Voice'
This commit is contained in:
commit
efae437b70
35 changed files with 3401 additions and 2721 deletions
|
@ -149,4 +149,4 @@ app.listen(PORT, ()=>{
|
|||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export{ getApiUrls };
|
||||
export{ getApiUrls };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
96
src/webpage/i18n.ts
Normal 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};
|
1
src/webpage/icons/plus.svg
Normal file
1
src/webpage/icons/plus.svg
Normal 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
1
src/webpage/icons/x.svg
Normal 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 |
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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", ()=>{
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
@ -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;
|
||||
|
||||
--servers-bg: #7a7aaa;
|
||||
--channels-bg: #babdd2;
|
||||
--channel-selected: #9c9fbf;
|
||||
--typebox-bg: #bac0df;
|
||||
|
||||
--server-border: #aaaac4;
|
||||
--server-hover: #7f7fa8;
|
||||
--button-bg: #babdd2;
|
||||
--button-hover: #9c9fbf;
|
||||
--spoiler-bg: #34333a;
|
||||
--link: #283c8b;
|
||||
|
||||
--reply-border: #474b76;
|
||||
--reply-bg: #d4d6e9;
|
||||
--reply-text: #38383d;
|
||||
|
||||
--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);
|
||||
}
|
16
src/webpage/translations/en.json
Normal file
16
src/webpage/translations/en.json
Normal 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"
|
||||
}
|
12
src/webpage/translations/ru.json
Normal file
12
src/webpage/translations/ru.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
],
|
||||
"last-updated": "2024/15/24",
|
||||
"locale": "ru",
|
||||
"comment":"I need some help with this :P"
|
||||
},
|
||||
"ru": {
|
||||
|
||||
}
|
||||
}
|
|
@ -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
652
src/webpage/voice.ts
Normal 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};
|
Loading…
Add table
Add a link
Reference in a new issue