create guild settings for stickers

This commit is contained in:
MathMan05 2025-04-11 12:11:27 -05:00
parent a69b8c552f
commit 956016a9a0
8 changed files with 363 additions and 3 deletions

View file

@ -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,

View file

@ -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<Member>();
static contextmenu = new Contextmenu<Guild, undefined>("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];

View file

@ -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,
};

View file

@ -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);

View file

@ -1,4 +1,6 @@
import {Emoji} from "./emoji.js";
import {I18n} from "./i18n.js";
import {Localuser} from "./localuser.js";
interface OptionsElement<x> {
//
@ -521,6 +523,66 @@ class MDInput implements OptionsElement<string> {
this.onSubmit(this.value);
}
}
class EmojiInput implements OptionsElement<Emoji | undefined> {
readonly label: string;
readonly owner: Options;
readonly onSubmit: (str: Emoji | undefined) => void;
input!: WeakRef<HTMLInputElement>;
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<FileList | null> {
readonly label: string;
readonly owner: Options;
@ -654,6 +716,7 @@ class Dialog {
background.remove();
}
}
export {Dialog};
class Options implements OptionsElement<void> {
name: string;
@ -744,6 +807,19 @@ class Options implements OptionsElement<void> {
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<object> {
}
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<object> {
} 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;
}

36
src/webpage/sticker.ts Normal file
View file

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

View file

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

View file

@ -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?",