From 956016a9a03409a8ca71ee37a5e2bf97db9d5342 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Fri, 11 Apr 2025 12:11:27 -0500 Subject: [PATCH] create guild settings for stickers --- src/webpage/emoji.ts | 21 +++++++ src/webpage/guild.ts | 117 ++++++++++++++++++++++++++++++++++++++- src/webpage/jsontypes.ts | 20 ++++++- src/webpage/localuser.ts | 8 +++ src/webpage/settings.ts | 100 +++++++++++++++++++++++++++++++++ src/webpage/sticker.ts | 36 ++++++++++++ src/webpage/style.css | 52 ++++++++++++++++- translations/en.json | 12 ++++ 8 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 src/webpage/sticker.ts diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 3b65e4e..b463c08 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -121,6 +121,27 @@ class Emoji { Emoji.decodeEmojiList(e); }); } + static getEmojiFromIDOrString(idOrString: string, localuser: Localuser) { + for (const list of Emoji.emojis) { + const emj = list.emojis.find((_) => _.emoji === idOrString); + if (emj) { + return new Emoji(emj, localuser); + } + } + for (const guild of localuser.guilds) { + const emj = guild.emojis.find((_) => _.id === idOrString); + if (emj) { + return new Emoji(emj, localuser); + } + } + return new Emoji( + { + id: idOrString, + name: "", + }, + localuser, + ); + } static async emojiPicker( this: typeof Emoji, x: number, diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 63c4775..e1bd5d1 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -3,7 +3,7 @@ import {Localuser} from "./localuser.js"; import {Contextmenu} from "./contextmenu.js"; import {Role, RoleList} from "./role.js"; import {Member} from "./member.js"; -import {Dialog, Options, Settings} from "./settings.js"; +import {Dialog, FormError, Options, Settings} from "./settings.js"; import {Permissions} from "./permissions.js"; import {SnowFlake} from "./snowflake.js"; import { @@ -20,6 +20,7 @@ import {I18n} from "./i18n.js"; import {Emoji} from "./emoji.js"; import {webhookMenu} from "./webhooks.js"; import {createImg} from "./utils/utils.js"; +import {Sticker} from "./sticker.js"; class Guild extends SnowFlake { owner!: Localuser; @@ -39,6 +40,7 @@ class Guild extends SnowFlake { html!: HTMLElement; emojis!: emojipjson[]; large!: boolean; + stickers!: Sticker[]; members = new Set(); static contextmenu = new Contextmenu("guild menu"); static setupcontextmenu() { @@ -311,6 +313,117 @@ class Guild extends SnowFlake { genDiv(); emoji.addHTMLArea(containdiv); } + { + const emoji = settings.addButton(I18n.sticker.title()); + emoji.addButtonInput("", I18n.sticker.upload(), () => { + const popup = new Dialog(I18n.sticker.upload()); + const form = popup.options.addForm("", async () => { + const body = new FormData(); + body.set("name", name.value); + if (!filei.value) throw new FormError(filei, I18n.sticker.errFileMust()); + const file = filei.value.item(0); + if (!file) throw new FormError(filei, I18n.sticker.errFileMust()); + body.set("file", file); + if (!tags.value) throw new FormError(tags, I18n.sticker.errEmjMust()); + if (tags.value.id) { + body.set("tags", tags.value.id); + } else if (tags.value.emoji) { + body.set("tags", tags.value.emoji); + } else { + throw new FormError(tags, I18n.sticker.errEmjMust()); + } + const res = await fetch(this.info.api + "/guilds/" + this.id + "/stickers", { + method: "POST", + headers: { + Authorization: this.headers.Authorization, + }, + body, + }); + if (res.ok) { + popup.hide(); + } else { + const json = await res.json(); + if ("message" in json && typeof json.message === "string") { + throw new FormError(filei, json.message); + } + } + }); + const filei = form.addFileInput(I18n.sticker.image(), "file", {required: true}); + const name = form.addTextInput(I18n.sticker.name(), "name", {required: true}); + const tags = form.addEmojiInput(I18n.sticker.tags(), "tags", this.localuser, { + required: true, + }); + popup.show(); + }); + const containdiv = document.createElement("div"); + containdiv.classList.add("stickersDiv"); + const genDiv = () => { + containdiv.innerHTML = ""; + for (const sticker of this.stickers) { + const div = document.createElement("div"); + div.classList.add("flexttb", "stickerOption"); + + const text = document.createElement("span"); + text.textContent = sticker.name; + + div.onclick = () => { + const form = emoji.addSubForm(emoji.name, () => {}, { + fetchURL: this.info.api + "/guilds/" + this.id + "/stickers/" + sticker.id, + method: "PATCH", + headers: this.headers, + traditionalSubmit: true, + }); + + form.addHTMLArea(sticker.getHTML()); + form.addTextInput(I18n.sticker.name(), "name", { + initText: sticker.name, + }); + + form.addMDInput(I18n.sticker.desc(), "description", { + initText: sticker.description, + }); + + let initEmoji = Emoji.getEmojiFromIDOrString(sticker.tags, this.localuser); + form.addEmojiInput(I18n.sticker.tags(), "tags", this.localuser, { + initEmoji, + required: false, + }); + + form.addButtonInput("", I18n.sticker.del(), () => { + const diaolog = new Dialog(""); + diaolog.options.addTitle(I18n.sticker.confirmDel()); + const options = diaolog.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.yes(), () => { + fetch(`${this.info.api}/guilds/${this.id}/stickers/${sticker.id}`, { + method: "DELETE", + headers: this.headers, + }); + diaolog.hide(); + }); + options.addButtonInput("", I18n.no(), () => { + diaolog.hide(); + }); + diaolog.show(); + }); + }; + + div.append(sticker.getHTML(), text); + + containdiv.append(div); + } + }; + this.onStickerUpdate = () => { + emoji.returnFromSub(); + if (!document.body.contains(containdiv)) { + this.onStickerUpdate = () => {}; + return; + } + genDiv(); + }; + genDiv(); + emoji.addHTMLArea(containdiv); + } + (async () => { const widgetMenu = settings.addButton(I18n.widget()); const cur = (await ( @@ -349,6 +462,7 @@ class Guild extends SnowFlake { } settings.show(); } + onStickerUpdate = (_stickers: Sticker[]) => {}; addCommunity(settings: Settings, textChannels: Channel[]) { const com = settings.addButton(I18n.guild.community()).addForm("", () => {}, { fetchURL: this.info.api + "/guilds/" + this.id, @@ -606,6 +720,7 @@ class Guild extends SnowFlake { } } this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel); + this.stickers = json.stickers.map((_) => new Sticker(_, this)); } get perminfo() { return this.localuser.perminfo.guilds[this.id]; diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 1efe1b1..3c6120c 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -237,12 +237,20 @@ type guildjson = { }; roles: rolesjson[]; stage_instances: []; - stickers: []; + stickers: stickerJson[]; threads: []; version: string; guild_hashes: {}; joined_at: string; }; +interface stickerJson { + id: string; + name: string; + tags: string; + type: number; + format_type: number; + description?: string; +} type extendedProperties = guildjson["properties"] & { emojis: emojipjson[]; large: boolean; @@ -626,6 +634,15 @@ type wsjson = guild_id: string; }; s: number; + } + | { + op: 0; + t: "GUILD_STICKERS_UPDATE"; + d: { + guild_id: string; + stickers: stickerJson[]; + }; + s: 3; }; type memberChunk = { @@ -799,4 +816,5 @@ export { extendedProperties, webhookInfo, webhookType, + stickerJson, }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 21835ff..2c2bf7b 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -31,6 +31,7 @@ import {Message} from "./message.js"; import {badgeArr} from "./Dbadges.js"; import {Rights} from "./rights.js"; import {Contextmenu} from "./contextmenu.js"; +import {Sticker} from "./sticker.js"; const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]); interface CustomHTMLDivElement extends HTMLDivElement { @@ -804,6 +805,13 @@ class Localuser { guild.onEmojiUpdate(guild.emojis); break; } + case "GUILD_STICKERS_UPDATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + guild.stickers = temp.d.stickers.map((_) => new Sticker(_, guild)); + guild.onStickerUpdate(guild.stickers); + break; + } default: { //@ts-ignore console.warn("Unhandled case " + temp.t, temp); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 42d3cc8..038a57a 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -1,4 +1,6 @@ +import {Emoji} from "./emoji.js"; import {I18n} from "./i18n.js"; +import {Localuser} from "./localuser.js"; interface OptionsElement { // @@ -521,6 +523,66 @@ class MDInput implements OptionsElement { this.onSubmit(this.value); } } +class EmojiInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: Emoji | undefined) => void; + input!: WeakRef; + value!: Emoji | undefined; + localuser: Localuser; + constructor( + label: string, + onSubmit: (str: Emoji | undefined) => void, + owner: Options, + localuser: Localuser, + {initEmoji = undefined}: {initEmoji: undefined | Emoji}, + ) { + this.label = label; + this.owner = owner; + this.onSubmit = onSubmit; + this.value = initEmoji; + this.localuser = localuser; + } + generateHTML(): HTMLElement { + const div = document.createElement("div"); + div.classList.add("flexltr", "emojiForm"); + const label = document.createElement("span"); + label.textContent = this.label; + + let emoji: HTMLElement; + if (this.value) { + emoji = this.value.getHTML(); + } else { + emoji = document.createElement("span"); + emoji.classList.add("emptyEmoji"); + } + div.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + (async () => { + const Emoji = (await import("./emoji.js")).Emoji; + const emj = await Emoji.emojiPicker(e.x, e.y, this.localuser); + if (emj) { + this.value = emj; + emoji.remove(); + emoji = emj.getHTML(); + div.append(emoji); + this.onchange(emj); + this.owner.changed(); + } + })(); + }; + div.append(label, emoji); + return div; + } + onchange = (_: Emoji | undefined) => {}; + watchForChange(func: (arg1: Emoji | undefined) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.value); + } +} class FileInput implements OptionsElement { readonly label: string; readonly owner: Options; @@ -654,6 +716,7 @@ class Dialog { background.remove(); } } + export {Dialog}; class Options implements OptionsElement { name: string; @@ -744,6 +807,19 @@ class Options implements OptionsElement { this.genTop(); return options; } + addEmojiInput( + label: string, + onSubmit: (str: Emoji | undefined) => void, + localuser: Localuser, + {initEmoji = undefined} = {} as {initEmoji?: Emoji}, + ) { + const emoji = new EmojiInput(label, onSubmit, this, localuser, { + initEmoji: initEmoji, + }); + this.options.push(emoji); + this.generate(emoji); + return emoji; + } returnFromSub() { this.subOptions = undefined; this.genTop(); @@ -1127,6 +1203,21 @@ class Form implements OptionsElement { } return FI; } + addEmojiInput( + label: string, + formName: string, + localuser: Localuser, + {initEmoji = undefined, required = false} = {} as {initEmoji?: Emoji; required: boolean}, + ) { + const emoji = this.options.addEmojiInput(label, () => {}, localuser, { + initEmoji: initEmoji, + }); + if (required) { + this.required.add(emoji); + } + this.names.set(formName, emoji); + return emoji; + } addTextInput( label: string, @@ -1294,6 +1385,15 @@ class Form implements OptionsElement { } else { console.error(options.files + " is not currently implemented"); } + } else if (input instanceof EmojiInput) { + if (!input.value) { + (build as any)[thing] = undefined; + } else if (input.value.id) { + (build as any)[thing] = input.value.id; + } else if (input.value.emoji) { + (build as any)[thing] = input.value.emoji; + } + continue; } (build as any)[thing] = input.value; } diff --git a/src/webpage/sticker.ts b/src/webpage/sticker.ts new file mode 100644 index 0000000..b5e7ef0 --- /dev/null +++ b/src/webpage/sticker.ts @@ -0,0 +1,36 @@ +import {Guild} from "./guild.js"; +import {stickerJson} from "./jsontypes.js"; +import {SnowFlake} from "./snowflake.js"; +import {createImg} from "./utils/utils.js"; + +class Sticker extends SnowFlake { + name: string; + type: number; + format_type: number; + owner: Guild; + description: string; + tags: string; + get guild() { + return this.owner; + } + get localuser() { + return this.owner.localuser; + } + constructor(json: stickerJson, owner: Guild) { + super(json.id); + this.name = json.name; + this.type = json.type; + this.format_type = json.format_type; + this.owner = owner; + this.tags = json.tags; + this.description = json.description || ""; + } + getHTML(): HTMLElement { + const img = createImg( + this.owner.info.cdn + "/stickers/" + this.id + ".webp?size=160&quality=lossless", + ); + img.classList.add("sticker"); + return img; + } +} +export {Sticker}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 469d907..053d968 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -2046,6 +2046,7 @@ img.bigembedimg { box-sizing: border-box; user-select: none; background: var(--secondary-bg); + z-index: 4; input { width: 1in; @@ -2364,12 +2365,25 @@ fieldset input[type="radio"] { margin-left: auto; } } +.stickersDiv { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} .webhookpfppreview { width: 0.8in; height: 0.8in; border-radius: 1in; margin-right: 0.2in; } +.stickerView { + max-width: 2.5in; + max-height: 2.5in; +} +.sticker { + max-width: 2.5in; + max-height: 2.5in; +} .optionElement, .FormSettings > button { margin: 16px 16px 0 16px; @@ -2749,6 +2763,21 @@ fieldset input[type="radio"] { .friendlyButton:hover { background: black; } +.stickerOption { + border: solid 1px var(--black); + display: flex; + align-items: center; + padding: 0.075in; + margin-bottom: 0.2in; + border-radius: 0.1in; + background: var(--primary-hover); + position: relative; + margin-right: 15px; + cursor: pointer; + img { + height: 2in; + } +} .emojiOption { border: solid 1px var(--black); display: flex; @@ -2833,8 +2862,29 @@ fieldset input[type="radio"] { max-width: 196px; border-radius: 4px; } - cursor: pointer; position: absolute; overflow: hidden; } +.emojiForm { + display: flex; + background: var(--secondary-bg); + padding: 6px; + width: fit-content; + border-radius: 4px; + align-items: center; + cursor: pointer; + :last-child { + margin-left: 6px; + max-width: 32px !important; + max-height: 32px !important; + flex-shrink: 0; + font-size: 28px; + } +} +.emptyEmoji { + background: var(--primary-bg); + width: 32px; + height: 32px; + border-radius: 2in; +} diff --git a/translations/en.json b/translations/en.json index 95db398..4033bf1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -553,6 +553,18 @@ "name:": "Name:", "confirmDel": "Are you sure you want to delete this emoji?" }, + "sticker": { + "title": "Stickers", + "upload": "Upload Stickers", + "image": "Image:", + "name": "Name:", + "desc": "Description", + "confirmDel": "Are you sure you want to delete this sticker?", + "del": "Delete sticker", + "errFileMust": "Must include an image for your sticker", + "errEmjMust": "Must include an emoji with your sticker", + "tags": "Associated Emoji: " + }, "widget": "Guild Widget", "widgetEnabled": "Widget enabled", "incorrectURLS": "## This instance has likely sent the incorrect URLs.\n### If you're the instance owner please see [here](https://docs.spacebar.chat/setup/server/) under *Connecting from remote machines* to correct the issue.\n Would you like Jank Client to automatically try to fix this error to let you connect to the instance?",