From 648df3fc12b8a4ddaaf3a08a3fc4a475da02ccc7 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Sat, 30 Aug 2025 16:39:04 -0500 Subject: [PATCH] admin finder Menu --- src/webpage/contextmenu.ts | 2 +- src/webpage/guild.ts | 181 +++++++++++++++++++++++++++++++++++++ src/webpage/settings.ts | 19 ++-- src/webpage/style.css | 38 +++++++- src/webpage/user.ts | 30 ++++-- translations/en.json | 7 ++ 6 files changed, 260 insertions(+), 17 deletions(-) diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index f1aaeb3..53f48d9 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -147,6 +147,7 @@ class Contextmenu { static setup() { Contextmenu.declareMenu(); document.addEventListener("click", (event) => { + console.log(event.target, Contextmenu.currentmenu); while (Contextmenu.currentmenu && !Contextmenu.currentmenu.contains(event.target as Node)) { Contextmenu.declareMenu(); } @@ -270,7 +271,6 @@ class Contextmenu { obj.style.left = Math.floor(docwidth - box.width) + "px"; } if (box.bottom > docheight) { - debugger; obj.style.top = Math.floor(docheight - box.height) + "px"; } } diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 8579ba0..a4adf75 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -263,6 +263,13 @@ class Guild extends SnowFlake { }, }, ); + //TODO make icon for this + Guild.contextmenu.addButton( + () => I18n.guild.admins(), + function (this: Guild) { + this.findAdmin(); + }, + ); Guild.contextmenu.addSeperator(); Guild.contextmenu.addButton( @@ -273,6 +280,180 @@ class Guild extends SnowFlake { ); //TODO mute guild button } + async findAdmin() { + const menu = new Dialog(I18n.guild.admins()); + menu.options.addText("Loading"); + menu.show(); + const roles = new Set( + Object.entries( + (await ( + await fetch(this.info.api + `/guilds/${this.id}/roles/member-counts/`, { + headers: this.headers, + }) + ).json()) as {[key: string]: number}, + ) + .map(([id, count]) => { + return [this.roleids.get(id), count] as [Role, number]; + }) + //Just in case, this should never fire + .filter((_) => _[0] !== undefined) + //Filter out those who have too many users + .filter((_) => _[1] > 1000) + .map((_) => _[0]), + ); + const everyone = this.roleids.get(this.id); + if (everyone) roles.add(everyone); + menu.options.removeAll(); + let owner = true; + let perms = [ + "ADMINISTRATOR", + "BAN_MEMBERS", + "KICK_MEMBERS", + "MANAGE_GUILD", + "MANAGE_CHANNELS", + "MODERATE_MEMBERS", + "MANAGE_ROLES", + "MANAGE_MESSAGES", + "MANAGE_NICKNAMES", + "MANAGE_WEBHOOKS", + "MANAGE_EVENTS", + "MANAGE_THREADS", + ].filter((_) => { + for (const role of roles) { + if (role.permissions.hasPermission(_, false)) { + return false; + } + } + return true; + }); + + menu.options.addButtonInput("", I18n.guild.adminMenu.changePerms(), () => { + const d = new Dialog("", {noSubmit: false}); + const opt = d.options; + opt + .addCheckboxInput( + I18n.guild.adminMenu.owner(), + (b) => { + owner = b; + d.hide(); + queueMicrotask(() => loadResults()); + }, + { + initState: owner, + }, + ) + .watchForChange(() => opt.changed()); + for (const perm of Permissions.info().filter((_) => { + for (const role of roles) { + if (role.permissions.hasPermission(_.name, false)) { + return false; + } + } + return true; + })) { + opt.addHR(); + opt + .addCheckboxInput( + perm.readableName, + (b) => { + if (b) { + perms.push(perm.name); + } else { + perms = perms.filter((_) => _ !== perm.name); + } + }, + { + initState: perms.includes(perm.name), + }, + ) + .watchForChange(() => opt.changed()); + opt.addText(perm.description); + } + d.show().style.width = "80%"; + }); + + const retDiv = document.createElement("div"); + menu.options.addHTMLArea(retDiv); + const loadResults = async () => { + retDiv.textContent = I18n.guild.adminMenu.finding(); + const results = new Set( + ( + await Promise.all( + this.roles + .filter((_) => { + for (const perm of perms) { + if (_.permissions.hasPermission(perm, false)) { + return true; + } + } + return false; + }) + .map(async (_) => { + return ( + await fetch(`${this.info.api}/guilds/${this.id}/roles/${_.id}/member-ids`, { + headers: this.headers, + }) + ).json() as Promise; + }), + ) + ).flat(), + ); + if (owner) { + results.add(this.properties.owner_id); + } + const members = ( + await Promise.all( + [...results].map(async (_) => { + const json = await this.localuser.resolvemember(_, this.id); + return json ? Member.new(json, this) : undefined; + }), + ) + ).filter((_) => _ !== undefined); + members.sort((a, b) => { + return a.name < b.name ? 1 : -1; + }); + retDiv.innerHTML = ""; + retDiv.append( + ...members.map((memb) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "adminList"); + const name = document.createElement("b"); + name.textContent = memb.name; + const nameBox = document.createElement("div"); + nameBox.classList.add("flexttb"); + + const roles = document.createElement("div"); + roles.classList.add("flexltr"); + roles.append( + ...perms + .filter((_) => memb.hasPermission(_, false)) + .map((perm) => { + const span = document.createElement("span"); + //@ts-ignore + span.textContent = I18n.permissions.readableNames[perm](); + return span; + }), + ); + if (owner && memb.id === this.properties.owner_id) { + const span = document.createElement("span"); + span.textContent = I18n.guild.adminMenu.ownName(); + roles.append(span); + } + + nameBox.append(name, roles); + + const pfp = memb.user.buildpfp(memb, div); + + div.append(pfp, nameBox); + + memb.user.bind(div, this, undefined); + return div; + }), + ); + console.log(members); + }; + loadResults(); + } generateSettings() { const settings = new Settings(I18n.getTranslation("guild.settingsFor", this.properties.name)); const textChannels = this.channels.filter((e) => { diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index c01e287..f25d402 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -824,15 +824,22 @@ class HtmlArea implements OptionsElement { */ class Float { options: Options; + html: WeakRef = new WeakRef(document.createElement("div")); /** * This is a simple wrapper class for Options to make it happy so it can be used outside of Settings. */ constructor(name: string, options = {ltr: false, noSubmit: true}) { this.options = new Options(name, this, options); } - changed = () => {}; + changed(d: HTMLElement) { + const html = this.html.deref(); + if (!html) return; + html.append(d); + } generateHTML() { - return this.options.generateHTML(); + const html = this.options.generateHTML(); + this.html = new WeakRef(html); + return html; } } class Dialog { @@ -852,16 +859,14 @@ class Dialog { center.classList.add("centeritem", "nonimagecenter"); center.classList.remove("titlediv"); background.append(center); - center.onclick = (e) => { - e.stopImmediatePropagation(); - }; document.body.append(background); this.background = new WeakRef(background); background.onclick = (_) => { - if (hideOnClick) { + if (hideOnClick && _.target === background) { background.remove(); } }; + return center; } hide() { const background = this.background.deref(); @@ -1098,7 +1103,7 @@ class Options implements OptionsElement { } addEmojiInput( label: string, - onSubmit: (str: Emoji | undefined) => void, + onSubmit: (str: Emoji | null | undefined) => void, localuser?: Localuser, {initEmoji = undefined, clear = false} = {} as {initEmoji?: Emoji; clear?: boolean}, ) { diff --git a/src/webpage/style.css b/src/webpage/style.css index 3af240b..dd65e2f 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -74,6 +74,35 @@ body { margin-left: 4px; } } +.adminList { + width: 95%; + background: var(--secondary-bg); + padding: 6px; + margin-bottom: 6px; + border-radius: 10px; + cursor: pointer; + img { + width: 48px; + height: 48px; + } + .flexttb { + margin-left: 10px; + display: flex; + justify-content: center; + + .flexltr { + span { + margin-right: 4px; + text-wrap: nowrap; + background: var(--primary-bg); + padding: 2px; + border-radius: 3px; + margin-bottom: 4px; + } + flex-wrap: wrap; + } + } +} .flexltr { min-height: 0; display: flex; @@ -2923,17 +2952,20 @@ fieldset input[type="radio"] { .tritoggle input:last-child { accent-color: var(--red); } +.nonimagecenter > .flexltr.savediv { + padding: 16px !important; +} .savediv { position: fixed; bottom: 24px; right: 50%; transform: translateX(50%); - padding: 16px; - background: var(--secondary-bg); + padding: 16px !important; + background: var(--secondary-bg) !important; font-size: 1.2em; font-weight: bold; color: var(--secondary-text); - border-radius: 8px; + border-radius: 8px !important; align-items: center; box-shadow: 0 0 24px var(--shadow), diff --git a/src/webpage/user.ts b/src/webpage/user.ts index a7518e7..22bff6d 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -496,7 +496,7 @@ class User extends SnowFlake { nameBox.append(notFound); } }); - this.bind(div, guild); + this.bind(div, guild, undefined); return div; } buildstatuspfp(guild: Guild | void | Member | null): HTMLDivElement { @@ -541,7 +541,12 @@ class User extends SnowFlake { } } - bind(html: HTMLElement, guild: Guild | null = null, error = true): void { + bind( + html: HTMLElement, + guild: Guild | null = null, + error = true, + button: "right" | "left" = "right", + ): void { if (guild && guild.id !== "@me") { Member.resolveMember(this, guild) .then((member) => { @@ -557,14 +562,14 @@ class User extends SnowFlake { if (member) { member.bind(html); } else { - User.contextmenu.bindContextmenu(html, this, undefined); + User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button); } }) .catch((err) => { console.log(err); }); } else { - User.contextmenu.bindContextmenu(html, this, undefined); + User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button); } if (guild) { this.profileclick(html, guild); @@ -913,6 +918,7 @@ class User extends SnowFlake { x: number, y: number, guild: Guild | null | Member = null, + zIndex = -1, ): Promise { const membres = (async () => { if (!guild) return; @@ -925,7 +931,9 @@ class User extends SnowFlake { return member; })(); const div = document.createElement("div"); - + if (zIndex !== -1) { + div.style.zIndex = zIndex + ""; + } if (this.accent_color) { div.style.setProperty( "--accent_color", @@ -1100,8 +1108,18 @@ class User extends SnowFlake { } } profileclick(obj: HTMLElement, guild?: Guild): void { + const getIndex = (elm: HTMLElement) => { + const index = getComputedStyle(elm).zIndex; + if (index === "auto") { + if (elm.parentElement) { + return getIndex(elm.parentElement); + } + } + return +index; + }; obj.onclick = (e: MouseEvent) => { - this.buildprofile(e.clientX, e.clientY, guild); + const index = 1 + getIndex(obj); + this.buildprofile(e.clientX, e.clientY, guild, index); e.stopPropagation(); }; } diff --git a/translations/en.json b/translations/en.json index 6b75d50..bcb6b0d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -288,6 +288,13 @@ "edit": "Edit", "guild": { "template": "Template:", + "admins":"Find Admins", + "adminMenu":{ + "finding":"Finding Admins", + "permission":"Permissions:", + "changePerms":"Change Permissions to find", + "owner":"Find the owner","ownName":"Owner" + }, "viewTemplate": "View Template", "createFromTemplate": "Guild From Template", "tempUseCount": "Template has been used $1 {{PLURAL:$1|time|times}}",