mention/channel autofil

This commit is contained in:
MathMan05 2024-11-11 22:49:42 -06:00
parent 26ac410da9
commit 4a64972cd1
9 changed files with 438 additions and 46 deletions

View file

@ -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 = "";

View file

@ -74,6 +74,9 @@
<div id="loadingdiv">
</div>
</div>
<div style="position: relative;">
<div id="searchOptions" class="flexttb"></div>
</div>
<div id="pasteimage" class="flexltr"></div>
<div id="replybox" class="hideReplyBox"></div>
<div id="typediv">

View file

@ -157,6 +157,7 @@ import { I18n } from "./i18n.js";
let replyingTo: Message | null = null;
async function handleEnter(event: KeyboardEvent): Promise<void>{
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);

View file

@ -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<void>{
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<User>[]=[];
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<string, (returns: memberjson | undefined) => void>
> = new Map();
readonly waitingmembers = new Map<string,Map<string, (returns: memberjson | undefined) => void>>();
readonly presences: Map<string, presencejson> = new Map();
async resolvemember(
id: string,
@ -1757,19 +1975,35 @@ class Localuser{
fetchingmembers: Map<string, boolean> = new Map();
noncemap: Map<string, (r: [memberjson[], string[]]) => void> = new Map();
noncebuild: Map<string, [memberjson[], string[], number[]]> = new Map();
searchMap=new Map<string,(arg:{
chunk_index: number,
chunk_count: number,
nonce: string,
not_found?: string[],
members?: memberjson[],
presences: presencejson[],
})=>unknown>();
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;

View file

@ -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(len<index){
index-=len;
}else{
const returny=getTextNodeAtPosition(node,index);
if(returny.position===-1){
index=0;
continue;
}
return returny;
}
}
const span=document.createElement("span");
root.appendChild(span)
return{
node: c ? c : root,
position: index,
node: span,
position: 0,
};
}
export{ MarkDown };
export{ MarkDown , saveCaretPosition, getTextNodeAtPosition};

View file

@ -124,6 +124,19 @@ class Member extends SnowFlake{
return memb;
}
}
compare(str:string){
function similar(str2:string|null|undefined){
if(!str2) return 0;
const strl=Math.max(str.length,1)
if(str2.includes(str)){
return strl/str2.length;
}else if(str2.toLowerCase().includes(str.toLowerCase())){
return strl/str2.length/1.2;
}
return 0;
}
return Math.max(similar(this.user.name),similar(this.user.nickname),similar(this.nick),similar(this.user.username))
}
static async resolveMember(
user: User,
guild: Guild

View file

@ -74,7 +74,7 @@ class Message extends SnowFlake{
}
);
Message.contextmenu.addbutton(
()=>I18n.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();
},

View file

@ -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;

View file

@ -309,7 +309,8 @@
},
"message":{
"reactionAdd":"Add reaction",
"delete":"Delete message"
"delete":"Delete message",
"edit":"Edit message"
},
"instanceStats":{
"name":"Instance stats: $1",