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;
+}