diff --git a/src/index.ts b/src/index.ts index 3bbe747..882a219 100644 --- a/src/index.ts +++ b/src/index.ts @@ -149,4 +149,4 @@ app.listen(PORT, ()=>{ console.log(`Server running on port ${PORT}`); }); -export{ getApiUrls }; \ No newline at end of file +export{ getApiUrls }; diff --git a/src/webpage/audio.ts b/src/webpage/audio.ts index a92bbaf..13b90ce 100644 --- a/src/webpage/audio.ts +++ b/src/webpage/audio.ts @@ -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 }; diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index f792ab7..1910540 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -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 = new Map(); idToNext: Map = new Map(); messages: Map = 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{ @@ -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; diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 205c025..56f2256 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -2,12 +2,12 @@ class Contextmenu{ 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{ 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{ 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{ 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){ diff --git a/src/webpage/dialog.ts b/src/webpage/dialog.ts index 2e08c11..fe7a2fd 100644 --- a/src/webpage/dialog.ts +++ b/src/webpage/dialog.ts @@ -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; diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index 1678f76..edabcd5 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -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); diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts index 6683e6a..91804fc 100644 --- a/src/webpage/embed.ts +++ b/src/webpage/embed.ts @@ -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"); diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 7756967..c605345 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -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"); diff --git a/src/webpage/file.ts b/src/webpage/file.ts index 20862ef..63969ca 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -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); diff --git a/src/webpage/home.html b/src/webpage/home.html index ae7f312..ac50c0f 100644 --- a/src/webpage/home.html +++ b/src/webpage/home.html @@ -11,28 +11,29 @@ + - + -
+
-
-

Welcome to Jank Client

-
+

Welcome to Jank Client

-

Jank Client is a spacebar compatible client seeking to be as good as it can be with many features including: -

+

Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:

  • Direct Messaging
  • Reactions support
  • @@ -44,16 +45,16 @@
-

Spacebar compatible Instances:

+

Spacebar-Compatible Instances:

Contribute to Jank Client

-

We always appreciate some help, wether that be in the form of bug reports, or code, or even just pointing out +

We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos.


- -

Github

+
+ Github
diff --git a/src/webpage/home.ts b/src/webpage/home.ts index 669c0d1..9f41e77 100644 --- a/src/webpage/home.ts +++ b/src/webpage/home.ts @@ -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){ diff --git a/src/webpage/i18n.ts b/src/webpage/i18n.ts new file mode 100644 index 0000000..7f7094e --- /dev/null +++ b/src/webpage/i18n.ts @@ -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((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}; diff --git a/src/webpage/icons/plus.svg b/src/webpage/icons/plus.svg new file mode 100644 index 0000000..e487b7a --- /dev/null +++ b/src/webpage/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/x.svg b/src/webpage/icons/x.svg new file mode 100644 index 0000000..f7fb14d --- /dev/null +++ b/src/webpage/icons/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index e7b6b89..fcf0797 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -11,15 +11,15 @@ - + - +
-
+

Jank Client is loading

This shouldn't take long

@@ -27,50 +27,62 @@
-
-
-
+
-
-

Server Name

+
+

Server Name

-
-
- - -
-

USERNAME

-

STATUS

-
+
+
+
+
+
+ -
- +
+

USERNAME

+

STATUS

+
+
+ +
+ +
-
-
- - Channel name - +
+
+ + + + Channel name + + + +
-
-
-
+
+
+
-
+
-