diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index add643e..e613838 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -26,6 +26,7 @@ import {I18n} from "./i18n.js"; import {mobile} from "./utils/utils.js"; import {webhookMenu} from "./webhooks.js"; import {File} from "./file.js"; +import {Sticker} from "./sticker.js"; declare global { interface NotificationOptions { @@ -1569,7 +1570,12 @@ class Channel extends SnowFlake { this.fakeMessageMap.delete(id); } - makeFakeMessage(content: string, files: filejson[] = [], reply = undefined) { + makeFakeMessage( + content: string, + files: filejson[] = [], + reply = undefined, + sticker_ids: string[], + ) { const m = new Message( { author: this.localuser.user.tojson(), @@ -1590,6 +1596,11 @@ class Channel extends SnowFlake { type: 0, pinned: false, message_reference: reply, + sticker_items: sticker_ids + .map((_) => { + return Sticker.getFromId(_, this.localuser); + }) + .filter((_) => _ !== undefined), }, this, true, @@ -1654,9 +1665,20 @@ class Channel extends SnowFlake { attachments = [], replyingto = null, embeds = [], - }: {attachments: Blob[]; embeds: embedjson[]; replyingto: Message | null}, + sticker_ids = [], + }: { + attachments: Blob[]; + embeds: embedjson[]; + replyingto: Message | null; + sticker_ids: string[]; + }, ) { - if (content.trim() === "" && attachments.length === 0 && embeds.length == 0) { + if ( + content.trim() === "" && + attachments.length === 0 && + embeds.length == 0 && + sticker_ids.length === 0 + ) { return; } let replyjson: any; @@ -1702,6 +1724,7 @@ class Channel extends SnowFlake { content, nonce: Math.floor(Math.random() * 1000000000), message_reference: undefined, + sticker_ids, }; if (replyjson) { body.message_reference = replyjson; @@ -1714,7 +1737,7 @@ class Channel extends SnowFlake { res.open("POST", this.info.api + "/channels/" + this.id + "/messages"); res.setRequestHeader("Content-type", (ctype = this.headers["Content-type"])); res.setRequestHeader("Authorization", this.headers.Authorization); - funcs = this.makeFakeMessage(content, [], body.message_reference); + funcs = this.makeFakeMessage(content, [], body.message_reference, sticker_ids); res.send((rbody = JSON.stringify(body))); /* res = fetch(this.info.api + "/channels/" + this.id + "/messages", { @@ -1729,6 +1752,7 @@ class Channel extends SnowFlake { content, nonce: Math.floor(Math.random() * 1000000000), message_reference: undefined, + sticker_ids, }; if (replyjson) { body.message_reference = replyjson; @@ -1756,6 +1780,7 @@ class Channel extends SnowFlake { url: URL.createObjectURL(_), })), body.message_reference, + sticker_ids, ); res.send((rbody = formData)); /* diff --git a/src/webpage/icons/sticker.svg b/src/webpage/icons/sticker.svg new file mode 100644 index 0000000..b657613 --- /dev/null +++ b/src/webpage/icons/sticker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index ac7e8cb..2dfe5d4 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -104,6 +104,7 @@
+ diff --git a/src/webpage/index.ts b/src/webpage/index.ts index b2184be..ac56c81 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -309,4 +309,12 @@ import {I18n} from "./i18n.js"; thisUser.makeGifBox(gifTB.getBoundingClientRect()); }; gifTB.onclick = (e) => e.stopImmediatePropagation(); + + const stickerTB = document.getElementById("stickerTB") as HTMLElement; + stickerTB.onmousedown = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + thisUser.makeStickerBox(stickerTB.getBoundingClientRect()); + }; + stickerTB.onclick = (e) => e.stopImmediatePropagation(); })(); diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 3c6120c..edb69e4 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -250,6 +250,7 @@ interface stickerJson { type: number; format_type: number; description?: string; + guild_id?: string; } type extendedProperties = guildjson["properties"] & { emojis: emojipjson[]; @@ -358,6 +359,7 @@ type messagejson = { pinned: boolean; type: number; webhook?: webhookInfo; + sticker_items: stickerJson[]; message_reference?: string; }; type filejson = { diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 2c2bf7b..f2a77b2 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -2279,6 +2279,23 @@ class Localuser { if (!this.channelfocus) return; await this.channelfocus.pinnedClick(rect); } + async makeStickerBox(rect: DOMRect) { + const sticker = await Sticker.stickerPicker( + -0 + rect.right - window.innerWidth, + -20 + rect.top - window.innerHeight, + this, + ); + console.log(sticker); + if (this.channelfocus) { + this.channelfocus.sendMessage("", { + embeds: [], + attachments: [], + sticker_ids: [sticker.id], + replyingto: this.channelfocus.replyingto, + }); + this.channelfocus.replyingto = null; + } + } async makeGifBox(rect: DOMRect) { interface fullgif { id: string; @@ -2373,6 +2390,7 @@ class Localuser { this.channelfocus.sendMessage(gif.url, { embeds: [], attachments: [], + sticker_ids: [], replyingto: this.channelfocus.replyingto, }); menu.remove(); diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 1475c25..d2e0c1d 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -14,9 +14,11 @@ import {mobile} from "./utils/utils.js"; import {I18n} from "./i18n.js"; import {Hover} from "./hover.js"; import {Dialog} from "./settings.js"; +import {Sticker} from "./sticker.js"; class Message extends SnowFlake { static contextmenu = new Contextmenu("message menu"); + stickers: Sticker[]; owner: Channel; headers: Localuser["headers"]; embeds!: Embed[]; @@ -249,6 +251,11 @@ class Message extends SnowFlake { continue; } else if (thing === "author") { continue; + } else if (thing === "sticker_items") { + this.stickers = messagejson.sticker_items.map((_) => { + const guild = this.localuser.guildids.get(_.guild_id as string); + return new Sticker(_, guild || this.localuser); + }); } (this as any)[thing] = (messagejson as any)[thing]; } @@ -791,6 +798,12 @@ class Message extends SnowFlake { text.append(time); div.classList.add("topMessage"); } + const stickerArea = document.createElement("div"); + stickerArea.classList.add("flexltr", "stickerMArea"); + for (const sticker of this.stickers) { + stickerArea.append(sticker.getHTML()); + } + div.append(stickerArea); if (!dupe) { const reactions = document.createElement("div"); reactions.classList.add("flexltr", "reactiondiv"); diff --git a/src/webpage/sticker.ts b/src/webpage/sticker.ts index b5e7ef0..f8e94c4 100644 --- a/src/webpage/sticker.ts +++ b/src/webpage/sticker.ts @@ -1,5 +1,8 @@ +import {Contextmenu} from "./contextmenu.js"; import {Guild} from "./guild.js"; +import {Hover} from "./hover.js"; import {stickerJson} from "./jsontypes.js"; +import {Localuser} from "./localuser.js"; import {SnowFlake} from "./snowflake.js"; import {createImg} from "./utils/utils.js"; @@ -7,16 +10,19 @@ class Sticker extends SnowFlake { name: string; type: number; format_type: number; - owner: Guild; + owner: Guild | Localuser; description: string; tags: string; get guild() { return this.owner; } get localuser() { + if (this.owner instanceof Localuser) { + return this.owner; + } return this.owner.localuser; } - constructor(json: stickerJson, owner: Guild) { + constructor(json: stickerJson, owner: Guild | Localuser) { super(json.id); this.name = json.name; this.type = json.type; @@ -30,7 +36,207 @@ class Sticker extends SnowFlake { this.owner.info.cdn + "/stickers/" + this.id + ".webp?size=160&quality=lossless", ); img.classList.add("sticker"); + const hover = new Hover(this.name); + hover.addEvent(img); + img.alt = this.description; return img; } + static searchStickers(search: string, localuser: Localuser, results = 50): [Sticker, number][] { + //NOTE this function is used for searching in the emoji picker for reactions, and the emoji auto-fill + const ranked: [Sticker, number][] = []; + function similar(json: Sticker) { + if (json.name.includes(search)) { + ranked.push([json, search.length / json.name.length]); + return true; + } else if (json.name.toLowerCase().includes(search.toLowerCase())) { + ranked.push([json, search.length / json.name.length / 1.4]); + return true; + } else { + return false; + } + } + const weakGuild = new WeakMap(); + for (const guild of localuser.guilds) { + if (guild.id !== "@me" && guild.stickers.length !== 0) { + for (const sticker of guild.stickers) { + if (similar(sticker)) { + weakGuild.set(sticker, guild); + } + } + } + } + ranked.sort((a, b) => b[1] - a[1]); + return ranked.splice(0, results).map((a) => { + return a; + }); + } + static getFromId(id: string, localuser: Localuser) { + for (const guild of localuser.guilds) { + const stick = guild.stickers.find((_) => _.id === id); + if (stick) { + return stick; + } + } + return undefined; + } + static async stickerPicker(x: number, y: number, localuser: Localuser): Promise { + let res: (r: Sticker) => void; + this; + const promise: Promise = new Promise((r) => { + res = r; + }); + const menu = document.createElement("div"); + menu.classList.add("flexttb", "stickerPicker"); + if (y > 0) { + menu.style.top = y + "px"; + } else { + menu.style.bottom = y * -1 + "px"; + } + if (x > 0) { + menu.style.left = x + "px"; + } else { + menu.style.right = x * -1 + "px"; + } + + const topBar = document.createElement("div"); + topBar.classList.add("flexltr", "emojiHeading"); + const guilds = [ + localuser.lookingguild, + ...localuser.guilds + .filter((guild) => guild.id != "@me" && guild.stickers.length > 0) + .filter((guild) => guild !== localuser.lookingguild), + ].filter((guild) => guild !== undefined); + + const title = document.createElement("h2"); + title.textContent = guilds[0].properties.name; + title.classList.add("emojiTitle"); + topBar.append(title); + + const search = document.createElement("input"); + search.type = "text"; + topBar.append(search); + + let html: HTMLElement | undefined = undefined; + let topSticker: undefined | Sticker = undefined; + const updateSearch = () => { + if (search.value === "") { + if (html) html.click(); + search.style.removeProperty("width"); + topSticker = undefined; + return; + } + + search.style.setProperty("width", "3in"); + title.innerText = ""; + body.innerHTML = ""; + const searchResults = Sticker.searchStickers(search.value, localuser, 200); + if (searchResults[0]) { + topSticker = searchResults[0][0]; + } + for (const [sticker] of searchResults) { + const emojiElem = document.createElement("div"); + emojiElem.classList.add("stickerSelect"); + + emojiElem.append(sticker.getHTML()); + body.append(emojiElem); + + emojiElem.addEventListener("click", () => { + res(sticker); + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + }); + } + }; + search.addEventListener("input", () => { + updateSearch.call(this); + }); + search.addEventListener("keyup", (e) => { + if (e.key === "Enter" && topSticker) { + res(topSticker); + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + } + }); + + menu.append(topBar); + + const selection = document.createElement("div"); + selection.classList.add("flexltr", "emojirow"); + const body = document.createElement("div"); + body.classList.add("stickerBody"); + + let isFirst = true; + let i = 0; + guilds.forEach((guild) => { + const select = document.createElement("div"); + if (i === 0) { + html = select; + i++; + } + select.classList.add("emojiSelect"); + + if (guild.properties.icon) { + const img = document.createElement("img"); + img.classList.add("pfp", "servericon", "emoji-server"); + img.crossOrigin = "anonymous"; + img.src = + localuser.info.cdn + + "/icons/" + + guild.properties.id + + "/" + + guild.properties.icon + + ".png?size=48"; + img.alt = "Server: " + guild.properties.name; + select.appendChild(img); + } else { + const div = document.createElement("span"); + div.textContent = guild.properties.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word) => word[0]) + .replace(/\s/g, ""); + select.append(div); + } + + selection.append(select); + + const clickEvent = () => { + search.value = ""; + updateSearch.call(this); + title.textContent = guild.properties.name; + body.innerHTML = ""; + for (const sticker of guild.stickers) { + const stickerElem = document.createElement("div"); + stickerElem.classList.add("stickerSelect"); + stickerElem.append(sticker.getHTML()); + body.append(stickerElem); + stickerElem.addEventListener("click", () => { + res(sticker); + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + }); + } + }; + + select.addEventListener("click", clickEvent); + if (isFirst) { + clickEvent(); + isFirst = false; + } + }); + + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + document.body.append(menu); + Contextmenu.currentmenu = menu; + Contextmenu.keepOnScreen(menu); + menu.append(selection); + menu.append(body); + search.focus(); + return promise; + } } export {Sticker}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 053d968..18c33e3 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -393,6 +393,10 @@ textarea { mask: url(/icons/plainx.svg); mask-size: contain !important; } +.svg-sticker { + mask: url(/icons/sticker.svg); + mask-size: contain !important; +} .svg-search { mask: url(/icons/search.svg); mask-size: contain !important; @@ -535,6 +539,13 @@ textarea { bottom: -5px; border-radius: 1in; } +#stickerTB { + width: 0.2in; + height: 0.2in; + cursor: pointer; + flex-shrink: 0; + margin-left: 6px; +} #emojiTB { width: 0.2in; height: 0.2in; @@ -568,6 +579,7 @@ textarea { animation-name: fade-in; border: solid 0.03in var(--black); + z-index: 4; } @keyframes fade-in { from { @@ -2057,6 +2069,44 @@ img.bigembedimg { background: var(--card-bg); } } +.stickerSelect { + width: 1in; + height: 1in; + margin-right: 6px; + padding: 8px; + background: var(--secondary-bg); + border-radius: 6px; + cursor: pointer; + display: flex; +} +.stickerPicker { + position: absolute; + height: 440px; + width: 390px; + max-height: 100svh; + padding: 12px; + border-radius: 8px; + box-shadow: 0 0 8px var(--shadow); + gap: 8px; + box-sizing: border-box; + user-select: none; + background: var(--secondary-bg); + z-index: 4; + + input { + width: 1in; + position: absolute; + right: 8px; + top: 2px; + transition: width 0.2s; + background: var(--card-bg); + } + .sticker { + max-width: 1in; + max-height: 1in; + flex-grow: 1; + } +} .emojiHeading { height: 0.25in; } @@ -2067,6 +2117,7 @@ img.bigembedimg { flex: none; align-items: center; overflow-x: auto; + flex-grow: 0; } .emojiSelect { flex: none; @@ -2101,7 +2152,16 @@ img.bigembedimg { grid-auto-rows: min-content; overflow-y: auto; } - +.stickerBody { + flex: 1; + padding: 8px; + background: var(--card-bg); + border-radius: 8px; + display: flex; + overflow-y: auto; + flex-direction: row; + flex-wrap: wrap; +} /* Fullscreen and Modal (TEMP) */ .background { position: fixed; @@ -2888,3 +2948,6 @@ fieldset input[type="radio"] { height: 32px; border-radius: 2in; } +.stickerMArea { + padding-left: 48px; +}