From 4a64972cd11c4e74dc49b96c9b4a62b82e179b26 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 11 Nov 2024 22:49:42 -0600 Subject: [PATCH] mention/channel autofil --- src/webpage/channel.ts | 10 ++ src/webpage/index.html | 3 + src/webpage/index.ts | 6 +- src/webpage/localuser.ts | 260 +++++++++++++++++++++++++++++++++++++-- src/webpage/markdown.ts | 154 +++++++++++++++++++---- src/webpage/member.ts | 13 ++ src/webpage/message.ts | 4 +- src/webpage/style.css | 31 +++++ translations/en.json | 3 +- 9 files changed, 438 insertions(+), 46 deletions(-) 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",