diff --git a/src/equicordplugins/friendCloud/index.ts b/src/equicordplugins/friendCloud/index.ts
deleted file mode 100644
index 759b9f12f..000000000
--- a/src/equicordplugins/friendCloud/index.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2025 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
-import { EquicordDevs } from "@utils/constants";
-import { Logger } from "@utils/Logger";
-import definePlugin from "@utils/types";
-import { User } from "@vencord/discord-types";
-import { findStore } from "@webpack";
-import { DraftType, UploadHandler, UserStore } from "@webpack/common";
-
-const logger = new Logger("FriendCloud");
-
-interface AffinitiesV2 {
- otherUserId: User["id"];
- userSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU";
- otherUserSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU";
- isFriend: boolean;
- dmProbability: number;
- dmRank: number;
- vcProbability: number;
- vcRank: number;
- serverMessageProbability: number;
- serverMessageRank: number;
- communicationProbability: number;
- communicationRank: number;
-}
-
-interface UserPosition {
- member: User;
- affinity: number;
- x: number;
- y: number;
- size: number;
-}
-
-export default definePlugin({
- name: "FriendCloud",
- description: "Adds a /friendcloud command to visualize the users you most interact with",
- authors: [EquicordDevs.Fafa],
- commands: [
- {
- inputType: ApplicationCommandInputType.BUILT_IN,
- name: "friendcloud",
- description: "Display user you most interact with in a cloud",
- options: [
- {
- name: "count",
- description: "Number of users to display",
- type: ApplicationCommandOptionType.NUMBER,
- required: false
- }
- ],
- execute: async (opts, cmdCtx) => {
- const count = findOption(opts, "count", 25);
-
- if (!count) return sendBotMessage(cmdCtx.channel.id, { content: "The count must be 1 or higher!" });
-
- try {
- const affinities: AffinitiesV2[] = findStore("UserAffinitiesV2Store").getUserAffinities();
-
- if (!affinities?.length) {
- return sendBotMessage(cmdCtx.channel.id, {
- content: "No affinities found. Check your [privacy settings]()."
- });
- }
-
- const users = affinities
- .map(e => ({
- member: UserStore.getUser(e.otherUserId),
- affinity: calculateAffinityScore(e as AffinitiesV2)
- }))
- .filter(x => x.member?.id)
- .sort((a, b) => b.affinity - a.affinity)
- .slice(0, count);
-
- if (!users.length) {
- return sendBotMessage(cmdCtx.channel.id, {
- content: "No valid users found in affinities. Check your [privacy settings]()."
- });
- }
-
- const minAffinity = Math.min(...users.map(u => u.affinity));
- const maxAffinity = Math.max(...users.map(u => u.affinity));
- const minSize = 120;
- const maxSize = 240;
-
- const getSize = (affinity: number): number => {
- if (maxAffinity === minAffinity) return (minSize + maxSize) / 2;
- return minSize + ((affinity - minAffinity) / (maxAffinity - minAffinity)) * (maxSize - minSize);
- };
-
- const avgSize = (minSize + maxSize) / 2;
- const { width: canvasWidth, height: canvasHeight } = calculateCanvasSize(users.length, avgSize);
-
- const canvas = document.createElement("canvas");
- canvas.width = canvasWidth;
- canvas.height = canvasHeight;
- const ctx = canvas.getContext("2d")!;
-
- const positions: Array<{ x: number, y: number, size: number; }> = [];
- const userPositions = users.map(user => {
- const size = getSize(user.affinity);
- const pos = generatePoissonDiskPosition(positions, canvasWidth, canvasHeight, size);
- positions.push({ x: pos.x, y: pos.y, size });
- return { ...user, x: pos.x, y: pos.y, size };
- });
-
- let loadedImages = 0;
- const totalImages = userPositions.length;
-
- const drawImage = async (user: UserPosition) => {
- try {
- const avatarUrl = user.member?.avatar
- ? `https://cdn.discordapp.com/avatars/${user.member.id}/${user.member?.avatar}.webp?size=256`
- : `https://cdn.discordapp.com/embed/avatars/${user.member.id as any as number % 5}.png`;
-
- const img = await loadImage(avatarUrl);
- const centerX = user.x + user.size / 2;
- const centerY = user.y + user.size / 2;
-
- ctx.save();
- ctx.beginPath();
- ctx.arc(centerX, centerY, user.size / 2, 0, Math.PI * 2);
- ctx.clip();
- ctx.drawImage(img, user.x, user.y, user.size, user.size);
- ctx.restore();
-
- ctx.strokeStyle = "#808080";
- ctx.lineWidth = 3;
- ctx.beginPath();
- ctx.arc(centerX, centerY, user.size / 2 + 1, 0, Math.PI * 2);
- ctx.stroke();
- } catch {
- // we ignore
- } finally {
- loadedImages++;
- if (loadedImages === totalImages) {
- canvas.toBlob(blob => {
- if (!blob) {
- sendBotMessage(cmdCtx.channel.id, { content: "Couldn't generate the image :c" });
- return;
- }
- const file = new File([blob], "affinities-cloud.png", { type: "image/png" });
- UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage);
- }, "image/png");
- }
- }
- };
-
- userPositions.forEach(drawImage);
- } catch (e: unknown) {
- if (e instanceof Error)
- sendBotMessage(cmdCtx.channel.id, { content: e.message });
- else logger.error(e);
- }
- },
- },
- ]
-});
-
-function calculateAffinityScore(affinity: AffinitiesV2): number {
- const weights = {
- friend: 0.15,
- dm: 0.30,
- vc: 0.25,
- serverMsg: 0.20,
- communication: 0.10
- };
-
- let score = 0;
- if (affinity.isFriend) score += weights.friend * 100;
- score += affinity.dmProbability * weights.dm * 100;
- score += affinity.vcProbability * weights.vc * 100;
- score += affinity.serverMessageProbability * weights.serverMsg * 100;
- score += affinity.communicationProbability * weights.communication * 100;
-
- return Math.round(Math.min(100, Math.max(0, score)) * 100) / 100;
-}
-
-// stolen from petpet thanks vee
-function loadImage(source: File | string): Promise {
- const isFile = source instanceof File;
- const url = isFile ? URL.createObjectURL(source) : source;
-
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- if (isFile) URL.revokeObjectURL(url);
- resolve(img);
- };
- img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event);
- img.crossOrigin = "anonymous";
- img.src = url;
- });
-}
-
-function generatePoissonDiskPosition(
- existingPositions: Array<{ x: number, y: number, size: number; }>,
- canvasWidth: number,
- canvasHeight: number,
- size: number
-): { x: number, y: number; } {
- const edgePadding = 10;
- const minDist = size * 1.5;
- const textSpace = 60;
- const k = 30;
-
- function isValid(x: number, y: number) {
- if (
- x < edgePadding ||
- x + size > canvasWidth - edgePadding ||
- y < edgePadding ||
- y + size > canvasHeight - textSpace - edgePadding
- ) return false;
-
- return !existingPositions.some(pos => {
- const dx = pos.x - x;
- const dy = pos.y - y;
- const dist = Math.hypot(dx, dy);
- const minAllowed = (pos.size + size) / 2 + (minDist - size);
- return dist < minAllowed;
- });
- }
-
- if (existingPositions.length === 0) {
- return {
- x: canvasWidth / 2 - size / 2,
- y: canvasHeight / 2 - size / 2
- };
- }
-
- for (let tries = 0; tries < 100; tries++) {
- const base = existingPositions[Math.floor(Math.random() * existingPositions.length)];
- for (let i = 0; i < k; i++) {
- const angle = Math.random() * Math.PI * 2;
- const radius = minDist + Math.random() * minDist;
- const x = base.x + Math.cos(angle) * radius;
- const y = base.y + Math.sin(angle) * radius;
- if (isValid(x, y)) {
- return {
- x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)),
- y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding))
- };
- }
- }
- }
-
- for (let tries = 0; tries < 100; tries++) {
- const x = Math.random() * (canvasWidth - size - edgePadding * 2) + edgePadding;
- const y = Math.random() * (canvasHeight - size - textSpace - edgePadding * 2) + edgePadding;
- if (isValid(x, y)) {
- return {
- x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)),
- y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding))
- };
- }
- }
-
- return {
- x: edgePadding,
- y: edgePadding
- };
-}
-
-function calculateCanvasSize(userCount: number, avatarSize: number): { width: number, height: number; } {
- const padding = 50;
- const textSpace = 60;
- const itemWidth = avatarSize + padding;
- const itemHeight = avatarSize + textSpace + padding;
- const aspectRatio = 16 / 9;
- const cols = Math.ceil(Math.sqrt(userCount * aspectRatio));
- const rows = Math.ceil(userCount / cols);
-
- return {
- width: Math.max(1000, cols * itemWidth + padding),
- height: Math.max(700, rows * itemHeight + padding)
- };
-}
diff --git a/src/equicordplugins/gifRoulette/index.tsx b/src/equicordplugins/gifRoulette/index.tsx
deleted file mode 100644
index fdb527f04..000000000
--- a/src/equicordplugins/gifRoulette/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Vencord, a Discord client mod
- * Copyright (c) 2024 Vendicated and contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { definePluginSettings } from "@api/Settings";
-import { Devs } from "@utils/constants";
-import definePlugin, { OptionType } from "@utils/types";
-import { UserSettingsActionCreators } from "@webpack/common";
-
-function getMessage(opts, other) {
- const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue();
-
- const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs);
-
- const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)];
-
- let ownerPing = "";
-
- if (other.guild != null) {
- if (other.guild.ownerId != null && settings.store.pingOwnerChance && Math.random() <= 0.1) {
- ownerPing = `<@${other.guild.ownerId}>`;
- }
- }
-
- return `${chosenGifUrl} ${ownerPing}`;
-}
-
-const settings = definePluginSettings({
- pingOwnerChance: {
- type: OptionType.BOOLEAN,
- description: "If there should be a 1 in 10 change to ping the owner of the guild (oh no)",
- default: true
- }
-});
-
-export default definePlugin({
- name: "GifRoulette",
- description: "Adds a command to send a random gif from your favourites, and a one in ten chance to ping the owner of the server",
- authors: [Devs.Samwich],
- settings,
- commands: [
- {
- name: "gifroulette",
- description: "Tempt fate and send a gif",
- execute: (opts, other) => ({
- content: getMessage(opts, other)
- }),
- }
- ]
-});
diff --git a/src/equicordplugins/imgToGif/index.tsx b/src/equicordplugins/imgToGif/index.tsx
deleted file mode 100644
index fc4d81d52..000000000
--- a/src/equicordplugins/imgToGif/index.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-import { ApplicationCommandInputType, ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
-import { EquicordDevs } from "@utils/constants";
-import definePlugin from "@utils/types";
-import { CommandArgument, CommandContext } from "@vencord/discord-types";
-import { DraftType, UploadAttachmentStore, UploadHandler, UploadManager } from "@webpack/common";
-import { applyPalette, GIFEncoder, quantize } from "gifenc";
-
-const FRAMES = 1;
-
-function loadImage(source: File | string) {
- const isFile = source instanceof File;
- const url = isFile ? URL.createObjectURL(source) : source;
-
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- if (isFile) URL.revokeObjectURL(url);
- resolve(img);
- };
- img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event);
- img.crossOrigin = "Anonymous";
- img.src = url;
- });
-}
-
-async function resolveImage(options: CommandArgument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> {
- let image: File | null = null;
- let width: number | null = null;
- let height: number | null = null;
-
- for (const opt of options) {
- switch (opt.name) {
- case "image":
- const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
- if (upload) {
- if (!upload.isImage) {
- UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
- throw "Upload is not an image";
- }
- image = upload.item.file;
- }
- break;
- case "width":
- width = Number(opt.value);
- break;
- case "height":
- height = Number(opt.value);
- break;
- }
- }
-
- UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
- return { image, width, height };
-}
-
-export default definePlugin({
- name: "ImgToGif",
- description: "Adds a /imgtogif slash command to create a gif from any image",
- authors: [EquicordDevs.zyqunix],
- commands: [
- {
- inputType: ApplicationCommandInputType.BUILT_IN,
- name: "imgtogif",
- description: "Allows you to turn an image to a gif",
- options: [
- {
- name: "image",
- description: "Image attachment to use",
- type: ApplicationCommandOptionType.ATTACHMENT
- },
- {
- name: "width",
- description: "Width of the gif",
- type: ApplicationCommandOptionType.INTEGER
- },
- {
- name: "height",
- description: "Height of the gif",
- type: ApplicationCommandOptionType.INTEGER
- }
- ],
- execute: async (opts, cmdCtx) => {
- try {
- const { image, width, height } = await resolveImage(opts, cmdCtx);
- if (!image) throw "No Image specified!";
-
- const avatar = await loadImage(image);
-
- let gifWidth: number;
- let gifHeight: number;
-
- if (width && height) {
- gifWidth = width;
- gifHeight = height;
- } else if (width) {
- gifWidth = width;
- gifHeight = Math.round((avatar.height / avatar.width) * width);
- } else if (height) {
- gifHeight = height;
- gifWidth = Math.round((avatar.width / avatar.height) * height);
- } else {
- gifWidth = avatar.width;
- gifHeight = avatar.height;
- }
-
- const gif = GIFEncoder();
- const canvas = document.createElement("canvas");
- canvas.width = gifWidth;
- canvas.height = gifHeight;
- const ctx = canvas.getContext("2d")!;
-
- UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
-
- for (let i = 0; i < FRAMES; i++) {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height);
-
- const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const palette = quantize(data, 256);
- const index = applyPalette(data, palette);
-
- gif.writeFrame(index, canvas.width, canvas.height, {
- transparent: true,
- palette,
- });
- }
-
- gif.finish();
- const originalName = image.name ? image.name.replace(/\.[^/.]+$/, "") : "converted";
- const file = new File([new Uint8Array(gif.bytesView())], `${originalName}.gif`, { type: "image/gif" });
- setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
- } catch (err) {
- UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
- sendBotMessage(cmdCtx.channel.id, { content: String(err) });
- }
- },
- },
- ]
-});
diff --git a/src/equicordplugins/moreCommands/index.ts b/src/equicordplugins/moreCommands/index.ts
index 8e367cd0d..b174a4576 100644
--- a/src/equicordplugins/moreCommands/index.ts
+++ b/src/equicordplugins/moreCommands/index.ts
@@ -18,17 +18,55 @@
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
import { addMessagePreEditListener, addMessagePreSendListener, MessageObject, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
+import { migratePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import definePlugin from "@utils/types";
-import { UserStore } from "@webpack/common";
+import { DraftType, UploadHandler, UploadManager, UserAffinitiesStore, UserStore } from "@webpack/common";
+import { applyPalette, GIFEncoder, quantize } from "gifenc";
-import { fromMorse, getCuteAnimeBoys, getCuteNeko, getCutePats, isMorse, makeFreaky, mock, settings, toMorse, uwuify, uwuifyArray } from "./utils";
+import {
+ calculateAffinityScore,
+ calculateCanvasSize,
+ FRAMES,
+ fromMorse,
+ generatePoissonDiskPosition,
+ getCuteAnimeBoys,
+ getCuteNeko,
+ getCutePats,
+ getMessage,
+ isMorse,
+ loadFriendImage,
+ loadImage,
+ makeFreaky,
+ mock,
+ resolveImage,
+ settings,
+ toMorse,
+ uwuify,
+ uwuifyArray
+} from "./utils";
+
+migratePluginSettings("MoreCommands", "FriendCloud", "GifRoulette", "ImgToGif", "MoreKaomoji");
export default definePlugin({
name: "MoreCommands",
description: "Adds various fun and useful commands",
- authors: [Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, Devs.thororen, Devs.Korbo, Devs.nyx, Devs.amy],
+ authors: [
+ Devs.Arjix,
+ Devs.amy,
+ Devs.Samu,
+ EquicordDevs.zyqunix,
+ EquicordDevs.ShadyGoat,
+ Devs.thororen,
+ Devs.Korbo,
+ Devs.nyx,
+ Devs.amy,
+ Devs.Samwich,
+ EquicordDevs.Fafa,
+ Devs.JacobTm,
+ EquicordDevs.voidbbg
+ ],
settings,
commands: [
{
@@ -435,7 +473,394 @@ export default definePlugin({
content: uwuify(findOption(opts, "message", "")),
}),
},
+ {
+ name: "gifroulette",
+ description: "Tempt fate and send a gif",
+ execute: (opts, other) => ({
+ content: getMessage(opts, other)
+ }),
+ },
+ {
+ inputType: ApplicationCommandInputType.BUILT_IN,
+ name: "friendcloud",
+ description: "Display user you most interact with in a cloud",
+ options: [
+ {
+ name: "count",
+ description: "Number of users to display",
+ type: ApplicationCommandOptionType.NUMBER,
+ required: false
+ }
+ ],
+ execute: async (opts, cmdCtx) => {
+ const count = findOption(opts, "count", 25);
+
+ if (!count) return sendBotMessage(cmdCtx.channel.id, { content: "The count must be 1 or higher!" });
+
+ try {
+ const affinities = UserAffinitiesStore.getUserAffinities();
+
+ if (!affinities?.length) {
+ return sendBotMessage(cmdCtx.channel.id, {
+ content: "No affinities found. Check your [privacy settings]()."
+ });
+ }
+
+ const users = affinities
+ .map(e => ({
+ member: UserStore.getUser(e.otherUserId),
+ affinity: calculateAffinityScore(e)
+ }))
+ .filter(x => x.member?.id)
+ .sort((a, b) => b.affinity - a.affinity)
+ .slice(0, count);
+
+ if (!users.length) {
+ return sendBotMessage(cmdCtx.channel.id, {
+ content: "No valid users found in affinities. Check your [privacy settings]()."
+ });
+ }
+
+ const minAffinity = Math.min(...users.map(u => u.affinity));
+ const maxAffinity = Math.max(...users.map(u => u.affinity));
+ const minSize = 120;
+ const maxSize = 240;
+
+ const getSize = (affinity: number): number => {
+ if (maxAffinity === minAffinity) return (minSize + maxSize) / 2;
+ return minSize + ((affinity - minAffinity) / (maxAffinity - minAffinity)) * (maxSize - minSize);
+ };
+
+ const avgSize = (minSize + maxSize) / 2;
+ const { width: canvasWidth, height: canvasHeight } = calculateCanvasSize(users.length, avgSize);
+
+ const canvas = document.createElement("canvas");
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+ const ctx = canvas.getContext("2d")!;
+
+ const positions: Array<{ x: number, y: number, size: number; }> = [];
+ const userPositions = users.map(user => {
+ const size = getSize(user.affinity);
+ const pos = generatePoissonDiskPosition(positions, canvasWidth, canvasHeight, size);
+ positions.push({ x: pos.x, y: pos.y, size });
+ return { ...user, x: pos.x, y: pos.y, size };
+ });
+
+ let loadedImages = 0;
+ const totalImages = userPositions.length;
+
+ const drawImage = async user => {
+ try {
+ const avatarUrl = user.member?.avatar
+ ? `https://cdn.discordapp.com/avatars/${user.member.id}/${user.member?.avatar}.webp?size=256`
+ : `https://cdn.discordapp.com/embed/avatars/${user.member.id as any as number % 5}.png`;
+
+ const img = await loadFriendImage(avatarUrl);
+ const centerX = user.x + user.size / 2;
+ const centerY = user.y + user.size / 2;
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, user.size / 2, 0, Math.PI * 2);
+ ctx.clip();
+ ctx.drawImage(img, user.x, user.y, user.size, user.size);
+ ctx.restore();
+
+ ctx.strokeStyle = "#808080";
+ ctx.lineWidth = 3;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, user.size / 2 + 1, 0, Math.PI * 2);
+ ctx.stroke();
+ } catch {
+ // we ignore
+ } finally {
+ loadedImages++;
+ if (loadedImages === totalImages) {
+ canvas.toBlob(blob => {
+ if (!blob) {
+ sendBotMessage(cmdCtx.channel.id, { content: "Couldn't generate the image :c" });
+ return;
+ }
+ const file = new File([blob], "affinities-cloud.png", { type: "image/png" });
+ UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage);
+ }, "image/png");
+ }
+ }
+ };
+
+ userPositions.forEach(drawImage);
+ } catch (e: unknown) {
+ if (e instanceof Error) sendBotMessage(cmdCtx.channel.id, { content: e.message });
+ }
+ },
+ },
+ {
+ inputType: ApplicationCommandInputType.BUILT_IN,
+ name: "imgtogif",
+ description: "Allows you to turn an image to a gif",
+ options: [
+ {
+ name: "image",
+ description: "Image attachment to use",
+ type: ApplicationCommandOptionType.ATTACHMENT
+ },
+ {
+ name: "width",
+ description: "Width of the gif",
+ type: ApplicationCommandOptionType.INTEGER
+ },
+ {
+ name: "height",
+ description: "Height of the gif",
+ type: ApplicationCommandOptionType.INTEGER
+ }
+ ],
+ execute: async (opts, cmdCtx) => {
+ try {
+ const { image, width, height } = await resolveImage(opts, cmdCtx);
+ if (!image) throw "No Image specified!";
+
+ const avatar = await loadImage(image);
+
+ let gifWidth: number;
+ let gifHeight: number;
+
+ if (width && height) {
+ gifWidth = width;
+ gifHeight = height;
+ } else if (width) {
+ gifWidth = width;
+ gifHeight = Math.round((avatar.height / avatar.width) * width);
+ } else if (height) {
+ gifHeight = height;
+ gifWidth = Math.round((avatar.width / avatar.height) * height);
+ } else {
+ gifWidth = avatar.width;
+ gifHeight = avatar.height;
+ }
+
+ const gif = GIFEncoder();
+ const canvas = document.createElement("canvas");
+ canvas.width = gifWidth;
+ canvas.height = gifHeight;
+ const ctx = canvas.getContext("2d")!;
+
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+
+ for (let i = 0; i < FRAMES; i++) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height);
+
+ const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const palette = quantize(data, 256);
+ const index = applyPalette(data, palette);
+
+ gif.writeFrame(index, canvas.width, canvas.height, {
+ transparent: true,
+ palette,
+ });
+ }
+
+ gif.finish();
+ const originalName = image.name ? image.name.replace(/\.[^/.]+$/, "") : "converted";
+ const file = new File([new Uint8Array(gif.bytesView())], `${originalName}.gif`, { type: "image/gif" });
+ setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
+ } catch (err) {
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+ sendBotMessage(cmdCtx.channel.id, { content: String(err) });
+ }
+ },
+ },
+ {
+ name: "dissatisfaction",
+ description: " >﹏<",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + " >﹏<",
+ }),
+ },
+ {
+ name: "smug",
+ description: "ಠ_ಠ",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ಠ_ಠ",
+ }),
+ },
+ {
+ name: "happy",
+ description: "ヽ(´▽`)/",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ヽ(´▽`)/",
+ }),
+ },
+ {
+ name: "crying",
+ description: "ಥ_ಥ",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ಥ_ಥ",
+ }),
+ },
+ {
+ name: "angry",
+ description: "ヽ(`Д´)ノ",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ヽ(`Д´)ノ",
+ }),
+ },
+ {
+ name: "anger",
+ description: "ヽ(o`皿′o)ノ",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ヽ(o`皿′o)ノ",
+ }),
+ },
+ {
+ name: "joy",
+ description: "<( ̄︶ ̄)>",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "<( ̄︶ ̄)>",
+ }),
+ },
+ {
+ name: "blush",
+ description: "૮ ˶ᵔ ᵕ ᵔ˶ ა",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "૮ ˶ᵔ ᵕ ᵔ˶ ა",
+ }),
+ },
+ {
+ name: "confused",
+ description: "(•ิ_•ิ)?",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(•ิ_•ิ)?",
+ }),
+ },
+ {
+ name: "sleeping",
+ description: "(ᴗ_ᴗ)",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(ᴗ_ᴗ)",
+ }),
+ },
+ {
+ name: "laughing",
+ description: "o(≧▽≦)o",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "o(≧▽≦)o",
+ }),
+ },
+ /*
+ even more kaomoji
+ */
+ {
+ name: "giving",
+ description: "(ノ◕ヮ◕)ノ*:・゚✧",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(ノ◕ヮ◕)ノ*:・゚✧",
+ }),
+ },
+ {
+ name: "peace",
+ description: "✌(◕‿-)✌",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "✌(◕‿-)✌",
+ }),
+ },
+ {
+ name: "ending1",
+ description: "Ꮺ ָ࣪ ۰ ͙⊹",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "Ꮺ ָ࣪ ۰ ͙⊹",
+ }),
+ },
+ {
+ name: "uwu",
+ description: "(>⩊<)",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(>⩊<)",
+ }),
+ },
+ {
+ name: "comfy",
+ description: "(─‿‿─)♡",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(─‿‿─)♡",
+ }),
+ },
+ {
+ name: "lovehappy",
+ description: "(*≧ω≦*)",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(*≧ω≦*)",
+ }),
+ },
+ {
+ name: "loveee",
+ description: "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)",
+ }),
+ },
+ {
+ name: "give",
+ description: "(ノ= ⩊ = )ノ",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "(ノ= ⩊ = )ノ",
+ }),
+ },
+ {
+ name: "lovegive",
+ description: "ღゝ◡╹)ノ♡",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "ღゝ◡╹)ノ♡",
+ }),
+ },
+ {
+ name: "music",
+ description: "( ̄▽ ̄)/♫•¨•.¸¸♪",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "( ̄▽ ̄)/♫•¨•.¸¸♪",
+ }),
+ },
+ {
+ name: "stars",
+ description: ".𖥔 ݁ ˖๋ ࣭ ⭑",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + ".𖥔 ݁ ˖๋ ࣭ ⭑",
+ }),
+ },
+ {
+ name: "lovegiving",
+ description: "⸜(。˃ ᵕ ˂ )⸝♡",
+ options: [OptionalMessageOption],
+ execute: opts => ({
+ content: findOption(opts, "message", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡",
+ }),
+ }
],
+
patches: [
{
find: ".isPureReactComponent=!0;",
diff --git a/src/equicordplugins/moreCommands/utils.ts b/src/equicordplugins/moreCommands/utils.ts
index a5a0f7815..0dd7ae3a1 100644
--- a/src/equicordplugins/moreCommands/utils.ts
+++ b/src/equicordplugins/moreCommands/utils.ts
@@ -6,6 +6,8 @@
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
+import { CommandArgument, CommandContext } from "@vencord/discord-types";
+import { DraftType, UploadAttachmentStore, UploadManager, UserSettingsActionCreators } from "@webpack/common";
export const settings = definePluginSettings({
addFreakyEnding: {
@@ -207,3 +209,180 @@ export function uwuifyArray(arr) {
return newArr;
}
+
+export function getMessage(opts, other) {
+ const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue();
+
+ const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs);
+
+ const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)];
+
+ return `${chosenGifUrl}`;
+}
+
+export function calculateAffinityScore(affinity): number {
+ const weights = {
+ friend: 0.15,
+ dm: 0.30,
+ vc: 0.25,
+ serverMsg: 0.20,
+ communication: 0.10
+ };
+
+ let score = 0;
+ if (affinity.isFriend) score += weights.friend * 100;
+ score += affinity.dmProbability * weights.dm * 100;
+ score += affinity.vcProbability * weights.vc * 100;
+ score += affinity.serverMessageProbability * weights.serverMsg * 100;
+ score += affinity.communicationProbability * weights.communication * 100;
+
+ return Math.round(Math.min(100, Math.max(0, score)) * 100) / 100;
+}
+
+// stolen from petpet thanks vee
+export function loadFriendImage(source: File | string): Promise {
+ const isFile = source instanceof File;
+ const url = isFile ? URL.createObjectURL(source) : source;
+
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ if (isFile) URL.revokeObjectURL(url);
+ resolve(img);
+ };
+ img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event);
+ img.crossOrigin = "anonymous";
+ img.src = url;
+ });
+}
+
+export function generatePoissonDiskPosition(
+ existingPositions: Array<{ x: number, y: number, size: number; }>,
+ canvasWidth: number,
+ canvasHeight: number,
+ size: number
+): { x: number, y: number; } {
+ const edgePadding = 10;
+ const minDist = size * 1.5;
+ const textSpace = 60;
+ const k = 30;
+
+ function isValid(x: number, y: number) {
+ if (
+ x < edgePadding ||
+ x + size > canvasWidth - edgePadding ||
+ y < edgePadding ||
+ y + size > canvasHeight - textSpace - edgePadding
+ ) return false;
+
+ return !existingPositions.some(pos => {
+ const dx = pos.x - x;
+ const dy = pos.y - y;
+ const dist = Math.hypot(dx, dy);
+ const minAllowed = (pos.size + size) / 2 + (minDist - size);
+ return dist < minAllowed;
+ });
+ }
+
+ if (existingPositions.length === 0) {
+ return {
+ x: canvasWidth / 2 - size / 2,
+ y: canvasHeight / 2 - size / 2
+ };
+ }
+
+ for (let tries = 0; tries < 100; tries++) {
+ const base = existingPositions[Math.floor(Math.random() * existingPositions.length)];
+ for (let i = 0; i < k; i++) {
+ const angle = Math.random() * Math.PI * 2;
+ const radius = minDist + Math.random() * minDist;
+ const x = base.x + Math.cos(angle) * radius;
+ const y = base.y + Math.sin(angle) * radius;
+ if (isValid(x, y)) {
+ return {
+ x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)),
+ y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding))
+ };
+ }
+ }
+ }
+
+ for (let tries = 0; tries < 100; tries++) {
+ const x = Math.random() * (canvasWidth - size - edgePadding * 2) + edgePadding;
+ const y = Math.random() * (canvasHeight - size - textSpace - edgePadding * 2) + edgePadding;
+ if (isValid(x, y)) {
+ return {
+ x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)),
+ y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding))
+ };
+ }
+ }
+
+ return {
+ x: edgePadding,
+ y: edgePadding
+ };
+}
+
+export function calculateCanvasSize(userCount: number, avatarSize: number): { width: number, height: number; } {
+ const padding = 50;
+ const textSpace = 60;
+ const itemWidth = avatarSize + padding;
+ const itemHeight = avatarSize + textSpace + padding;
+ const aspectRatio = 16 / 9;
+ const cols = Math.ceil(Math.sqrt(userCount * aspectRatio));
+ const rows = Math.ceil(userCount / cols);
+
+ return {
+ width: Math.max(1000, cols * itemWidth + padding),
+ height: Math.max(700, rows * itemHeight + padding)
+ };
+}
+
+export const FRAMES = 1;
+
+export function loadImage(source: File | string) {
+ const isFile = source instanceof File;
+ const url = isFile ? URL.createObjectURL(source) : source;
+
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ if (isFile) URL.revokeObjectURL(url);
+ resolve(img);
+ };
+ img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event);
+ img.crossOrigin = "Anonymous";
+ img.src = url;
+ });
+}
+
+export async function resolveImage(options: CommandArgument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> {
+ let image: File | null = null;
+ let width: number | null = null;
+ let height: number | null = null;
+
+ for (const opt of options) {
+ switch (opt.name) {
+ case "image":
+ const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
+ if (upload) {
+ if (!upload.isImage) {
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ throw "Upload is not an image";
+ }
+ image = upload.item.file;
+ }
+ break;
+ case "width":
+ width = Number(opt.value);
+ break;
+ case "height":
+ height = Number(opt.value);
+ break;
+ }
+ }
+
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ return { image, width, height };
+}
diff --git a/src/equicordplugins/moreKaomoji/index.ts b/src/equicordplugins/moreKaomoji/index.ts
deleted file mode 100644
index 986b1f332..000000000
--- a/src/equicordplugins/moreKaomoji/index.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
-*/
-
-import { findOption, OptionalMessageOption } from "@api/Commands";
-import { Devs, EquicordDevs } from "@utils/constants";
-import definePlugin from "@utils/types";
-
-export default definePlugin({
- name: "MoreKaomoji",
- description: "Adds more Kaomoji to discord. ヽ(´▽`)/",
- authors: [Devs.JacobTm, EquicordDevs.voidbbg],
- commands: [
- {
- name: "dissatisfaction",
- description: " >﹏<",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + " >﹏<",
- }),
- },
- {
- name: "smug",
- description: "ಠ_ಠ",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ಠ_ಠ",
- }),
- },
- {
- name: "happy",
- description: "ヽ(´▽`)/",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ヽ(´▽`)/",
- }),
- },
- {
- name: "crying",
- description: "ಥ_ಥ",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ಥ_ಥ",
- }),
- },
- {
- name: "angry",
- description: "ヽ(`Д´)ノ",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ヽ(`Д´)ノ",
- }),
- },
- {
- name: "anger",
- description: "ヽ(o`皿′o)ノ",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ヽ(o`皿′o)ノ",
- }),
- },
- {
- name: "joy",
- description: "<( ̄︶ ̄)>",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "<( ̄︶ ̄)>",
- }),
- },
- {
- name: "blush",
- description: "૮ ˶ᵔ ᵕ ᵔ˶ ა",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "૮ ˶ᵔ ᵕ ᵔ˶ ა",
- }),
- },
- {
- name: "confused",
- description: "(•ิ_•ิ)?",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(•ิ_•ิ)?",
- }),
- },
- {
- name: "sleeping",
- description: "(ᴗ_ᴗ)",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(ᴗ_ᴗ)",
- }),
- },
- {
- name: "laughing",
- description: "o(≧▽≦)o",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "o(≧▽≦)o",
- }),
- },
- /*
- even more kaomoji
- */
- {
- name: "giving",
- description: "(ノ◕ヮ◕)ノ*:・゚✧",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(ノ◕ヮ◕)ノ*:・゚✧",
- }),
- },
- {
- name: "peace",
- description: "✌(◕‿-)✌",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "✌(◕‿-)✌",
- }),
- },
- {
- name: "ending1",
- description: "Ꮺ ָ࣪ ۰ ͙⊹",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "Ꮺ ָ࣪ ۰ ͙⊹",
- }),
- },
- {
- name: "uwu",
- description: "(>⩊<)",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(>⩊<)",
- }),
- },
- {
- name: "comfy",
- description: "(─‿‿─)♡",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(─‿‿─)♡",
- }),
- },
- {
- name: "lovehappy",
- description: "(*≧ω≦*)",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(*≧ω≦*)",
- }),
- },
- {
- name: "loveee",
- description: "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)",
- }),
- },
- {
- name: "give",
- description: "(ノ= ⩊ = )ノ",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "(ノ= ⩊ = )ノ",
- }),
- },
- {
- name: "lovegive",
- description: "ღゝ◡╹)ノ♡",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "ღゝ◡╹)ノ♡",
- }),
- },
- {
- name: "music",
- description: "( ̄▽ ̄)/♫•¨•.¸¸♪",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "( ̄▽ ̄)/♫•¨•.¸¸♪",
- }),
- },
- {
- name: "stars",
- description: ".𖥔 ݁ ˖๋ ࣭ ⭑",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + ".𖥔 ݁ ˖๋ ࣭ ⭑",
- }),
- },
- {
- name: "lovegiving",
- description: "⸜(。˃ ᵕ ˂ )⸝♡",
- options: [OptionalMessageOption],
- execute: opts => ({
- content: findOption(opts, "message", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡",
- }),
- }
- ]
-});
diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts
index 9c681386c..e5a547dbc 100644
--- a/src/plugins/implicitRelationships/index.ts
+++ b/src/plugins/implicitRelationships/index.ts
@@ -20,12 +20,9 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
-import { findStoreLazy } from "@webpack";
-import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
+import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserAffinitiesStore, UserStore } from "@webpack/common";
import { Settings } from "Vencord";
-const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store");
-
export default definePlugin({
name: "ImplicitRelationships",
description: "Shows your implicit relationships in the Friends tab.",
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index 175ab75de..1217eedbd 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -84,6 +84,7 @@ export let EditMessageStore: t.EditMessageStore;
export let QuestStore: t.QuestStore;
export let PendingReplyStore: t.PendingReplyStore;
export let ExperimentStore: GenericStore;
+export let UserAffinitiesStore: GenericStore;
/**
* @see jsdoc of {@link t.useStateFromStores}
@@ -140,6 +141,7 @@ waitForStore("EditMessageStore", m => EditMessageStore = m);
waitForStore("PendingReplyStore", m => PendingReplyStore = m);
waitForStore("ExperimentStore", m => ExperimentStore = m);
waitForStore("QuestStore", m => QuestStore = m);
+waitForStore("UserAffinitiesV2Store", m => UserAffinitiesStore = m);
waitForStore("ThemeStore", m => {
ThemeStore = m;
// Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.