From b8d53782b688c56dd809631a8c462b851c7a57a1 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 May 2026 14:53:21 +0200 Subject: [PATCH 01/14] Localisation updates from https://translatewiki.net. --- translations/ko.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/translations/ko.json b/translations/ko.json index f9084c0..6c500e0 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -145,6 +145,9 @@ "title": "이모티콘", "upload": "이모티콘 업로드" }, + "folder": { + "name": "폴더 이름:" + }, "friends": { "addfriend": "친구 추가", "addfriendpromt": "사용자 이름으로 친구 추가:", @@ -470,6 +473,7 @@ } }, "sticker": { + "name": "이름:", "upload": "스티커 업로드" }, "submit": "제출", @@ -496,6 +500,9 @@ "unblock": "사용자 차단 해제", "viewProfile": "프로필 보기" }, + "webauth": { + "keyname": "키 이름:" + }, "webhook": "웹훅", "webhooks": { "EnterWebhookName": "웹훅 이름 입력", From 5bffd1e49423056bb36e3d99d7c5ec6cc33a62a9 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 12:50:54 -0500 Subject: [PATCH 02/14] fix reaction bug --- src/webpage/favorites.ts | 2 ++ src/webpage/message.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/webpage/favorites.ts b/src/webpage/favorites.ts index 207e035..1a83344 100644 --- a/src/webpage/favorites.ts +++ b/src/webpage/favorites.ts @@ -122,6 +122,8 @@ export class Favorites { this.gifs = deapClone(store.current.gifs); this.emojiFrecency = deapClone(store.current.emojiFrecency); this.emojiReactionFrecency = deapClone(store.current.emojiReactionFrecency); + //TODO remove this eventually + delete this.emojiReactionFrecency["undefined"]; this.guildAndChannelFrecency = deapClone(store.current.guildAndChannelFrecency); } saveLocal() { diff --git a/src/webpage/message.ts b/src/webpage/message.ts index eded582..39d5928 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -355,7 +355,11 @@ class Message extends SnowFlake { let reactiontxt: string; if (emoji instanceof Emoji) { - reactiontxt = `${emoji.name}:${emoji.id}`; + if (emoji.id) { + reactiontxt = `${emoji.name}:${emoji.id}`; + } else { + reactiontxt = encodeURIComponent(emoji.name); + } } else { reactiontxt = encodeURIComponent(emoji); } From 557783dbbd42c96d72223bffc5e1e15f1f9f29f5 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 12:55:11 -0500 Subject: [PATCH 03/14] make self reactions more clear on dark-accent theme --- src/webpage/themes.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/webpage/themes.css b/src/webpage/themes.css index 8eaa226..54c5ff3 100644 --- a/src/webpage/themes.css +++ b/src/webpage/themes.css @@ -125,28 +125,24 @@ --primary-hover: color-mix(in srgb, #373737 68%, var(--accent-color)); --primary-text: #ebebeb; --primary-text-soft: #ebebebb8; - --secondary-bg: color-mix(in srgb, #222222 72%, var(--accent-color)); --secondary-hover: color-mix(in srgb, #222222 65%, var(--accent-color)); --folder-bg: color-mix(in srgb, var(--primary-text-soft) 70%, var(--accent-color)); - --servers-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color)); --channels-bg: color-mix(in srgb, #292929 68%, var(--accent-color)); --channel-selected: color-mix(in srgb, #555555 65%, var(--accent-color)); --typebox-bg: color-mix(in srgb, #666666 60%, var(--accent-color)); - --button-bg: color-mix(in srgb, #777777 56%, var(--accent-color)); --button-hover: color-mix(in srgb, #585858 58%, var(--accent-color)); - --spoiler: color-mix(in srgb, #101010 72%, var(--accent-color)); --link: color-mix(in srgb, #99ccff 75%, var(--accent-color)); - --black: color-mix(in srgb, #000000 90%, var(--accent-color)); --icon: color-mix(in srgb, #ffffff, var(--accent-color)); --dock-bg: color-mix(in srgb, #171717 68%, var(--accent-color)); --spoiler-hover: color-mix(in srgb, #111111 80%, var(--accent-color)); --card-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color)); --spoiler-bg: #000000; + --reaction-reacted-bg: color-mix(in srgb, var(--reaction-bg) 80%, white); } /* Optional Variables */ From ff13770a736db21af8230fae5fce20027b297929 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 12:57:59 -0500 Subject: [PATCH 04/14] delete it more --- src/webpage/favorites.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webpage/favorites.ts b/src/webpage/favorites.ts index 1a83344..3b7c526 100644 --- a/src/webpage/favorites.ts +++ b/src/webpage/favorites.ts @@ -124,6 +124,7 @@ export class Favorites { this.emojiReactionFrecency = deapClone(store.current.emojiReactionFrecency); //TODO remove this eventually delete this.emojiReactionFrecency["undefined"]; + delete store.current.emojiReactionFrecency["undefined"]; this.guildAndChannelFrecency = deapClone(store.current.guildAndChannelFrecency); } saveLocal() { From 99e5530db69de53d292e33d614c880438cb139fe Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 12:59:48 -0500 Subject: [PATCH 05/14] more deleting --- src/webpage/favorites.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/webpage/favorites.ts b/src/webpage/favorites.ts index 3b7c526..077afc4 100644 --- a/src/webpage/favorites.ts +++ b/src/webpage/favorites.ts @@ -122,9 +122,7 @@ export class Favorites { this.gifs = deapClone(store.current.gifs); this.emojiFrecency = deapClone(store.current.emojiFrecency); this.emojiReactionFrecency = deapClone(store.current.emojiReactionFrecency); - //TODO remove this eventually - delete this.emojiReactionFrecency["undefined"]; - delete store.current.emojiReactionFrecency["undefined"]; + this.guildAndChannelFrecency = deapClone(store.current.guildAndChannelFrecency); } saveLocal() { @@ -184,6 +182,11 @@ export class Favorites { headers: this.headers, }); const res: {settings: Partial} = await (await sat).json(); + //TODO remove this eventually + delete this.emojiReactionFrecency["undefined"]; + delete this.store.current.emojiReactionFrecency["undefined"]; + if (res.settings.emojiReactionFrecency) + delete res.settings.emojiReactionFrecency.emojis["undefined"]; this.saveDifs(res.settings, save); } async setup() { From dc56d77f4feb86fd68ba98d2629333b37126b624 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 13:32:08 -0500 Subject: [PATCH 06/14] make discovery page-able --- src/webpage/discovery.ts | 154 ++++++++++++++++++++++++--------------- src/webpage/style.css | 2 +- 2 files changed, 98 insertions(+), 58 deletions(-) diff --git a/src/webpage/discovery.ts b/src/webpage/discovery.ts index 53c7280..3922941 100644 --- a/src/webpage/discovery.ts +++ b/src/webpage/discovery.ts @@ -92,7 +92,6 @@ export class Discovery { const content = document.createElement("div"); content.classList.add("flexttb", "guildy", "messagecontainer"); - content.textContent = I18n.guild.loadingDiscovery(); scrollWrap.append(content); @@ -100,79 +99,120 @@ export class Discovery { guildsButton.textContent = I18n.guild.guilds(); guildsButton.classList.add("discoverButton", "selected"); channels.append(guildsButton); + const guilds = document.createElement("div"); + guilds.classList.add("discovery-guild-content"); - const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { - headers: this.headers, - }); - const json = await res.json(); - console.log([...json.guilds], json.guilds); - - content.innerHTML = ""; const title = document.createElement("h2"); - title.textContent = I18n.guild.disoveryTitle(json.guilds.length + ""); content.appendChild(title); - const guilds = document.createElement("div"); - guilds.id = "discovery-guild-content"; + const buttonRow = document.createElement("div"); + buttonRow.classList.add("flexltr"); - json.guilds.forEach((guild: guildjson["properties"]) => { - const content = document.createElement("div"); + const render = async (offset = 0) => { + guilds.textContent = I18n.guild.loadingDiscovery(); + const limit = 50; + const res = await fetch( + `${this.info.api}/discoverable-guilds?limit=${limit}&offset=${offset}`, + { + headers: this.headers, + }, + ); - this.context.bindContextmenu(content, guild.id, () => { - const div = document.createElement("div"); - div.classList.add("flexltr"); + const json = (await res.json()) as {guilds: guildjson["properties"][]; total: number}; + console.log([...json.guilds], json.guilds); + guilds.textContent = ""; + + title.textContent = I18n.guild.disoveryTitle(json.total + ""); + + content.appendChild(guilds); + + json.guilds.forEach((guild) => { + const content = document.createElement("div"); + + this.context.bindContextmenu(content, guild.id, () => { + const div = document.createElement("div"); + div.classList.add("flexltr"); + const img = this.getIconURL(guild); + img.classList.add("icon"); + img.crossOrigin = "anonymous"; + + img.alt = ""; + div.appendChild(img); + + const name = document.createElement("h3"); + name.textContent = guild.name; + div.appendChild(name); + return div; + }); + content.classList.add("discovery-guild"); + const banner = this.getBannerURL(guild); + if (banner) { + banner.classList.add("banner"); + banner.crossOrigin = "anonymous"; + banner.alt = ""; + content.appendChild(banner); + } + + const nameContainer = document.createElement("div"); + nameContainer.classList.add("flex"); const img = this.getIconURL(guild); img.classList.add("icon"); img.crossOrigin = "anonymous"; img.alt = ""; - div.appendChild(img); + nameContainer.appendChild(img); const name = document.createElement("h3"); name.textContent = guild.name; - div.appendChild(name); - return div; + nameContainer.appendChild(name); + content.appendChild(nameContainer); + const desc = document.createElement("p"); + desc.textContent = guild.description; + content.appendChild(desc); + + content.addEventListener("click", async () => { + let guildObj = this.localuser.guildids.get(guild.id); + if (guildObj) { + guildObj.loadGuild(); + guildObj.loadChannel(); + return; + } + if (await this.confirmJoin(guild)) { + await this.join(guild); + } + }); + guilds.appendChild(content); }); - content.classList.add("discovery-guild"); - const banner = this.getBannerURL(guild); - if (banner) { - banner.classList.add("banner"); - banner.crossOrigin = "anonymous"; - banner.alt = ""; - content.appendChild(banner); + + let switching = false; + + buttonRow.textContent = ""; + + if (offset !== 0) { + const back = document.createElement("button"); + back.textContent = I18n.search.back(); + buttonRow.append(back); + back.onclick = () => { + if (switching) return; + switching = true; + render(offset - limit); + }; } + //TODO once https://codeberg.org/MelodyChat/Harmony/pulls/77 is merged this should be reverted to a < only, the === case means there is no more, though right now server side logic is incorrect. + if (offset + json.guilds.length <= json.total) { + const next = document.createElement("button"); + next.textContent = I18n.search.next(); + buttonRow.append(next); + next.onclick = () => { + if (switching) return; + switching = true; + render(offset + limit); + }; + } + content.append(buttonRow); + }; - const nameContainer = document.createElement("div"); - nameContainer.classList.add("flex"); - const img = this.getIconURL(guild); - img.classList.add("icon"); - img.crossOrigin = "anonymous"; - - img.alt = ""; - nameContainer.appendChild(img); - - const name = document.createElement("h3"); - name.textContent = guild.name; - nameContainer.appendChild(name); - content.appendChild(nameContainer); - const desc = document.createElement("p"); - desc.textContent = guild.description; - content.appendChild(desc); - - content.addEventListener("click", async () => { - let guildObj = this.localuser.guildids.get(guild.id); - if (guildObj) { - guildObj.loadGuild(); - guildObj.loadChannel(); - return; - } - if (await this.confirmJoin(guild)) { - await this.join(guild); - } - }); - guilds.appendChild(content); - }); - content.appendChild(guilds); + render(); } getIconURL(guild: guildjson["properties"]) { return createImg( diff --git a/src/webpage/style.css b/src/webpage/style.css index 3418123..323c7df 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -3847,7 +3847,7 @@ fieldset input[type="radio"] { padding: 10px; box-sizing: border-box; } -#discovery-guild-content { +.discovery-guild-content { flex: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); From b2c75957e10199c30a75e03eb2b282f3245affd5 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 13:41:11 -0500 Subject: [PATCH 07/14] fix DM search --- src/webpage/localuser.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index f2d5a94..a328aa2 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -4541,9 +4541,15 @@ class Localuser { if (!sideDiv || !sideContainDiv) return; const genPage = (page: number) => { p.set("offset", page * 50 + ""); - fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), { - headers: this.headers, - }) + const guildSearch = this.lookingguild?.id !== "@me"; + fetch( + this.info.api + + `${guildSearch ? `/guilds/${this.lookingguild?.id}` : `/channels/${this.channelfocus?.id}`}/messages/search/?` + + p.toString(), + { + headers: this.headers, + }, + ) .then((_) => _.json()) .then((json: {messages: [messagejson][]; total_results: number}) => { if (this.curSearch !== searchy) { From ea00aa6a8324ccf13fa3edb1f6d86a5931fad249 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 15:45:46 -0500 Subject: [PATCH 08/14] search filtering --- src/webpage/localuser.ts | 148 ++++++++++++++++++++++++++++++++- src/webpage/settings.ts | 173 ++++++++++++++++++++++++++++++++++++++- src/webpage/snowflake.ts | 3 + src/webpage/style.css | 58 +++++++++++++ translations/en.json | 9 +- 5 files changed, 386 insertions(+), 5 deletions(-) diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index a328aa2..c32a6ab 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -47,6 +47,7 @@ import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSet import {getLocalSettings, ServiceWorkerModeValues} from "./utils/storage/localSettings.js"; import {PromiseLock} from "./utils/promiseLock.js"; import {CDNParams} from "./utils/cdnParams.js"; +import {SnowFlake} from "./snowflake.js"; type traceObj = { micros: number; calls?: (string | traceObj)[]; @@ -4359,7 +4360,7 @@ class Localuser { MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { const maybe: [number, Channel][] = []; if (this.lookingguild && this.lookingguild.id !== "@me") { - for (const channel of this.lookingguild.channels) { + for (const channel of this.lookingguild.channels.filter((_) => _.visible)) { const confidence = channel.similar(name); if (confidence > 0) { maybe.push([confidence, channel]); @@ -4539,13 +4540,19 @@ class Localuser { const sideDiv = document.getElementById("sideDiv"); const sideContainDiv = document.getElementById("sideContainDiv"); if (!sideDiv || !sideContainDiv) return; + let authorIds = [] as string[]; + let mentionIds = [] as string[]; + let channels = [] as Channel[]; const genPage = (page: number) => { p.set("offset", page * 50 + ""); const guildSearch = this.lookingguild?.id !== "@me"; fetch( this.info.api + `${guildSearch ? `/guilds/${this.lookingguild?.id}` : `/channels/${this.channelfocus?.id}`}/messages/search/?` + - p.toString(), + p.toString() + + (authorIds.length ? authorIds.map((_) => `&author_id=${_}`).join("") : "") + + (mentionIds.length ? mentionIds.map((_) => `&mentions=${_}`).join("") : "") + + (channels.length ? channels.map((_) => `&channel_id=${_.id}`).join("") : ""), { headers: this.headers, }, @@ -4577,6 +4584,141 @@ class Localuser { const sortBar = document.createElement("div"); sortBar.classList.add("flexltr", "sortBar"); + const settingsB = document.createElement("button"); + settingsB.textContent = I18n.search.settings(); + settingsB.onclick = () => { + const d = new Dialog(I18n.search.settings()); + const opt = d.options; + const b = p.get("max_id"); + const before = opt.addDateInput(I18n.search.before(), () => {}, { + initText: b ? new Date(SnowFlake.stringToUnixTime(b)) : undefined, + }); + before.onchange = (_) => { + if (before.dateValue) p.set("max_id", SnowFlake.DateToID(before.dateValue)); + else p.delete("max_id"); + console.log([...p.entries()], before.dateValue); + }; + + const a = p.get("min_id"); + const after = opt.addDateInput(I18n.search.after(), () => {}, { + initText: a ? new Date(SnowFlake.stringToUnixTime(a)) : undefined, + }); + after.onchange = (_) => { + if (after.dateValue) p.set("min_id", SnowFlake.DateToID(after.dateValue)); + else p.delete("min_id"); + console.log([...p.entries()], after.dateValue); + }; + opt.addCheckboxInput(I18n.search.includensfw(), () => {}, { + initState: p.get("include_nsfw") !== "false", + }).onchange = (s) => { + if (s) p.delete("include_nsfw"); + else p.set("include_nsfw", "false"); + }; + const userSearch = async (name: string, ids: string[]) => { + const g = this.lookingguild; + if (!g) return []; + g.searchMembers(8, name); + const members = [] as [User | Member, number][]; + if (g.id === "@me") { + const dirrect = this.channelfocus as Group; + + for (const user of dirrect.users) { + const rank = user.compare(name); + if (rank > 0) { + members.push([user, rank]); + } + } + } else { + for (const member of g.members) { + const rank = member.compare(name); + if (rank > 0) { + members.push([member, rank]); + } + } + } + const idSet = new Set(ids); + members.sort((a, b) => b[1] - a[1]); + + return members + .filter((_) => !idSet.has(_[0].id)) + .slice(0, 5) + .map(([_]) => { + return {value: _.id, name: _.name}; + }); + }; + opt.addAsyncMultiSelect(I18n.search.authors(), () => {}, userSearch, { + defaultValues: authorIds + .map((id) => { + const g = this.lookingguild; + if (!g || g.id === "@me") { + return this.userMap.get(id); + } else { + return [...g.members].find(({id: d}) => d === id); + } + }) + .filter((_) => _ !== undefined) + .map((_) => ({value: _.id, name: _.name})), + }).onchange = (values: string[]) => { + authorIds = values; + }; + + opt.addAsyncMultiSelect(I18n.search.mentions(), () => {}, userSearch, { + defaultValues: mentionIds + .map((id) => { + const g = this.lookingguild; + if (!g || g.id === "@me") { + return this.userMap.get(id); + } else { + return [...g.members].find(({id: d}) => d === id); + } + }) + .filter((_) => _ !== undefined) + .map((_) => ({value: _.id, name: _.name})), + }).onchange = (values: string[]) => { + mentionIds = values; + }; + if (this.lookingguild?.id !== "@me") + opt.addAsyncMultiSelect( + I18n.search.channels(), + () => {}, + async (name, ids) => { + const g = this.lookingguild; + if (!g) return []; + const c = g.channels.filter((_) => _.visible); + + const maybe: [number, Channel][] = []; + + for (const channel of c) { + const confidence = channel.similar(name); + if (confidence > 0) { + maybe.push([confidence, channel]); + } + } + + maybe.sort((a, b) => b[0] - a[0]); + const idSet = new Set(ids); + + return maybe + .filter((_) => !idSet.has(_[1].id)) + .slice(0, 5) + .map(([_r, _]) => { + return {value: _.id, name: _.name}; + }); + }, + { + defaultValues: channels.map((_) => ({name: _.name, value: _.id})), + }, + ).onchange = (values: string[]) => { + const g = this.lookingguild; + if (!g) return; + channels = values.map((id) => g.getChannel(id)).filter((_) => _ !== undefined); + }; + d.onhide = () => { + genPage(0); + }; + d.show(); + }; + const newB = document.createElement("button"); const old = document.createElement("button"); [newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()]; @@ -4599,7 +4741,7 @@ class Localuser { const spaceElm = document.createElement("div"); spaceElm.classList.add("spaceElm"); - sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, newB, old); + sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, settingsB, newB, old); sideDiv.append(sortBar); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 62e7de8..ac8aceb 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -187,6 +187,21 @@ class TextInput implements OptionsElement { } } class DateInput extends TextInput { + dateValue: Date | null; + constructor( + label: string, + onSubmit: (str: string) => void, + owner: Options, + {initText = "" as string | Date} = {}, + ) { + let initDate: DateInput["dateValue"] = null; + if (initText instanceof Date) { + initDate = initText; + initText = ""; + } + super(label, onSubmit, owner, {initText}); + this.dateValue = initDate; + } generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); @@ -195,11 +210,20 @@ class DateInput extends TextInput { const input = document.createElement("input"); input.value = this.value; input.type = "date"; + if (this.dateValue) input.valueAsDate = this.dateValue; input.oninput = this.onChange.bind(this); this.input = new WeakRef(input); div.append(input); return div; } + onChange() { + const input = this.input.deref(); + if (input) { + const value = input.valueAsDate; + this.dateValue = value; + } + super.onChange(); + } } class SettingsMDText implements OptionsElement { readonly onSubmit!: (str: string) => void; @@ -412,7 +436,134 @@ export class ColorInput implements OptionsElement { this.onSubmit(this.colorContent); } } +interface searchRes { + value: string; + name: string; +} +class AsyncMultiSelect implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: string[]) => void; + select!: WeakRef; + searchFunc: (term: string, cur: string[]) => Promise | searchRes[]; + value = [] as string[]; + nmap = [] as string[]; + constructor( + label: string, + onSubmit: (str: string[]) => void, + selections: AsyncMultiSelect["searchFunc"], + owner: Options, + {defaultValues = []}: {defaultValues: searchRes[]} = {defaultValues: []}, + ) { + this.label = label; + this.value = defaultValues.map((_) => _.value); + this.nmap = defaultValues.map((_) => _.name); + this.owner = owner; + this.onSubmit = onSubmit; + this.searchFunc = selections; + } + generateHTML() { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const s = document.createElement("div"); + s.classList.add("amsCont"); + const genArea = () => { + s.textContent = ""; + let i = 0; + for (const name of this.nmap) { + const si = i++; + const elm = document.createElement("div"); + elm.classList.add("amsElm", "flexltr"); + const x = document.createElement("span"); + x.classList.add("svgicon", "svg-plainx"); + x.onclick = () => { + this.value.splice(si, 1); + this.nmap.splice(si, 1); + genArea(); + this.onchange(this.value); + }; + const nspan = document.createElement("span"); + nspan.textContent = name; + + elm.append(nspan, x); + + s.append(elm); + } + const addbg = document.createElement("div"); + addbg.classList.add("addbg"); + const add = document.createElement("span"); + add.classList.add("svgicon", "svg-plus"); + addbg.append(add); + s.append(addbg); + add.onclick = (e) => { + this.popUpSearchBox(e.x, e.y, () => { + genArea(); + this.onchange(this.value); + }); + }; + }; + genArea(); + div.append(s); + return div; + } + async popUpSearchBox(x: number, y: number, done: () => void) { + const searchBox = document.createElement("div"); + searchBox.style.top = y + "px"; + searchBox.style.left = x + "px"; + searchBox.classList.add("amsBox"); + const input = document.createElement("input"); + input.type = "text"; + + const reses = document.createElement("div"); + reses.classList.add("flexttb", "reses"); + searchBox.append(input, reses); + let opts = [] as searchRes[]; + const search = async () => { + const res = await this.searchFunc(input.value || "", this.value); + reses.textContent = ""; + opts = res; + for (const opt of opts) { + const span = document.createElement("span"); + span.textContent = opt.name; + span.onmousedown = () => { + removeAni(searchBox); + this.value.push(opt.value); + this.nmap.push(opt.name); + done(); + }; + reses.append(span); + } + input.onblur = async () => { + removeAni(searchBox); + }; + }; + + input.onkeyup = (e) => { + if (e.key === "Enter" && opts[0]) { + removeAni(searchBox); + this.value.push(opts[0].value); + this.nmap.push(opts[0].name); + done(); + } else { + search(); + } + }; + + search(); + document.body.append(searchBox); + input.focus(); + } + submit() { + this.onSubmit(this.value); + } + onchange: (str: string[]) => void = (_) => {}; + watchForChange(func: (str: string[]) => void) { + this.onchange = func; + } +} class SelectInput implements OptionsElement { readonly label: string; readonly owner: Options; @@ -869,6 +1020,7 @@ class Dialog { this.float = new Float(name, {ltr, noSubmit}); this.above = goAbove; } + onhide = () => {}; show(hideOnClick = true) { const background = document.createElement("div"); background.classList.add("background"); @@ -883,6 +1035,7 @@ class Dialog { background.onclick = (_) => { if (hideOnClick && _.target === background) { removeAni(background); + this.onhide(); } }; background.tabIndex = 0; @@ -890,6 +1043,7 @@ class Dialog { background.onkeydown = (e) => { if (e.key === "Escape" && hideOnClick) { removeAni(background); + this.onhide(); } }; return center; @@ -1177,6 +1331,19 @@ class Options implements OptionsElement { this.generate(select); return select; } + addAsyncMultiSelect( + label: string, + onSubmit: (str: string[]) => void, + selections: AsyncMultiSelect["searchFunc"], + {defaultValues = [] as searchRes[]} = {}, + ) { + const select = new AsyncMultiSelect(label, onSubmit, selections, this, { + defaultValues, + }); + this.options.push(select); + this.generate(select); + return select; + } addImageInput( label: string, onSubmit: (files: FileList | null) => void, @@ -1193,7 +1360,11 @@ class Options implements OptionsElement { this.generate(FI); return FI; } - addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) { + addDateInput( + label: string, + onSubmit: (str: string | undefined) => void, + {initText = "" as string | Date} = {}, + ) { const textInput = new DateInput(label, onSubmit, this, { initText, }); diff --git a/src/webpage/snowflake.ts b/src/webpage/snowflake.ts index 2a2af43..f1ee276 100644 --- a/src/webpage/snowflake.ts +++ b/src/webpage/snowflake.ts @@ -13,5 +13,8 @@ abstract class SnowFlake { throw new Error(`The ID is corrupted, it's ${str} when it should be some number.`); } } + static DateToID(date: Date) { + return ((BigInt(+date) - 1420070400000n) << 22n).toString(); + } } export {SnowFlake}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 323c7df..e0e7883 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -183,6 +183,42 @@ body { min-height: 0; display: flex; } +.amsCont { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + + .addbg { + background: #0000006e; + padding: 4px; + border-radius: 6px; + height: 16px; + width: 16px; + cursor: pointer; + } + .svg-plus { + width: 16px; + height: 16px; + } +} +.amsElm { + height: 18px; + background: #00000061; + border-radius: 4px; + margin: 4px; + padding: 4px; + width: fit-content; + cursor: pointer; + align-items: center; + + .svg-plainx { + margin-left: 10px; + width: 14px; + height: 14px; + cursor: pointer; + } +} .sessionDiv { padding: 8px; background: #00000040; @@ -221,6 +257,15 @@ body { display: flex; flex-direction: column; } +.reses { + span { + background: #0000007a; + margin: 3px; + padding: 3px; + border-radius: 4px; + cursor: pointer; + } +} .actConnectionDiv { background: #0000005c; padding: 6px; @@ -5051,3 +5096,16 @@ img.error::after { max-height: 400px; border-radius: 3px; } +.amsBox { + position: absolute; + z-index: 10000; + padding: 5px; + background: var(--primary-bg); + box-shadow: 0px 0px 3px 1px black; + border-radius: 6px; + &.removeElm { + animation-duration: 0.2s; + animation-name: context-fade-out; + animation-timing-function: ease-out; + } +} diff --git a/translations/en.json b/translations/en.json index 5931658..f6138e7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -825,7 +825,14 @@ "nofind": "There seems to be no messages that match your search, maybe trying broadening your search to try and find what you want", "old": "Old", "page": "Page $1", - "search": "Search" + "search": "Search", + "settings":"Search Settings", + "before":"Before:", + "after":"After:", + "includensfw":"Include NSFW:", + "authors":"Authors:", + "mentions":"Mentions:", + "channels":"Channels:" }, "searchGifs": "Search $1", "settings": { From 4933345965f4651f785831548fe326e040b10024 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 15:51:37 -0500 Subject: [PATCH 09/14] that's not async --- src/webpage/localuser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index c32a6ab..096be62 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -4681,7 +4681,7 @@ class Localuser { opt.addAsyncMultiSelect( I18n.search.channels(), () => {}, - async (name, ids) => { + (name, ids) => { const g = this.lookingguild; if (!g) return []; const c = g.channels.filter((_) => _.visible); From 56e9ef77329f0c8acac88cd9655e6815ed2495a4 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 20 May 2026 15:56:07 -0500 Subject: [PATCH 10/14] hypoprofile no friend --- src/webpage/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpage/user.ts b/src/webpage/user.ts index a3dfa57..1017641 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -1587,7 +1587,7 @@ class User extends SnowFlake { friendSpan.classList.add("svg-hasfriend"); } }; - if (this !== this.localuser.user && !this.bot) { + if (this !== this.localuser.user && !this.bot && x !== -1) { friendDiv.append(friendSpan); div.append(friendDiv); updateIcon(); From 0e0af2a6dde514e3f22a114cfbd1cf57081547c3 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 21 May 2026 14:18:50 +0200 Subject: [PATCH 11/14] Localisation updates from https://translatewiki.net. --- translations/ko.json | 3 ++- translations/nl.json | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/translations/ko.json b/translations/ko.json index 6c500e0..b1e7fc6 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -53,7 +53,8 @@ "guild_booster_lvl6": "부스트된 길드", "guild_booster_lvl7": "부스트된 길드", "guild_booster_lvl8": "부스트된 길드", - "guild_booster_lvl9": "잠시동안 부스트된 길드" + "guild_booster_lvl9": "잠시동안 부스트된 길드", + "hypesquad": "Vibesquad [자리 표시자]" }, "blankMessage": "빈 메시지", "blog": { diff --git a/translations/nl.json b/translations/nl.json index a2911fb..1705618 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -827,7 +827,14 @@ "nofind": "Er zijn er geen berichten gevonden met uw zoekopdracht. Misschien vind u wat u zoekt als u de zoekopdracht verbreedt.", "old": "Oud", "page": "Pagina $1", - "search": "Zoeken" + "search": "Zoeken", + "settings": "Zoekinstellingen", + "before": "Vóór:", + "after": "Na:", + "includensfw": "NSFW opnemen:", + "authors": "Auterurs:", + "mentions": "Vermeldingen:", + "channels": "Kanalen:" }, "searchGifs": "Zoeken in $1", "settings": { From eeff93db285c508b0e2b082859f8e42d5933c439 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 21 May 2026 21:32:45 -0500 Subject: [PATCH 12/14] user notes --- src/webpage/jsontypes.ts | 22 ++++++++++++++++++++++ src/webpage/localuser.ts | 5 +++++ src/webpage/style.css | 3 +++ src/webpage/user.ts | 35 +++++++++++++++++++++++++++++++++++ translations/en.json | 3 ++- 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 4949e78..24031c4 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -939,6 +939,27 @@ type messageCreateJson = { s: number; t: "MESSAGE_CREATE"; }; +type userNote = { + op: 0; + t: "USER_NOTE_UPDATE"; + d: { + note: string; + id: string; + }; + s: number; +}; +export type pollUpdateJson = { + op: 0; + d: { + user_id: string; + channel_id: string; + message_id: string; + guild_id?: string; + answer_id: number; + }; + s: number; + t: "MESSAGE_POLL_VOTE_ADD" | "MESSAGE_POLL_VOTE_REMOVE"; +}; export interface relationJson { id: string; type: 0 | 1 | 2 | 3 | 4; @@ -1030,6 +1051,7 @@ type wsjson = heartbeat_interval: number; }; } + | userNote | { op: 0; t: "MESSAGE_REACTION_ADD"; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 096be62..1f4d386 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -769,6 +769,11 @@ class Localuser { this.messageCreate(temp); } break; + case "USER_NOTE_UPDATE": { + const u = this.userMap.get(temp.d.id); + if (u) u.note = temp.d.note; + break; + } case "USER_CONNECTIONS_UPDATE": { this.conectionChange(); break; diff --git a/src/webpage/style.css b/src/webpage/style.css index e0e7883..aa69231 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -422,6 +422,9 @@ body { padding: 0; border: 0; } +.userNote { + max-width: 100% !important; +} #player { flex-grow: 0; diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 1017641..a85a9cf 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -1173,6 +1173,33 @@ class User extends SnowFlake { } } } + note?: string; + async saveNotes(note: string) { + this.note = note; + await fetch(`${this.info.api}/users/@me/notes/${this.id}`, { + method: "PUT", + headers: this.headers, + body: JSON.stringify({note}), + }); + } + async getNotes() { + try { + if (this.note === undefined) { + this.note = + ( + await ( + await fetch(`${this.info.api}/users/@me/notes/${this.id}`, { + method: "GET", + headers: this.headers, + }) + ).json() + ).note || ""; + } + } catch { + this.note = ""; + } + return this.note as string; + } async fullProfile(guild: Guild | null | Member = null) { console.log(guild); const membres = (async () => { @@ -1380,6 +1407,14 @@ class User extends SnowFlake { document.body.append(background); background.append(div); console.log(background); + const notes = buttons.add(I18n.profile.notes()); + const textArea = document.createElement("textarea"); + textArea.classList.add("userNote"); + notes.addHTMLArea(textArea); + this.getNotes().then((_) => (textArea.textContent = _)); + textArea.onblur = () => { + this.saveNotes(textArea.value); + }; (async () => { const high = await this.highInfo(); const mut = buttons.add(I18n.profile.mut()); diff --git a/translations/en.json b/translations/en.json index f6138e7..2d23705 100644 --- a/translations/en.json +++ b/translations/en.json @@ -773,7 +773,8 @@ "mut": "Mutual guilds", "mutFriends": "Mutual friends", "permInfo": "Permissions", - "userInfo": "User info" + "userInfo": "User info", + "notes":"User notes" }, "profileColor": "Profile color", "pronouns": "Pronouns:", From 12ee608c3c2665c2569cba0b1b4d257f9fb12a4c Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 21 May 2026 21:48:27 -0500 Subject: [PATCH 13/14] ban by id --- src/webpage/guild.ts | 20 ++++++++++++++++++++ translations/en.json | 2 ++ 2 files changed, 22 insertions(+) diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index d9a306d..5386c98 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -1074,6 +1074,26 @@ class Guild extends SnowFlake { if (this.member.hasPermission("BAN_MEMBERS")) { const banMenu = settings.addButton(I18n.guild.bans()); + banMenu.addButtonInput("", I18n.guild.banId(), () => { + const opt = banMenu.addSubOptions(I18n.guild.banId(), {noSubmit: true}); + const reason = opt.addTextInput(I18n.member["reason:"](), () => {}); + opt.addTextInput(I18n.guild.idSel(), async (id) => { + const headers = structuredClone(this.headers); + (headers as any)["x-audit-log-reason"] = reason.value; + const ret = await fetch(`${this.info.api}/guilds/${this.id}/bans/${id}`, { + method: "PUT", + headers, + }); + if (!ret.ok) { + new Dialog((await ret.json()).message).show(); + return; + } + banMenu.returnFromSub(); + }); + opt.addButtonInput("", I18n.submit(), () => { + opt.submit(); + }); + }); const makeBanMenu = () => { const banDiv = document.createElement("div"); const bansp = ProgessiveDecodeJSON( diff --git a/translations/en.json b/translations/en.json index 2d23705..dbc8283 100644 --- a/translations/en.json +++ b/translations/en.json @@ -320,6 +320,8 @@ "banner:": "Banner:", "bans": "Bans", "boostMessage?": "Send a message when someone boosts your guild!", + "banId":"Ban by ID", + "idSel":"ID:", "community": "Community", "confirmDelete": "Are you sure you want to delete $1?", "confirmLeave": "Are you sure you want to leave?", From 4e4650f885803c3ddc0cd62f89fffb75b4971257 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 21 May 2026 23:47:54 -0500 Subject: [PATCH 14/14] fix ban render bug --- src/webpage/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/webpage/style.css b/src/webpage/style.css index aa69231..5e19c5d 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -78,6 +78,10 @@ body { div { margin-left: 8px; } + .pfp { + width: 32px; + height: 32px; + } } .templateMiniBox { display: flex;