diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts
index 2d71e98..4ba0d35 100644
--- a/src/webpage/channel.ts
+++ b/src/webpage/channel.ts
@@ -856,6 +856,16 @@ class Channel extends SnowFlake{
setTimeout(this.rendertyping.bind(this), 10000);
this.rendertyping();
}
+ similar(str:string){
+ if(this.type===4) return -1;
+ const strl=Math.max(str.length,1)
+ if(this.name.includes(str)){
+ return strl/this.name.length;
+ }else if(this.name.toLowerCase().includes(str.toLowerCase())){
+ return strl/this.name.length/1.2;
+ }
+ return 0;
+ }
rendertyping(): void{
const typingtext = document.getElementById("typing") as HTMLDivElement;
let build = "";
diff --git a/src/webpage/index.html b/src/webpage/index.html
index 1fef553..d2aa1c2 100644
--- a/src/webpage/index.html
+++ b/src/webpage/index.html
@@ -74,6 +74,9 @@
+
diff --git a/src/webpage/index.ts b/src/webpage/index.ts
index 54c8c9f..27ec571 100644
--- a/src/webpage/index.ts
+++ b/src/webpage/index.ts
@@ -157,6 +157,7 @@ import { I18n } from "./i18n.js";
let replyingTo: Message | null = null;
async function handleEnter(event: KeyboardEvent): Promise{
+ if(thisUser.keyup(event)){return}
const channel = thisUser.channelfocus;
if(!channel)return;
@@ -198,15 +199,14 @@ import { I18n } from "./i18n.js";
}
}
- interface CustomHTMLDivElement extends HTMLDivElement {
- markdown: MarkDown;
- }
+ interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;}
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
const markdown = new MarkDown("", thisUser);
typebox.markdown = markdown;
typebox.addEventListener("keyup", handleEnter);
typebox.addEventListener("keydown", event=>{
+ thisUser.keydown(event)
if(event.key === "Enter" && !event.shiftKey) event.preventDefault();
});
markdown.giveBox(typebox);
diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts
index acbf011..869268a 100644
--- a/src/webpage/localuser.ts
+++ b/src/webpage/localuser.ts
@@ -5,10 +5,10 @@ import{ AVoice }from"./audio.js";
import{ User }from"./user.js";
import{ Dialog }from"./dialog.js";
import{ getapiurls, getBulkInfo, setTheme, Specialuser, SW }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,userjson,wsjson,}from"./jsontypes.js";
import{ Member }from"./member.js";
import{ Form, FormError, Options, Settings }from"./settings.js";
-import{ MarkDown }from"./markdown.js";
+import{ getTextNodeAtPosition, MarkDown, saveCaretPosition }from"./markdown.js";
import { Bot } from "./bot.js";
import { Role } from "./role.js";
import { VoiceFactory } from "./voice.js";
@@ -67,6 +67,7 @@ class Localuser{
};
}
async gottenReady(ready: readyjson): Promise{
+
await I18n.done;
this.initialized = true;
this.ready = ready;
@@ -77,6 +78,8 @@ class Localuser{
this.resume_gateway_url=ready.d.resume_gateway_url;
this.session_id=ready.d.session_id;
+ this.mdBox();
+
this.voiceFactory=new VoiceFactory({id:this.user.id});
this.handleVoice();
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
@@ -1711,11 +1714,226 @@ class Localuser{
Bot.InviteMaker(appId,form,this.info);
})
}
+ typeMd?:MarkDown;
+ readonly autofillregex=Object.freeze(/[@#:]([a-z0-9 ]*)$/i);
+ mdBox(){
+ interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;}
+
+ const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
+ this.typeMd=typebox.markdown;
+ this.typeMd.onUpdate=this.search.bind(this);
+ }
+ MDReplace(replacewith:string,original:string){
+ const typebox = document.getElementById("typebox") as HTMLDivElement;
+ if(!this.typeMd)return;
+ let raw=this.typeMd.rawString;
+ raw=raw.split(original)[1];
+ if(raw===undefined) return;
+ raw=original.replace(this.autofillregex,"")+replacewith+raw;
+ console.log(raw);
+ console.log(replacewith);
+ console.log(original);
+ this.typeMd.txt = raw.split("");
+ this.typeMd.boxupdate(typebox);
+ }
+ MDSearchOptions(options:[string,string][],original:string){
+ console.warn(original);
+ const div=document.getElementById("searchOptions");
+ if(!div)return;
+ div.innerHTML="";
+ let i=0;
+ const htmloptions:HTMLSpanElement[]=[];
+ for(const thing of options){
+ if(i==8){
+ break;
+ }
+ i++;
+ const span=document.createElement("span");
+ htmloptions.push(span);
+ span.textContent=thing[0];
+ span.onclick=(e)=>{
+
+ if(e){
+ const selection = window.getSelection() as Selection;
+ const typebox = document.getElementById("typebox") as HTMLDivElement;
+ if(selection){
+ console.warn(original);
+ const pos = getTextNodeAtPosition(typebox, original.length-(original.match(this.autofillregex) as RegExpMatchArray)[0].length+thing[1].length);
+ selection.removeAllRanges();
+ const range = new Range();
+ range.setStart(pos.node, pos.position);
+ selection.addRange(range);
+ }
+ e.preventDefault();
+ typebox.focus();
+ }
+ this.MDReplace(thing[1],original);
+ div.innerHTML="";
+ remove();
+ }
+ div.prepend(span);
+ }
+ const remove=()=>{
+ if(div&&div.innerHTML===""){
+ this.keyup=()=>false;
+ this.keydown=()=>{};
+ return true;
+ }
+ return false;
+ }
+ if(htmloptions[0]){
+ let curindex=0;
+ let cur=htmloptions[0];
+ cur.classList.add("selected");
+ const cancel=new Set(["ArrowUp","ArrowDown","Enter","Tab"]);
+ this.keyup=(event)=>{
+ if(remove()) return false;
+ if(cancel.has(event.key)){
+ switch(event.key){
+ case "ArrowUp":
+ if(htmloptions[curindex+1]){
+ cur.classList.remove("selected");
+ curindex++;
+ cur=htmloptions[curindex];
+ cur.classList.add("selected");
+ }
+ break;
+ case "ArrowDown":
+ if(htmloptions[curindex-1]){
+ cur.classList.remove("selected");
+ curindex--;
+ cur=htmloptions[curindex];
+ cur.classList.add("selected");
+ }
+ break;
+ case "Enter":
+ case "Tab":
+ //@ts-ignore
+ cur.onclick();
+ break;
+ }
+ return true;
+ }
+ return false;
+ }
+ this.keydown=(event)=>{
+ if(remove()) return;
+ if(cancel.has(event.key)){
+ event.preventDefault();
+ }
+ }
+ }else{
+ remove();
+ }
+ }
+ MDFindChannel(name:string,orginal:string){
+ const maybe:[number,Channel][]=[];
+ if(this.lookingguild&&this.lookingguild.id!=="@me"){
+ for(const channel of this.lookingguild.channels){
+ const confidence=channel.similar(name);
+ if(confidence>0){
+ maybe.push([confidence,channel]);
+ }
+ }
+ }
+ maybe.sort((a,b)=>b[0]-a[0]);
+ this.MDSearchOptions(maybe.map((a)=>["# "+a[1].name,`<#${a[1].id}> `]),orginal);
+ }
+ async getUser(id:string){
+ if(this.userMap.has(id)){
+ return this.userMap.get(id) as User;
+ }
+ return new User(await (await fetch(this.info.api+"/users/"+id)).json(),this);
+ }
+ MDFineMentionGen(name:string,original:string){
+ let members:[Member,number][]=[];
+ if(this.lookingguild){
+ for(const member of this.lookingguild.members){
+ const rank=member.compare(name);
+ if(rank>0){
+ members.push([member,rank])
+ }
+ }
+ }
+ members.sort((a,b)=>a[1]-b[1]);
+ console.log(members);
+ this.MDSearchOptions(members.map((a)=>["@"+a[0].name,`<@${a[0].id}> `]),original);
+ }
+ MDFindMention(name:string,original:string){
+ console.log(original);
+ if(this.ws&&this.lookingguild){
+ this.MDFineMentionGen(name,original);
+ const nonce=Math.floor(Math.random()*10**8)+"";
+ if(this.lookingguild.member_count<=this.lookingguild.members.size) return;
+ this.ws.send(JSON.stringify(
+ {op:8,
+ d:{
+ guild_id:[this.lookingguild.id],
+ query:name,
+ limit:8,
+ presences:true,
+ nonce
+ }
+ }
+ ));
+ this.searchMap.set(nonce,async (e)=>{
+ console.log(e);
+ if(e.members&&e.members[0]){
+ if(e.members[0].user){
+ for(const thing of e.members){
+ await Member.new(thing,this.lookingguild as Guild)
+ }
+ }else{
+ const prom1:Promise[]=[];
+ for(const thing of e.members){
+ prom1.push(this.getUser(thing.id));
+ }
+ Promise.all(prom1);
+ for(const thing of e.members){
+ if(!this.userMap.has(thing.id)){
+ console.warn("Dumb server bug for this member",thing);
+ continue;
+ }
+ await Member.new(thing,this.lookingguild as Guild)
+ }
+ }
+ this.MDFineMentionGen(name,original);
+ }
+ })
+ }
+ }
+ search(str:string,pre:boolean){
+ if(!pre){
+ const match=str.match(this.autofillregex);
+
+ if(match){
+ console.log(str,match);
+ const [type, search]=[match[0][0],match[0].split(/@|#|:/)[1]];
+ console.log(type,search);
+ switch(type){
+ case "#":
+ this.MDFindChannel(search,str);
+ break;
+ case "@":
+ this.MDFindMention(search,str);
+ break;
+ case ":":
+ if(search.length>=2){
+ console.log("implement me");
+ }
+ break;
+ }
+ return
+ }
+ }
+ const div=document.getElementById("searchOptions");
+ if(!div)return;
+ div.innerHTML="";
+ }
+ keydown:(event:KeyboardEvent)=>unknown=()=>{};
+ keyup:(event:KeyboardEvent)=>boolean=()=>false;
//---------- resolving members code -----------
- readonly waitingmembers: Map<
- string,
- Map void>
- > = new Map();
+ readonly waitingmembers = new Map void>>();
readonly presences: Map = new Map();
async resolvemember(
id: string,
@@ -1757,19 +1975,35 @@ class Localuser{
fetchingmembers: Map = new Map();
noncemap: Map void> = new Map();
noncebuild: Map = new Map();
+ searchMap=new Mapunknown>();
async gotChunk(chunk: {
- chunk_index: number;
- chunk_count: number;
- nonce: string;
- not_found?: string[];
- members?: memberjson[];
- presences: presencejson[];
- }){
+ chunk_index: number;
+ chunk_count: number;
+ nonce: string;
+ not_found?: string[];
+ members?: memberjson[];
+ presences: presencejson[];
+ }){
for(const thing of chunk.presences){
if(thing.user){
this.presences.set(thing.user.id, thing);
}
}
+ if(this.searchMap.has(chunk.nonce)){
+ const func=this.searchMap.get(chunk.nonce);
+ this.searchMap.delete(chunk.nonce);
+ if(func){
+ func(chunk);
+ return;
+ }
+ }
chunk.members ??= [];
const arr = this.noncebuild.get(chunk.nonce);
if(!arr)return;
diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts
index ce02f07..1179d1e 100644
--- a/src/webpage/markdown.ts
+++ b/src/webpage/markdown.ts
@@ -687,7 +687,9 @@ txt[j + 1] === undefined)
e.target.classList.remove("spoiler");
e.target.classList.add("unspoiled");
}
- giveBox(box: HTMLDivElement){
+ onUpdate:(upto:string,pre:boolean)=>unknown=()=>{};
+ giveBox(box: HTMLDivElement,onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}){
+ this.onUpdate=onUpdate;
box.onkeydown = _=>{
//console.log(_);
};
@@ -698,7 +700,9 @@ txt[j + 1] === undefined)
prevcontent = content;
this.txt = content.split("");
this.boxupdate(box);
+ MarkDown.gatherBoxText(box);
}
+
};
box.onpaste = _=>{
if(!_.clipboardData)return;
@@ -717,7 +721,10 @@ txt[j + 1] === undefined)
box.append(this.makeHTML({ keep: true }));
if(restore){
restore();
+ const test=saveCaretPosition(box);
+ if(test) test();
}
+ this.onUpdate(text,formatted);
}
static gatherBoxText(element: HTMLElement): string{
if(element.tagName.toLowerCase() === "img"){
@@ -729,11 +736,17 @@ txt[j + 1] === undefined)
if(element.hasAttribute("real")){
return element.getAttribute("real") as string;
}
+ if(element.tagName.toLowerCase() === "pre"||element.tagName.toLowerCase() === "samp"){
+ formatted=true;
+ }else{
+ formatted=false;
+ }
let build = "";
for(const thing of Array.from(element.childNodes)){
if(thing instanceof Text){
const text = thing.textContent;
build += text;
+
continue;
}
const text = this.gatherBoxText(thing as HTMLElement);
@@ -747,10 +760,7 @@ txt[j + 1] === undefined)
static safeLink(elm: HTMLElement, url: string){
if(URL.canParse(url)){
const Url = new URL(url);
- if(
- elm instanceof HTMLAnchorElement &&
- this.trustedDomains.has(Url.host)
- ){
+ if(elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)){
elm.href = url;
elm.target = "_blank";
return;
@@ -820,20 +830,81 @@ txt[j + 1] === undefined)
//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div
let text = "";
-function saveCaretPosition(context: Node){
- const selection = window.getSelection();
+let formatted=false;
+function saveCaretPosition(context: HTMLElement){
+ const selection = window.getSelection() as Selection;
if(!selection)return;
const range = selection.getRangeAt(0);
+ let base=selection.anchorNode as Node;
+ range.setStart(base, 0);
+ let baseString:string;
+ if(!(base instanceof Text)){
+ let i=0;
+ const index=selection.focusOffset;
+ //@ts-ignore
+ for(const thing of base.childNodes){
+ if(i===index){
+ base=thing;
+ break;
+ }
+ i++;
+ }
+ if(base instanceof HTMLElement){
+ baseString=MarkDown.gatherBoxText(base)
+ }else{
+ baseString=base.textContent as string;
+ }
+ }else{
+ baseString=selection.toString();
+ }
+
+
range.setStart(context, 0);
- text = selection.toString();
- let len = text.length + 1;
- for(const str in text.split("\n")){
- if(str.length !== 0){
- len--;
+
+ let build="";
+ //I think this is working now :3
+ function crawlForText(context:Node){
+ //@ts-ignore
+ const children=[...context.childNodes];
+ if(children.length===1&&children[0] instanceof Text){
+ if(selection.containsNode(context,false)){
+ build+=MarkDown.gatherBoxText(context as HTMLElement);
+ }else if(selection.containsNode(context,true)){
+ if(context.contains(base)||context===base||base.contains(context)){
+ build+=baseString;
+ }else{
+ build+=context.textContent;
+ }
+ }else{
+ console.error(context);
+ }
+ return;
+ }
+ for(const node of children as Node[]){
+
+ if(selection.containsNode(node,false)){
+ if(node instanceof HTMLElement){
+ build+=MarkDown.gatherBoxText(node);
+ }else{
+ build+=node.textContent;
+ }
+ }else if(selection.containsNode(node,true)){
+ if(node instanceof HTMLElement){
+ crawlForText(node);
+ }else{
+ console.error(node,"This shouldn't happen")
+ }
+ }else{
+ console.error(node,"This shouldn't happen");
+ }
}
}
- len += Number(text.at(-1) === "\n");
-
+ crawlForText(context);
+ if(baseString==="\n"){
+ build+=baseString;
+ }
+ text=build;
+ const len=build.length;
return function restore(){
if(!selection)return;
const pos = getTextNodeAtPosition(context, len);
@@ -844,20 +915,49 @@ function saveCaretPosition(context: Node){
};
}
-function getTextNodeAtPosition(root: Node, index: number){
- const NODE_TYPE = NodeFilter.SHOW_TEXT;
- const treeWalker = document.createTreeWalker(root, NODE_TYPE, elem=>{
- if(!elem.textContent)return 0;
- if(index > elem.textContent.length){
- index -= elem.textContent.length;
- return NodeFilter.FILTER_REJECT;
+function getTextNodeAtPosition(root: Node, index: number):{
+ node: Node,
+ position: number,
+ }{
+ if(root instanceof Text){
+ return{
+ node: root,
+ position: index,
+ };
+ }else if(root instanceof HTMLBRElement){
+ return{
+ node: root,
+ position: 0,
+ };
+ }else if(root instanceof HTMLElement&&root.hasAttribute("real")){
+ return{
+ node: root,
+ position: -1,
+ };
+ }
+ for(const node of root.childNodes as unknown as Node[]){
+ let len:number
+ if(node instanceof HTMLElement){
+ len=MarkDown.gatherBoxText(node).length;
+ }else{
+ len=(node.textContent as string).length
}
- return NodeFilter.FILTER_ACCEPT;
- });
- const c = treeWalker.nextNode();
+ if(lenI18n.getTranslation("message.delete"),
+ ()=>I18n.getTranslation("message.edit"),
function(this: Message){
this.setEdit();
},
@@ -84,7 +84,7 @@ class Message extends SnowFlake{
}
);
Message.contextmenu.addbutton(
- "Delete message",
+ ()=>I18n.getTranslation("message.delete"),
function(this: Message){
this.delete();
},
diff --git a/src/webpage/style.css b/src/webpage/style.css
index 1070ad2..28d0fee 100644
--- a/src/webpage/style.css
+++ b/src/webpage/style.css
@@ -21,6 +21,37 @@ body {
display: flex;
flex-direction: column;
}
+#searchOptions{
+ padding:.05in .15in;
+ border-radius: .1in;
+ background: var(--channels-bg);
+ position:absolute;
+ bottom:0;
+ width: calc(100% - 32px);
+ box-sizing: border-box;
+ span {
+ transition: background .1s;
+ margin-bottom:.025in;
+ margin-top:.025in;
+ padding:.075in .05in;
+ border-radius:.03in;
+ cursor:pointer;
+ }
+ span.selected{
+ background:var(--button-bg);
+ }
+ span:hover{
+ background:var(--button-bg);
+ }
+
+;
+ margin: 16px;
+ border: solid .025in var(--black);
+}
+#searchOptions:empty{
+ padding: 0;
+ border: 0;
+}
.flexgrow {
flex-grow: 1;
min-height: 0;
diff --git a/translations/en.json b/translations/en.json
index fa35eb1..b11b49c 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -309,7 +309,8 @@
},
"message":{
"reactionAdd":"Add reaction",
- "delete":"Delete message"
+ "delete":"Delete message",
+ "edit":"Edit message"
},
"instanceStats":{
"name":"Instance stats: $1",