From e72ec0131411b366028432cfca3537d358b21cbf Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 20 Nov 2025 16:23:38 -0600 Subject: [PATCH] stacked context menus and more group improvements --- src/webpage/contextmenu.ts | 126 +++++++++++++++++++++++++++++++++---- src/webpage/direct.ts | 26 +++++++- src/webpage/localuser.ts | 10 +-- 3 files changed, 146 insertions(+), 16 deletions(-) diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 41cb85f..7af5067 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -11,7 +11,14 @@ type iconJson = }; interface menuPart { - makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void; + group?: string; + makeContextHTML( + obj1: x, + obj2: y, + menu: HTMLDivElement, + layered: contextCluster[], + processed: WeakSet>, + ): void; } class ContextButton implements menuPart { @@ -22,6 +29,7 @@ class ContextButton implements menuPart { private enabled?: (this: x, arg: y) => boolean; //TODO there *will* be more colors private color?: "red" | "blue"; + group?: string; constructor( text: ContextButton["text"], onClick: ContextButton["onClick"], @@ -30,6 +38,7 @@ class ContextButton implements menuPart { visable?: (this: x, arg: y) => boolean; enabled?: (this: x, arg: y) => boolean; color?: "red" | "blue"; + group?: string; } = {}, ) { this.text = text; @@ -38,6 +47,7 @@ class ContextButton implements menuPart { this.visable = addProps.visable; this.enabled = addProps.enabled; this.color = addProps.color; + this.group = addProps.group; } isVisable(obj1: x, obj2: y): boolean { if (!this.visable) return true; @@ -102,10 +112,50 @@ class ContextButton implements menuPart { return this.text; } } +class ContextGroup implements menuPart { + private visable?: (this: x, arg: y) => boolean; + groupSel: string; + group = undefined; + constructor( + group: string, + addProps: { + visable?: (this: x, arg: y) => boolean; + } = {}, + ) { + this.visable = addProps.visable; + + this.groupSel = group; + } + isVisable(obj1: x, obj2: y): boolean { + if (!this.visable) return true; + return this.visable.call(obj1, obj2); + } + makeContextHTML( + x: x, + y: y, + menuHtml: HTMLDivElement, + layered: contextCluster[], + processed: WeakSet>, + ) { + if (!this.isVisable(x, y)) { + return; + } + for (const [menu, x, y] of layered) { + for (const part of menu.buttons) { + if (part.group === this.groupSel && !processed.has(part)) { + processed.add(part); + part.makeContextHTML(x, y, menuHtml, [], processed); + } + } + } + } +} class Seperator implements menuPart { private visable?: (obj1: x, obj2: y) => boolean; - constructor(visable?: (obj1: x, obj2: y) => boolean) { + group?: string; + constructor(visable?: (obj1: x, obj2: y) => boolean, group?: string) { this.visable = visable; + this.group = group; } makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void { if (!this.visable || this.visable(obj1, obj2)) { @@ -116,6 +166,28 @@ class Seperator implements menuPart { } } } +declare global { + interface HTMLElementEventMap { + layered: LayeredEvent; + } +} +type contextCluster = [Contextmenu, X, Y]; +class LayeredEvent extends CustomEvent { + menus: contextCluster[]; + primary?: contextCluster; + constructor(mouse: MouseEvent, menus: LayeredEvent["menus"]) { + super("layered", {bubbles: true}); + this.menus = menus; + queueMicrotask(() => { + console.log(this); + const pop = this.primary || menus.pop(); + if (!pop) return; + const [menu, addinfo, other] = pop; + menu.makemenu(mouse.clientX, mouse.clientY, addinfo, other, undefined, menus); + }); + } +} + class Contextmenu { static currentmenu: HTMLElement | "" = ""; static prevmenus: HTMLElement[] = []; @@ -152,8 +224,10 @@ class Contextmenu { } }); } - constructor(name: string) { + private layered = false; + constructor(name: string, layered = false) { this.name = name; + this.layered = layered; this.buttons = []; } @@ -165,24 +239,45 @@ class Contextmenu { visable?: (this: x, arg: y) => boolean; enabled?: (this: x, arg: y) => boolean; color?: "red" | "blue"; + group?: string; } = {}, ) { this.buttons.push(new ContextButton(text, onClick, addProps)); } - addSeperator(visable?: (obj1: x, obj2: y) => boolean) { - this.buttons.push(new Seperator(visable)); + addSeperator(visable?: (obj1: x, obj2: y) => boolean, group?: string) { + this.buttons.push(new Seperator(visable, group)); } - makemenu(x: number, y: number, addinfo: x, other: y, keep: boolean | HTMLElement = false) { + addGroup( + group: string, + addprops?: { + visable?: (this: x, arg: y) => boolean; + }, + ) { + this.buttons.push(new ContextGroup(group, addprops)); + } + makemenu( + x: number, + y: number, + addinfo: x, + other: y, + keep: boolean | HTMLElement = false, + layered: LayeredEvent["menus"] = [], + ) { const div = document.createElement("div"); div.classList.add("contextmenu", "flexttb"); + const processed = new WeakSet>(); for (const button of this.buttons) { - button.makeContextHTML(addinfo, other, div); + button.makeContextHTML(addinfo, other, div, layered, processed); } - if (div.children[div.children.length - 1]?.tagName === "HR") { + if (div.children[div.children.length - 1]?.tagName !== "HR") { + div.append(document.createElement("hr")); + } + new ContextGroup("default").makeContextHTML(addinfo, other, div, layered, processed); + + while (div.children[div.children.length - 1]?.tagName === "HR") { div.children[div.children.length - 1].remove(); } - //NOTE I don't know if this'll ever actually happen in reality if (div.childNodes.length === 0) return; Contextmenu.declareMenu(div, keep); @@ -223,10 +318,19 @@ class Contextmenu { } } } - event.preventDefault(); event.stopImmediatePropagation(); - this.makemenu(event.clientX, event.clientY, addinfo, other); + event.preventDefault(); + const layered = new LayeredEvent(event, []); + obj.dispatchEvent(layered); }; + obj.addEventListener("layered", (layered) => { + if (this.layered) { + layered.menus.push([this, addinfo, other]); + } else if (!layered.primary) { + layered.primary = [this, addinfo, other]; + } + return; + }); if (click === "right") { obj.addEventListener("contextmenu", func); } else { diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index e961192..9bf6133 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -434,6 +434,30 @@ class Group extends Channel { users: User[]; owner_id?: string; static contextmenu = new Contextmenu("channel menu"); + static groupMenu = this.makeGroupMenu(); + static makeGroupMenu() { + const menu = new Contextmenu("group menu", true); + menu.addButton( + function (user) { + return I18n.member.kick(user.name, this.name); + }, + function (user) { + fetch(this.info.api + `/channels/${this.id}/recipients/${user.id}`, { + method: "DELETE", + headers: this.headers, + }); + }, + { + group: "default", + visable: function (user) { + return this.localuser.user.id !== user.id && this.owner_id === this.localuser.user.id; + }, + color: "red", + }, + ); + return menu; + } + static setupcontextmenu() { this.contextmenu.addButton( () => I18n.DMs.markRead(), @@ -451,7 +475,7 @@ class Group extends Channel { }, { visable: function () { - return this.users.length !== 1; + return this.type !== 1; }, }, ); diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index e1f0332..09faf42 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -1119,7 +1119,7 @@ class Localuser { if (channel.voice && this.voiceAllowed) { return; } - if (guild.id === "@me" && (channel as Group).users.length === 1) { + if (guild.id === "@me" && (channel as Group).type === 1) { return; } @@ -1221,17 +1221,19 @@ class Localuser { bot.textContent = I18n.bot(); username.appendChild(bot); } - member.bind(username); - user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false); + memberdiv.append(pfp, username); if (channel instanceof Group) { - console.log(channel.owner_id); + Group.groupMenu.bindContextmenu(memberdiv, channel, user); if (channel.owner_id === user.id) { const crown = document.createElement("span"); crown.classList.add("svg-crown"); memberdiv.append(crown); } } + member.bind(username); + user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false); + memberdiv.classList.add("flexltr", "liststyle", "memberListStyle"); membershtml.append(memberdiv); }