From 785bf059e9c7d0eee6f365f1b402736c3ed5491d Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:50:10 +0100 Subject: [PATCH 01/19] feat: reset logs on client restart for MLE (#968) * feat: read the branch name * how did i miss this lmfao --- .../components/LogsModal.tsx | 6 +++--- src/equicordplugins/messageLoggerEnhanced/db.ts | 15 ++++++++------- .../messageLoggerEnhanced/index.tsx | 16 ++++++++++++++++ .../messageLoggerEnhanced/settings.tsx | 12 +++++++++--- .../messageLoggerEnhanced/utils/LimitedMap.ts | 4 ++++ .../messageLoggerEnhanced/utils/settingsUtils.ts | 6 +++--- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx index 06727dba8..1d28a6348 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx @@ -16,8 +16,8 @@ import { type User } from "@vencord/discord-types"; import { find, findByCode, findByCodeLazy } from "@webpack"; import { Alerts, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, NavigationRouter, React, TabBar, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common"; -import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "../db"; -import { cl, settings } from "../index"; +import { DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "../db"; +import { clearLogs, cl, settings } from "../index"; import { LoggedMessage, LoggedMessageJSON } from "../types"; import { messageJsonToMessageClass } from "../utils"; import { importLogs } from "../utils/settingsUtils"; @@ -147,7 +147,7 @@ export function LogsModal({ modalProps, initalQuery }: Props) { confirmVariant: "critical-primary", cancelText: "Cancel", onConfirm: async () => { - await clearMessagesIDB(); + await clearLogs(); reset(); } diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 31c34ef45..4fae28348 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -304,14 +304,15 @@ export async function deleteMessagesBulkIDB(message_ids: string[]) { }); } -export async function clearMessagesIDB() { +export async function clearMessagesIDB(showToast = true) { cachedMessages.clear(); + await withDb(void 0, database => database.clear("messages")); - return withDb(void 0, database => database.clear("messages")).then(() => { - Toasts.show({ - type: Toasts.Type.MESSAGE, - message: "Cleared message log database and cache.", - id: Toasts.genId() - }); + if (!showToast) return; + + Toasts.show({ + type: Toasts.Type.MESSAGE, + message: "Cleared message log database and cache.", + id: Toasts.genId() }); } diff --git a/src/equicordplugins/messageLoggerEnhanced/index.tsx b/src/equicordplugins/messageLoggerEnhanced/index.tsx index cf5a537f8..e8b3cdab9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/index.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/index.tsx @@ -36,8 +36,15 @@ export const Flogger = new Logger("MessageLoggerEnhanced", "#f26c6c"); export const cacheSentMessages = new LimitedMap(); export const cl = classNameFactory("vc-msg-logger-enhanced-"); +let didClearLogsOnStartup = false; + const cacheThing = findByPropsLazy("commit", "getOrCreate"); +export async function clearLogs(showToast = true) { + await idb.clearMessagesIDB(showToast); + cacheSentMessages.clear(); +} + let oldGetMessage: typeof MessageStore.getMessage; const handledMessageIds = new Set(); @@ -396,6 +403,15 @@ export default definePlugin({ Native.init(); + if (settings.store.clearLogsOnRestart && !didClearLogsOnStartup) { + try { + await clearLogs(false); + didClearLogsOnStartup = true; + } catch (e) { + Flogger.error("Failed to clear logs on restart", e); + } + } + const { imageCacheDir, logsDir } = await Native.getSettings(); settings.store.imageCacheDir = imageCacheDir; settings.store.logsDir = logsDir; diff --git a/src/equicordplugins/messageLoggerEnhanced/settings.tsx b/src/equicordplugins/messageLoggerEnhanced/settings.tsx index c573acff8..fce7de8b8 100644 --- a/src/equicordplugins/messageLoggerEnhanced/settings.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/settings.tsx @@ -11,10 +11,9 @@ import { OptionType } from "@utils/types"; import { Alerts, useState } from "@webpack/common"; import { Settings } from "Vencord"; -import { Native } from "."; +import { clearLogs, Native } from "."; import { ImageCacheDir, LogsDir } from "./components/FolderSelectInput"; import { openLogModal } from "./components/LogsModal"; -import { clearMessagesIDB } from "./db"; import { blockedExts } from "./list"; import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants"; import { exportLogs, importLogs } from "./utils/settingsUtils"; @@ -260,6 +259,13 @@ export const settings = definePluginSettings({ component: ExportLogsButton }, + clearLogsOnRestart: { + type: OptionType.BOOLEAN, + description: "Clear logs when Discord restarts.", + default: false, + restartNeeded: true, + }, + openLogs: { type: OptionType.COMPONENT, description: "Open Logs", @@ -298,7 +304,7 @@ export const settings = definePluginSettings({ confirmText: "Clear", cancelText: "Cancel", onConfirm: async () => { - await clearMessagesIDB(); + await clearLogs(); }, })} > diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts b/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts index a0b381e75..c027d3869 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts @@ -33,4 +33,8 @@ export class LimitedMap { get(key: K) { return this.map.get(key); } + + clear() { + this.map.clear(); + } } diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts index 1f89770c5..64c6d9d03 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts @@ -21,8 +21,8 @@ import { chooseFile as chooseFileWeb } from "@utils/web"; import { Toasts } from "@webpack/common"; import { showSaveFilePicker } from "native-file-system-adapter"; -import { Native } from ".."; -import { addMessagesBulkIDB, clearMessagesIDB, iterateAllMessagesIDB } from "../db"; +import { Native, clearLogs } from ".."; +import { addMessagesBulkIDB, iterateAllMessagesIDB } from "../db"; import { LoggedMessageJSON } from "../types"; export async function importLogs() { @@ -34,7 +34,7 @@ export async function importLogs() { for await (const logItems of iterateLogItems()) { if (!cleared) { - await clearMessagesIDB(); + await clearLogs(false); cleared = true; } From 61341888d825a195afdecedf0b5c4245b72aaf92 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 11 Apr 2026 22:55:17 -0400 Subject: [PATCH 02/19] remake messageTranslate, fix gifcollections bug (#967) * remake message translations plugin, fix only show gifs for gifcollections * Update index.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/gifCollections/index.tsx | 4 +- .../messageTranslate/index.tsx | 133 ++++++++++++------ .../messageTranslate/settings.ts | 72 ++++++++++ .../messageTranslate/styles.css | 9 ++ src/equicordplugins/messageTranslate/types.ts | 21 +++ .../messageTranslate/utils/translate.ts | 81 +++++++++++ 6 files changed, 275 insertions(+), 45 deletions(-) create mode 100644 src/equicordplugins/messageTranslate/settings.ts create mode 100644 src/equicordplugins/messageTranslate/styles.css create mode 100644 src/equicordplugins/messageTranslate/types.ts create mode 100644 src/equicordplugins/messageTranslate/utils/translate.ts diff --git a/src/equicordplugins/gifCollections/index.tsx b/src/equicordplugins/gifCollections/index.tsx index 54fb969a0..8bfc180d7 100644 --- a/src/equicordplugins/gifCollections/index.tsx +++ b/src/equicordplugins/gifCollections/index.tsx @@ -6,7 +6,7 @@ import "./style.css"; -import { Devs, EquicordDevs } from "@utils/constants"; +import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; @@ -157,7 +157,7 @@ export default definePlugin({ oldTrendingCat = instance.props.trendingCategories; } if (settings.store.onlyShowCollections) { - instance.props.trendingCategories = [...this.sortedCollections(), ...instance.props.favorites]; + instance.props.trendingCategories = this.sortedCollections(); } else if (oldTrendingCat != null) { instance.props.trendingCategories = [...this.sortedCollections(), ...oldTrendingCat]; } diff --git a/src/equicordplugins/messageTranslate/index.tsx b/src/equicordplugins/messageTranslate/index.tsx index 6384b6ee9..68adbbd0b 100644 --- a/src/equicordplugins/messageTranslate/index.tsx +++ b/src/equicordplugins/messageTranslate/index.tsx @@ -4,64 +4,111 @@ * 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 "./styles.css"; -const settings = definePluginSettings( - { - targetLanguage: { - type: OptionType.STRING, - description: "The language messages should be translated to", - default: "en", - restartNeeded: true - }, - confidenceRequirement: { - type: OptionType.STRING, - description: "The confidence required to translated the message. Best not to edit unless you know what you're doing", - default: "0.8", - restartNeeded: true - }, - }); +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import definePlugin from "@utils/types"; +import { ChannelStore, FluxDispatcher, MessageStore, UserStore } from "@webpack/common"; -async function translateAPI(sourceLang: string, targetLang: string, text: string): Promise { +import { getIgnoredChannels, getIgnoredGuilds, getIgnoredUsers, settings } from "./settings"; +import { MessageWithContent } from "./types"; +import { clearCache, getCached, hasFailed, isInProgress, translate } from "./utils/translate"; - const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dj=1&q=${encodeURIComponent(text)}`; - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to translate "${text}" from ${sourceLang} to ${targetLang}: ${response.status} ${response.statusText}`); +const cl = classNameFactory("mt-"); +const translatedMessages = new Map(); + +function shouldTranslate(message: MessageWithContent): boolean { + if (!message.content || typeof message.content !== "string") return false; + if (hasFailed(message.id, message.content)) return false; + + if (settings.store.skipOwnMessages) { + const currentUserId = UserStore.getCurrentUser()?.id; + if (currentUserId && message.author?.id === currentUserId) return false; } - return await response.json(); + if (settings.store.skipBotMessages && message.author?.bot) return false; + + if (message.author && getIgnoredUsers().has(message.author.id)) return false; + if (message.channel_id && getIgnoredChannels().has(message.channel_id)) return false; + + const guildId = message.channel_id ? ChannelStore.getChannel(message.channel_id)?.guild_id : null; + if (guildId && getIgnoredGuilds().has(guildId)) return false; + + return true; } -async function TranslateMessage(string) { - // there may be a better way to do this lmao - if (string.includes("(Translated)")) return string; +function triggerReRender(message: MessageWithContent) { + const current = MessageStore.getMessage(message.channel_id, message.id); + if (!current) return; - const response = await translateAPI("auto", settings.store.targetLanguage, string); - - if (response.src === settings.store.targetLanguage || response.confidence < settings.store.confidenceRequirement) return string; - - const { sentences }: { sentences: { trans?: string; }[]; } = await response; - const translatedText = sentences.map(s => s?.trans).filter(Boolean).join(""); - - return `${translatedText} *(Translated)*`; + FluxDispatcher.dispatch({ + type: "MESSAGE_UPDATE", + message: current, + }); } export default definePlugin({ name: "MessageTranslate", - description: "Auto translate messages to your language", - authors: [Devs.Samwich], + description: "Auto translate messages to your language with caching, per-channel toggles, and more options.", + authors: [EquicordDevs.creations], settings, - TranslateMessage: TranslateMessage, + patches: [ { find: '.CUSTOM_GIFT?""', - replacement: { - match: /renderContentOnly:\i}=\i;/, - replace: "$&$self.TranslateMessage(arguments[0].message.content).then(response => arguments[0].message.content = response);" - } + replacement: [ + { + match: /message:(\i),message:\{id:\i\}.{0,200}renderContentOnly:\i\}=\i;/, + replace: "$&$1=$self.transformMessage($1);", + }, + { + match: /childrenMessageContent:(\i),/g, + replace: "childrenMessageContent:$self.wrapContent($1,arguments[0].message.id),", + }, + ], }, - ] + ], + + transformMessage(message: MessageWithContent): MessageWithContent { + if (!settings.store.autoTranslate || !shouldTranslate(message)) { + translatedMessages.delete(message.id); + return message; + } + + const cached = getCached(message.id); + if (cached) { + if (cached.original !== message.content) { + clearCache(message.id); + translatedMessages.delete(message.id); + return message; + } + translatedMessages.set(message.id, cached.sourceLang); + return Object.assign(Object.create(Object.getPrototypeOf(message)), message, { + content: cached.translated, + }) as MessageWithContent; + } + + translatedMessages.delete(message.id); + if (!isInProgress(message.id)) { + translate(message.id, message.content).then(result => { + if (result) triggerReRender(message); + }); + } + + return message; + }, + + wrapContent(content: any, messageId: string) { + const sourceLang = translatedMessages.get(messageId); + if (!sourceLang) return content; + return ( + <> + {content} + {settings.store.showIndicator && ( +
translated from {sourceLang}
+ )} + + ); + }, }); diff --git a/src/equicordplugins/messageTranslate/settings.ts b/src/equicordplugins/messageTranslate/settings.ts new file mode 100644 index 000000000..491240737 --- /dev/null +++ b/src/equicordplugins/messageTranslate/settings.ts @@ -0,0 +1,72 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +export const settings = definePluginSettings({ + targetLanguage: { + type: OptionType.STRING, + description: "Target language code for translations (e.g. en, es, fr, de, ja).", + default: "en", + }, + confidenceRequirement: { + type: OptionType.NUMBER, + description: "Minimum confidence (0 to 1) required to show a translation.", + default: 0.8, + }, + autoTranslate: { + type: OptionType.BOOLEAN, + description: "Automatically translate messages as they appear.", + default: true, + }, + skipOwnMessages: { + type: OptionType.BOOLEAN, + description: "Do not translate your own messages.", + default: true, + }, + skipBotMessages: { + type: OptionType.BOOLEAN, + description: "Do not translate bot messages.", + default: false, + }, + ignoredGuilds: { + type: OptionType.STRING, + description: "Comma-separated list of server IDs to not translate in.", + default: "", + }, + ignoredChannels: { + type: OptionType.STRING, + description: "Comma-separated list of channel IDs to not translate in.", + default: "", + }, + ignoredUsers: { + type: OptionType.STRING, + description: "Comma-separated list of user IDs to not translate.", + default: "", + }, + showIndicator: { + type: OptionType.BOOLEAN, + description: "Append a small (translated) indicator to translated messages.", + default: true, + }, +}); + +function parseIdList(value: string): Set { + return new Set(value.split(",").map(s => s.trim()).filter(Boolean)); +} + +export function getIgnoredGuilds(): Set { + return parseIdList(settings.store.ignoredGuilds); +} + +export function getIgnoredChannels(): Set { + return parseIdList(settings.store.ignoredChannels); +} + +export function getIgnoredUsers(): Set { + return parseIdList(settings.store.ignoredUsers); +} diff --git a/src/equicordplugins/messageTranslate/styles.css b/src/equicordplugins/messageTranslate/styles.css new file mode 100644 index 000000000..14cd820e6 --- /dev/null +++ b/src/equicordplugins/messageTranslate/styles.css @@ -0,0 +1,9 @@ +.mt-indicator { + color: var(--text-muted); + font-style: italic; + font-weight: 400; + font-size: 0.7rem; + line-height: 1; + margin-top: 0.15em; + opacity: 0.75; +} diff --git a/src/equicordplugins/messageTranslate/types.ts b/src/equicordplugins/messageTranslate/types.ts new file mode 100644 index 000000000..d6cb8f1f5 --- /dev/null +++ b/src/equicordplugins/messageTranslate/types.ts @@ -0,0 +1,21 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { Message } from "@vencord/discord-types"; + +export interface TranslateResponse { + src: string; + confidence: number; + sentences: { trans?: string; }[]; +} + +export interface CachedTranslation { + original: string; + translated: string; + sourceLang: string; +} + +export type MessageWithContent = Message & { content: string; }; diff --git a/src/equicordplugins/messageTranslate/utils/translate.ts b/src/equicordplugins/messageTranslate/utils/translate.ts new file mode 100644 index 000000000..612b26c1a --- /dev/null +++ b/src/equicordplugins/messageTranslate/utils/translate.ts @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; + +import { settings } from "../settings"; +import { CachedTranslation, TranslateResponse } from "../types"; + +const logger = new Logger("MessageTranslate"); + +const translationCache = new Map(); +const inProgress = new Set(); +const failed = new Map(); + +export function getCached(messageId: string): CachedTranslation | undefined { + return translationCache.get(messageId); +} + +export function hasFailed(messageId: string, text: string): boolean { + return failed.get(messageId) === text; +} + +export function isInProgress(messageId: string): boolean { + return inProgress.has(messageId); +} + +export function clearCache(messageId: string) { + translationCache.delete(messageId); + failed.delete(messageId); +} + +async function fetchTranslation(text: string, targetLang: string): Promise { + const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&dj=1&q=${encodeURIComponent(text)}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Translation API returned ${response.status} ${response.statusText}`); + } + + return await response.json(); +} + +export async function translate(messageId: string, text: string): Promise { + if (inProgress.has(messageId)) return null; + if (translationCache.has(messageId)) return translationCache.get(messageId)!; + + inProgress.add(messageId); + + try { + const targetLang = settings.store.targetLanguage; + const response = await fetchTranslation(text, targetLang); + + if (response.src === targetLang || response.confidence < settings.store.confidenceRequirement) { + failed.set(messageId, text); + return null; + } + + const translatedText = response.sentences.map(s => s.trans).filter(Boolean).join(""); + if (!translatedText || translatedText === text) { + failed.set(messageId, text); + return null; + } + + const entry: CachedTranslation = { + original: text, + translated: translatedText, + sourceLang: response.src, + }; + translationCache.set(messageId, entry); + return entry; + } catch (e) { + logger.error("Translation failed", e); + failed.set(messageId, text); + return null; + } finally { + inProgress.delete(messageId); + } +} From ad51504d6dd5a948d0b59327572e219f34c644ae Mon Sep 17 00:00:00 2001 From: morsz <143611535+morsznetik@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:30:47 +0200 Subject: [PATCH 03/19] add custom status timeouts (#969) * add custom status timeouts patch the native status menu so idle, dnd, and invisible can use configurable timeout presets without replacing discord's ui add a settings driven preset editor with reorder controls, movable forever support, and per status direct click defaults while keeping the preset list as the single source of truth and using discord's native timed status action Signed-off-by: morsz * clean up custom status timeouts * framework * Update index.tsx --------- Signed-off-by: morsz Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: creations --- .../customStatusTimeouts/index.tsx | 115 ++++++++++++++++++ src/utils/constants.ts | 4 + 2 files changed, 119 insertions(+) create mode 100644 src/equicordplugins/customStatusTimeouts/index.tsx diff --git a/src/equicordplugins/customStatusTimeouts/index.tsx b/src/equicordplugins/customStatusTimeouts/index.tsx new file mode 100644 index 000000000..90b8eed4b --- /dev/null +++ b/src/equicordplugins/customStatusTimeouts/index.tsx @@ -0,0 +1,115 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const Millis = { + HALF_SECOND: 500, + SECOND: 1e3, + MINUTE: 6e4, + HOUR: 36e5, + DAY: 864e5, + WEEK: 6048e5, + DAYS_30: 2592e6 +}; + +const settings = definePluginSettings({ + showForeverOnTop: { + type: OptionType.BOOLEAN, + description: "Show the Forever option at the top of the list instead of the bottom.", + restartNeeded: true, + default: true + }, + extraSeconds: { + type: OptionType.STRING, + description: "Extra seconds to add, separated by a comma (e.g. 5, 10, 30)", + restartNeeded: true, + default: "15, 30, 45" + }, + extraMinutes: { + type: OptionType.STRING, + description: "Extra minutes to add, separated by a comma (e.g. 5, 10, 30)", + restartNeeded: true, + default: "5, 10, 30" + }, + extraHours: { + type: OptionType.STRING, + description: "Extra hours to add, separated by a comma (e.g. 2, 4, 6, 12)", + restartNeeded: true, + default: "2, 4, 6, 12" + }, + extraDays: { + type: OptionType.STRING, + description: "Extra days to add, separated by a comma (e.g. 1, 2)", + restartNeeded: true, + default: "1, 2" + }, +}); + +export default definePlugin({ + name: "CustomStatusTimeouts", + description: "Adds configurable timeout presets to the status (presence) menu.", + authors: [EquicordDevs.Kiri, EquicordDevs.thororen], + settings, + patches: [ + { + find: "#{intl::DURATION_FOREVER}", + replacement: { + match: /\[\{duration.*?#{intl::DURATION_FOREVER}\)\}\]/, + replace: "$self.buildTimeouts($&)" + } + } + ], + buildTimeouts(existing) { + const parse = (str: string) => str.split(",").map(s => Number(s.trim())).filter(Boolean); + + const seconds = parse(settings.store.extraSeconds); + const minutes = parse(settings.store.extraMinutes); + const hours = parse(settings.store.extraHours); + const days = parse(settings.store.extraDays); + // I was testing weeks and months and for some reason random ones didnt apply look below for the working ones + // tested weeks between 1-12 working weeks were 1, 2, 3, 8, 9, 10 the rest didnt work + // tested months between 1-12 working months were 2, 4, 10, 12 the rest didnt work + // for months I only tested evens except for 1 so I hardcoded them to avoid user complaints + const weeks = [1, 2, 3]; + const months = [2, 4]; + + const extra = [ + ...seconds.map(s => ({ + duration: s * Millis.SECOND, + label: () => `For ${s} ${s === 1 ? "Second" : "Seconds"}` + })), + ...minutes.map(m => ({ + duration: m * Millis.MINUTE, + label: () => `For ${m} ${m === 1 ? "Minute" : "Minutes"}` + })), + ...hours.map(h => ({ + duration: h * Millis.HOUR, + label: () => `For ${h} ${h === 1 ? "Hour" : "Hours"}` + })), + ...days.map(d => ({ + duration: d * Millis.DAY, + label: () => `For ${d} ${d === 1 ? "Day" : "Days"}` + })), + ...weeks.map(w => ({ + duration: w * Millis.WEEK, + label: () => `For ${w} ${w === 1 ? "Week" : "Weeks"}` + })), + ...months.map(m => ({ + duration: m * Millis.DAYS_30, + label: () => `For ${m} ${m === 1 ? "Month" : "Months"}` + })), + ]; + + return [...existing, ...extra].sort((a, b) => { + if (a.duration === undefined) return settings.store.showForeverOnTop ? -1 : 1; + if (b.duration === undefined) return settings.store.showForeverOnTop ? 1 : -1; + return a.duration - b.duration; + }); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1540f350b..96f90bd78 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1284,6 +1284,10 @@ export const EquicordDevs = Object.freeze({ name: "Ape", id: 722832640360841308n, }, + Kiri: { + name: "Kiri", + id: 310525496771346434n, + }, } satisfies Record); // iife so #__PURE__ works correctly From 028f2c2bb03e0d37505de7173cf591d334795cfb Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:19:44 -0400 Subject: [PATCH 04/19] IRememberYou: Remove Invite Stuff --- .../iRememberYou/components/data.tsx | 42 +------------------ 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/src/equicordplugins/iRememberYou/components/data.tsx b/src/equicordplugins/iRememberYou/components/data.tsx index 4d75730bb..6d5250f7e 100644 --- a/src/equicordplugins/iRememberYou/components/data.tsx +++ b/src/equicordplugins/iRememberYou/components/data.tsx @@ -6,7 +6,7 @@ import { DataStore } from "@api/index"; import { Guild, User } from "@vencord/discord-types"; -import { ChannelStore, GuildMemberStore, GuildStore, InviteActions, MessageStore, PermissionsBits, PermissionStore, UserStore, } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, GuildStore, MessageStore, UserStore, } from "@webpack/common"; export interface IUserExtra { isOwner?: boolean; @@ -104,9 +104,7 @@ export class Data { extra: { ...previouExtra, ...extra }, iconURL: user.getAvatarURL(), }; - - if (source && !processedGuilds.has(source.id) && this.hasInvitePermissions(source.id)) { - this.collectInviteLink(source.id); + if (source && !processedGuilds.has(source.id)) { processedGuilds.add(source.id); } } @@ -148,10 +146,6 @@ export class Data { } this.processUsersToCollection([...target]); - - if (guild && this.hasInvitePermissions(guild.id)) { - this.collectInviteLink(guild.id); - } } } @@ -176,38 +170,6 @@ export class Data { this.processUsersToCollection([...target]); } - hasInvitePermissions(guildId: string): boolean { - const guild = GuildStore.getGuild(guildId); - if (!guild) return false; - - const currentUser = UserStore.getCurrentUser(); - if (!currentUser) return false; - - if (guild.ownerId === currentUser.id) return true; - - const member = GuildMemberStore.getMember(guildId, currentUser.id); - if (!member) return false; - - return PermissionStore.can(PermissionsBits.CREATE_INSTANT_INVITE, guild); - } - - async collectInviteLink(guildId: string) { - try { - const invites = await InviteActions.getInvites(guildId); - if (invites && invites.length > 0) { - const invite = invites[0]; - const inviteCode = invite.code; - const inviteLink = `https://discord.gg/${inviteCode}`; - - if (this.usersCollection[guildId]) { - this.usersCollection[guildId].inviteLink = inviteLink; - } - } - } catch (error) { - console.warn("Failed to collect invite link for guild:", guildId, error); - } - } - storageAutoSaveProtocol() { this._storageAutoSaveProtocol_interval = setInterval( this.updateStorage.bind(this), From eba9ee446abc4419727a2417f550fec742813100 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:24:51 -0400 Subject: [PATCH 05/19] restrict build branches --- .github/workflows/build.yml | 1 + .github/workflows/{buildDev.yml => buildBranches.yml} | 1 + .github/workflows/nixosBuild.yml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename .github/workflows/{buildDev.yml => buildBranches.yml} (98%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d7d4d27b..cd39145b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,7 @@ jobs: gh release upload latest --clobber dist/release/* - name: Upload Plugin JSONs to Equibored repo + if: github.repository == 'Equicord/Equicord' run: | git config --global user.name "GitHub-Actions" git config --global user.email actions@github.com diff --git a/.github/workflows/buildDev.yml b/.github/workflows/buildBranches.yml similarity index 98% rename from .github/workflows/buildDev.yml rename to .github/workflows/buildBranches.yml index 6ae23fef9..568c8a379 100644 --- a/.github/workflows/buildDev.yml +++ b/.github/workflows/buildBranches.yml @@ -28,6 +28,7 @@ jobs: Build: name: Build Equicord runs-on: ubuntu-latest + if: github.repository == 'Equicord/Equicord' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index 2ea5a91b9..827e4d9eb 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -50,7 +50,7 @@ jobs: cp browser/browser.* release cp Equicord.user.{js,js.LEGAL.txt} release - # copy the plugin data jsons, the extension zips and the desktop/vesktop asars + # copy the plugin data jsons, the extension zips and the desktop/equibop asars cp *.{json,zip,asar} release # legacy un-asared files From 5e4a310c4e751d957805e36c5a44cdf339381911 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:05:45 -0400 Subject: [PATCH 06/19] revert #957 --- src/api/DataStore/index.ts | 252 ++++++++---------- .../LoggedMessageManager.ts | 4 +- .../messageLoggerEnhanced/db.ts | 245 +++++++---------- src/equicordplugins/vcNarratorCustom/util.ts | 130 ++++----- 4 files changed, 267 insertions(+), 364 deletions(-) diff --git a/src/api/DataStore/index.ts b/src/api/DataStore/index.ts index 3cdb50c7e..80ebb065f 100644 --- a/src/api/DataStore/index.ts +++ b/src/api/DataStore/index.ts @@ -18,97 +18,31 @@ * limitations under the License. */ -import { Logger } from "@utils/Logger"; - -const logger = new Logger("DataStore"); -let loggedFailure = false; -const memoryDatabases = new Map>>(); - -interface MemoryStore { - __memoryStore: true; - get(key: IDBValidKey): T | undefined; - put(value: unknown, key: IDBValidKey): void; - delete(key: IDBValidKey): void; - clear(): void; - getAllKeys(): KeyType[]; - getAll(): T[]; - entries(): [KeyType, ValueType][]; -} - -type StoreLike = IDBObjectStore | MemoryStore; - -function handleFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("IndexedDB unavailable, using empty storage.", error); -} - -function withFailureFallback(promise: Promise, fallback: T): Promise { - return promise.catch(error => { - handleFailure(error); - return fallback; - }); -} - -function getMemoryStore(dbName: string, storeName: string): MemoryStore { - let database = memoryDatabases.get(dbName); - if (!database) { - database = new Map(); - memoryDatabases.set(dbName, database); - } - - let data = database.get(storeName); - if (!data) { - data = new Map(); - database.set(storeName, data); - } - - return { - __memoryStore: true, - get: (key: IDBValidKey) => data.get(key) as T | undefined, - put: (value, key) => { data.set(key, value); }, - delete: key => { data.delete(key); }, - clear: () => { data.clear(); }, - getAllKeys: () => Array.from(data.keys()) as KeyType[], - getAll: () => Array.from(data.values()) as T[], - entries: () => Array.from(data.entries()) as [KeyType, ValueType][] - }; -} - -function isMemoryStore(store: StoreLike): store is MemoryStore { - return "__memoryStore" in store; -} - export function promisifyRequest( request: IDBRequest | IDBTransaction, ): Promise { return new Promise((resolve, reject) => { - const target = request as IDBRequest & IDBTransaction; - target.oncomplete = target.onsuccess = () => resolve(target.result); - target.onabort = target.onerror = () => reject(target.error); + // @ts-expect-error - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // @ts-expect-error - file size hacks + request.onabort = request.onerror = () => reject(request.error); }); } export function createStore(dbName: string, storeName: string): UseStore { const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); - const dbp = promisifyRequest(request).catch(error => { - handleFailure(error); - return null; - }); - const memoryStore = getMemoryStore(dbName, storeName); + const dbp = promisifyRequest(request); - return async (txMode, callback) => { - const db = await dbp; - if (!db) return callback(memoryStore); - - return callback(db.transaction(storeName, txMode).objectStore(storeName)); - }; + return (txMode, callback) => + dbp.then(db => + callback(db.transaction(storeName, txMode).objectStore(storeName)), + ); } export type UseStore = ( txMode: IDBTransactionMode, - callback: (store: StoreLike) => T | PromiseLike, + callback: (store: IDBObjectStore) => T | PromiseLike, ) => Promise; let defaultGetStoreFunc: UseStore | undefined; @@ -120,68 +54,88 @@ function defaultGetStore() { return defaultGetStoreFunc; } +/** + * Get a value by its key. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function get( key: IDBValidKey, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.get(key) as T | undefined; - return promisifyRequest(store.get(key)); - }), undefined); + return customStore("readonly", store => promisifyRequest(store.get(key))); } +/** + * Set a value with a key. + * + * @param key + * @param value + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function set( key: IDBValidKey, value: any, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.put(value, key); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Set multiple values at once. This is faster than calling set() multiple times. + * It's also atomic – if one of the pairs can't be added, none will be added. + * + * @param entries Array of entries, where each entry is an array of `[key, value]`. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function setMany( entries: [IDBValidKey, any][], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { entries.forEach(entry => store.put(entry[1], entry[0])); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Get multiple values by their keys + * + * @param keys + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function getMany( keys: IDBValidKey[], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) { - return keys.map(key => store.get(key) as T | undefined); - } - - return Promise.all(keys.map(key => promisifyRequest(store.get(key)))); - }), []); + return customStore("readonly", store => + Promise.all(keys.map(key => promisifyRequest(store.get(key)))), + ); } +/** + * Update a value. This lets you see the old value and update it as an atomic operation. + * + * @param key + * @param updater A callback that takes the old value and returns a new value. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function update( key: IDBValidKey, updater: (oldValue: T | undefined) => T, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore( + return customStore( "readwrite", - store => { - if (isMemoryStore(store)) { - store.put(updater(store.get(key) as T | undefined), key); - return; - } - - return new Promise((resolve, reject) => { + store => + // Need to create the promise manually. + // If I try to chain promises, the transaction closes in browsers + // that use a promise polyfill (IE10/11). + new Promise((resolve, reject) => { store.get(key).onsuccess = function () { try { store.put(updater(this.result), key); @@ -190,42 +144,52 @@ export function update( reject(err); } }; - }); - }, - ), undefined); + }), + ); } +/** + * Delete a particular key from the store. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function del( key: IDBValidKey, customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.delete(key); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Delete multiple keys at once. + * + * @param keys List of keys to delete. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function delMany( keys: IDBValidKey[], customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readwrite", store => { - keys.forEach(key => store.delete(key)); - if (isMemoryStore(store)) return; - + return customStore("readwrite", (store: IDBObjectStore) => { + keys.forEach((key: IDBValidKey) => store.delete(key)); return promisifyRequest(store.transaction); - }), undefined); + }); } +/** + * Clear all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function clear(customStore = defaultGetStore()): Promise { - return withFailureFallback(customStore("readwrite", store => { + return customStore("readwrite", store => { store.clear(); - if (isMemoryStore(store)) return; - return promisifyRequest(store.transaction); - }), undefined); + }); } function eachCursor( @@ -240,12 +204,16 @@ function eachCursor( return promisifyRequest(store.transaction); } +/** + * Get all keys in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function keys( customStore = defaultGetStore(), ): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.getAllKeys(); - + return customStore("readonly", store => { + // Fast path for modern browsers if (store.getAllKeys) { return promisifyRequest( store.getAllKeys() as unknown as IDBRequest, @@ -253,45 +221,59 @@ export function keys( } const items: KeyType[] = []; + return eachCursor(store, cursor => items.push(cursor.key as KeyType), ).then(() => items); - }), [] as KeyType[]); + }); } +/** + * Get all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function values(customStore = defaultGetStore()): Promise { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.getAll(); - + return customStore("readonly", store => { + // Fast path for modern browsers if (store.getAll) { return promisifyRequest(store.getAll() as IDBRequest); } const items: T[] = []; + return eachCursor(store, cursor => items.push(cursor.value as T)).then( () => items, ); - }), [] as T[]); + }); } +/** + * Get all entries in the store. Each entry is an array of `[key, value]`. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ export function entries( customStore = defaultGetStore(), ): Promise<[KeyType, ValueType][]> { - return withFailureFallback(customStore("readonly", store => { - if (isMemoryStore(store)) return store.entries(); - + return customStore("readonly", store => { + // Fast path for modern browsers + // (although, hopefully we'll get a simpler path some day) if (store.getAll && store.getAllKeys) { return Promise.all([ promisifyRequest( store.getAllKeys() as unknown as IDBRequest, ), promisifyRequest(store.getAll() as IDBRequest), - ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]] as [KeyType, ValueType])); + ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]])); } const items: [KeyType, ValueType][] = []; - return eachCursor(store, cursor => - items.push([cursor.key as KeyType, cursor.value]), - ).then(() => items); - }), [] as [KeyType, ValueType][]); + + return customStore("readonly", store => + eachCursor(store, cursor => + items.push([cursor.key as KeyType, cursor.value]), + ).then(() => items), + ); + }); } diff --git a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts index 7f51572af..5408349e9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts +++ b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts @@ -17,7 +17,7 @@ */ import { Flogger, settings } from "."; -import { addMessageIDB, countMessagesIDB, DBMessageStatus, deleteMessagesBulkIDB, getOlderThanTimestampForGuildsIDB, getOldestMessagesIDB } from "./db"; +import { addMessageIDB, db, DBMessageStatus, deleteMessagesBulkIDB, getOlderThanTimestampForGuildsIDB, getOldestMessagesIDB } from "./db"; import { LoggedMessage, LoggedMessageJSON } from "./types"; import { cleanupMessage } from "./utils"; import { cacheMessageImages } from "./utils/saveImage"; @@ -47,7 +47,7 @@ export const addMessage = async (message: LoggedMessage | LoggedMessageJSON, sta } if (settings.store.messageLimit > 0) { - const currentMessageCount = await countMessagesIDB(); + const currentMessageCount = await db.count("messages"); if (currentMessageCount > settings.store.messageLimit) { const messagesToDelete = currentMessageCount - settings.store.messageLimit; if (messagesToDelete <= 0 || messagesToDelete >= settings.store.messageLimit) return; diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 4fae28348..8d80e0213 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Logger } from "@utils/Logger"; import { ChannelStore, Toasts } from "@webpack/common"; import { DBSchema, IDBPDatabase, openDB } from "idb"; @@ -38,63 +37,17 @@ export interface MLIDB extends DBSchema { by_timestamp_and_message_id: [string, string]; }; }; + } -const logger = new Logger("MessageLoggerEnhanced"); -let db: IDBPDatabase | null = null; -let dbPromise: Promise | null> | null = null; -let loggedFailure = false; - +export let db: IDBPDatabase; export const cachedMessages = new Map(); -function handleDbFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("Message storage unavailable, using memory only.", error); -} - -async function ensureDb() { - if (db) return db; - if (!dbPromise) { - dbPromise = openDB(DB_NAME, DB_VERSION, { - upgrade(database) { - const messageStore = database.createObjectStore("messages", { keyPath: "message_id" }); - messageStore.createIndex("by_channel_id", "channel_id"); - messageStore.createIndex("by_status", "status"); - messageStore.createIndex("by_timestamp", "message.timestamp"); - messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]); - } - }).then(database => { - db = database; - return database; - }).catch(error => { - handleDbFailure(error); - return null; - }); - } - - db = await dbPromise; - return db; -} - -async function withDb(fallback: T, callback: (database: IDBPDatabase) => Promise): Promise { - const database = await ensureDb(); - if (!database) return fallback; - - try { - return await callback(database); - } catch (error) { - handleDbFailure(error); - return fallback; - } -} - -async function cacheRecords(records: DBMessageRecord[], cacheAttachmentBlobs = true) { +// this is probably not the best way to do this +async function cacheRecords(records: DBMessageRecord[]) { for (const r of records) { cacheRecord(r); - if (!cacheAttachmentBlobs) continue; - for (const att of r.message.attachments) { const blobUrl = await getAttachmentBlobUrl(att); if (blobUrl) { @@ -103,7 +56,6 @@ async function cacheRecords(records: DBMessageRecord[], cacheAttachmentBlobs = t } } } - return records; } @@ -116,56 +68,56 @@ async function cacheRecord(record?: DBMessageRecord | null) { } export async function initIDB() { - await ensureDb(); + db = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + const messageStore = db.createObjectStore("messages", { keyPath: "message_id" }); + messageStore.createIndex("by_channel_id", "channel_id"); + messageStore.createIndex("by_status", "status"); + messageStore.createIndex("by_timestamp", "message.timestamp"); + messageStore.createIndex("by_timestamp_and_message_id", ["channel_id", "message.timestamp"]); + } + }); } -void initIDB(); +initIDB(); export async function hasMessageIDB(message_id: string) { - if (cachedMessages.has(message_id)) return true; - return withDb(false, database => database.count("messages", message_id).then(count => count > 0)); + return cachedMessages.has(message_id) || (await db.count("messages", message_id)) > 0; } export async function countMessagesIDB() { - if (!db) return cachedMessages.size; return db.count("messages"); } export async function countMessagesByStatusIDB(status: DBMessageStatus) { - return withDb(0, database => database.countFromIndex("messages", "by_status", status)); + return db.countFromIndex("messages", "by_status", status); } export async function getAllMessagesIDB() { - return withDb([], async database => cacheRecords(await database.getAll("messages"))); + return cacheRecords(await db.getAll("messages")); } export async function getMessagesForChannelIDB(channel_id: string) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_channel_id", channel_id))); + return cacheRecords(await db.getAllFromIndex("messages", "by_channel_id", channel_id)); } export async function getMessageIDB(message_id: string) { - return withDb(null, async database => { - const record = await database.get("messages", message_id); - await cacheRecord(record); - return record ?? null; - }); + return cacheRecord(await db.get("messages", message_id)); } export async function getMessagesByStatusIDB(status: DBMessageStatus) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_status", status))); + return cacheRecords(await db.getAllFromIndex("messages", "by_status", status)); } export async function getOldestMessagesIDB(limit: number) { - return withDb([], async database => cacheRecords(await database.getAllFromIndex("messages", "by_timestamp", undefined, limit))); + return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit)); } export async function* iterateAllMessagesIDB(batchSize = 100) { - if (!db && !(await ensureDb())) return; - let lastId: string | undefined; while (true) { const batch: DBMessageRecord[] = []; - - const tx = db!.transaction("messages"); + // new transaction for each batch to avoid timeouts during yield + const tx = db.transaction("messages"); const range = lastId ? IDBKeyRange.lowerBound(lastId, true) : undefined; let cursor = await tx.store.openCursor(range); @@ -177,6 +129,7 @@ export async function* iterateAllMessagesIDB(batchSize = 100) { if (batch.length === 0) break; lastId = batch[batch.length - 1].message_id; + yield await cacheRecords(batch); if (batch.length < batchSize) break; @@ -184,20 +137,22 @@ export async function* iterateAllMessagesIDB(batchSize = 100) { } export async function getOlderThanTimestampIDB(timestamp: string) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_timestamp"); - const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp)); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_timestamp"); - if (!cursor) return []; + const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp)); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - } + if (!cursor) { + return []; + } - return cacheRecords(messages, false); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + } + + return cacheRecords(messages); } export async function getOlderThanTimestampForGuildsIDB(timestamp: string, currentChannelId?: string, preserveCurrentChannel?: boolean) { @@ -212,104 +167,96 @@ export async function getOlderThanTimestampForGuildsIDB(timestamp: string, curre } export async function getDateStortedMessagesByStatusIDB(newest: boolean, limit: number, status: DBMessageStatus) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_status"); - const direction = newest ? "prev" : "next"; - const cursor = await index.openCursor(IDBKeyRange.only(status), direction); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_status"); - if (!cursor) { - return []; - } + const direction = newest ? "prev" : "next"; + const cursor = await index.openCursor(IDBKeyRange.only(status), direction); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - if (messages.length >= limit) break; - } + if (!cursor) { + console.log("No messages found"); + return []; + } - return cacheRecords(messages); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + if (messages.length >= limit) break; + } + + return cacheRecords(messages); } export async function getMessagesByChannelAndAfterTimestampIDB(channel_id: string, start: string) { - return withDb([], async database => { - const tx = database.transaction("messages", "readonly"); - const index = tx.store.index("by_timestamp_and_message_id"); - const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"])); + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_timestamp_and_message_id"); - if (!cursor) { - return []; - } + const cursor = await index.openCursor(IDBKeyRange.bound([channel_id, start], [channel_id, "\uffff"])); - const messages: DBMessageRecord[] = []; - for await (const c of cursor) { - messages.push(c.value); - } + if (!cursor) { + console.log("No messages found in range"); + return []; + } - return cacheRecords(messages, false); - }); + const messages: DBMessageRecord[] = []; + for await (const c of cursor) { + messages.push(c.value); + } + + return cacheRecords(messages); } export async function addMessageIDB(message: LoggedMessageJSON, status: DBMessageStatus) { stripTransientRenderState(message); - cachedMessages.set(message.id, message); - return withDb(void 0, async database => { - await database.put("messages", { - channel_id: message.channel_id, - message_id: message.id, - status, - message, - }); + await db.put("messages", { + channel_id: message.channel_id, + message_id: message.id, + status, + message, }); + + cachedMessages.set(message.id, message); } export async function addMessagesBulkIDB(messages: LoggedMessageJSON[], status?: DBMessageStatus) { messages.forEach(stripTransientRenderState); + + const tx = db.transaction("messages", "readwrite"); + const { store } = tx; + + await Promise.all([ + ...messages.map(message => store.add({ + channel_id: message.channel_id, + message_id: message.id, + status: status ?? getMessageStatus(message), + message, + })), + tx.done + ]); + messages.forEach(message => cachedMessages.set(message.id, message)); - - return withDb(void 0, async database => { - const tx = database.transaction("messages", "readwrite"); - const { store } = tx; - - await Promise.all([ - ...messages.map(message => store.add({ - channel_id: message.channel_id, - message_id: message.id, - status: status ?? getMessageStatus(message), - message, - })), - tx.done - ]); - }); } export async function deleteMessageIDB(message_id: string) { - cachedMessages.delete(message_id); + await db.delete("messages", message_id); - return withDb(void 0, database => database.delete("messages", message_id)); + cachedMessages.delete(message_id); } export async function deleteMessagesBulkIDB(message_ids: string[]) { - message_ids.forEach(id => cachedMessages.delete(id)); + const tx = db.transaction("messages", "readwrite"); + const { store } = tx; - return withDb(void 0, async database => { - const tx = database.transaction("messages", "readwrite"); - const { store } = tx; - await Promise.all([ - ...message_ids.map(id => store.delete(id)), - tx.done - ]); - }); + await Promise.all([...message_ids.map(id => store.delete(id)), tx.done]); + message_ids.forEach(id => cachedMessages.delete(id)); } -export async function clearMessagesIDB(showToast = true) { +export async function clearMessagesIDB() { + await db.clear("messages"); cachedMessages.clear(); - await withDb(void 0, database => database.clear("messages")); - - if (!showToast) return; - Toasts.show({ type: Toasts.Type.MESSAGE, message: "Cleared message log database and cache.", diff --git a/src/equicordplugins/vcNarratorCustom/util.ts b/src/equicordplugins/vcNarratorCustom/util.ts index 476d2d2e0..5d27f5e3e 100644 --- a/src/equicordplugins/vcNarratorCustom/util.ts +++ b/src/equicordplugins/vcNarratorCustom/util.ts @@ -198,9 +198,6 @@ const DB_VERSION = 2; const VOICES_STORE = "voices"; -import { Logger } from "@utils/Logger"; - -const logger = new Logger("VcNarratorCustom"); const META_STORE = "voices_meta"; type VoiceMeta = { @@ -209,76 +206,49 @@ type VoiceMeta = { lastAccess: number; }; -let openDbPromise: Promise | undefined; -let loggedFailure = false; +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = event => { + const db = request.result; + const upgradeTx = request.transaction; + if (!db.objectStoreNames.contains(VOICES_STORE)) { + db.createObjectStore(VOICES_STORE); + } + if (!db.objectStoreNames.contains(META_STORE)) { + const meta = db.createObjectStore(META_STORE); + meta.createIndex("by_lastAccess", "lastAccess"); + } -function handleDbFailure(error: unknown) { - if (loggedFailure) return; - loggedFailure = true; - logger.error("Voice cache storage unavailable, falling back to memory.", error); -} - -function openDB(): Promise { - if (!openDbPromise) { - openDbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onupgradeneeded = event => { - const db = request.result; - const upgradeTx = request.transaction; - if (!db.objectStoreNames.contains(VOICES_STORE)) { - db.createObjectStore(VOICES_STORE); - } - if (!db.objectStoreNames.contains(META_STORE)) { - const meta = db.createObjectStore(META_STORE); - meta.createIndex("by_lastAccess", "lastAccess"); - } - - if (upgradeTx && (event?.oldVersion ?? 0) < 2) { - const now = Date.now(); - const voices = upgradeTx.objectStore(VOICES_STORE); - const meta = upgradeTx.objectStore(META_STORE); - const cursorReq = voices.openCursor(); - cursorReq.onsuccess = () => { - const cursor = cursorReq.result; - if (!cursor) return; - const blob = cursor.value as Blob | undefined; - meta.put( - { - size: blob?.size ?? 0, - createdAt: now, - lastAccess: now, - } satisfies VoiceMeta, - String(cursor.key) - ); - cursor.continue(); - }; - } - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }).then(db => db).catch(error => { - handleDbFailure(error); - return null; - }); - } - - return openDbPromise; -} - -async function withDb(fallback: T, callback: (db: IDBDatabase) => Promise): Promise { - const db = await openDB(); - if (!db) return fallback; - - try { - return await callback(db); - } catch (error) { - handleDbFailure(error); - return fallback; - } + if (upgradeTx && (event?.oldVersion ?? 0) < 2) { + const now = Date.now(); + const voices = upgradeTx.objectStore(VOICES_STORE); + const meta = upgradeTx.objectStore(META_STORE); + const cursorReq = voices.openCursor(); + cursorReq.onsuccess = () => { + const cursor = cursorReq.result; + if (!cursor) return; + const blob = cursor.value as Blob | undefined; + meta.put( + { + size: blob?.size ?? 0, + createdAt: now, + lastAccess: now, + } satisfies VoiceMeta, + String(cursor.key) + ); + cursor.continue(); + }; + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); } export async function getPersistentTtsCacheStats(): Promise<{ bytes: number; entries: number; }> { - return withDb<{ bytes: number; entries: number; }>({ bytes: 0, entries: 0 }, db => new Promise<{ bytes: number; entries: number; }>((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction(META_STORE, "readonly"); const store = tx.objectStore(META_STORE); @@ -298,7 +268,7 @@ export async function getPersistentTtsCacheStats(): Promise<{ bytes: number; ent cursor.continue(); }; cursorReq.onerror = () => reject(cursorReq.error); - })); + }); } export async function clearTtsCache(): Promise { @@ -309,17 +279,19 @@ export async function clearTtsCache(): Promise { } ttsCache.clear(); - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); tx.objectStore(VOICES_STORE).clear(); tx.objectStore(META_STORE).clear(); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); } async function trimPersistentCacheToMaxBytes(maxBytes = PERSISTENT_TTS_CACHE_MAX_BYTES): Promise { - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -372,11 +344,12 @@ async function trimPersistentCacheToMaxBytes(maxBytes = PERSISTENT_TTS_CACHE_MAX tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); } export async function getCachedVoiceFromDB(cacheKey: string): Promise { - return withDb(null, db => new Promise((resolve, reject) => { + const db = await openDB(); + return new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -400,11 +373,12 @@ export async function getCachedVoiceFromDB(cacheKey: string): Promise reject(request.error); - })); + }); } export async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise { - await withDb(void 0, db => new Promise((resolve, reject) => { + const db = await openDB(); + await new Promise((resolve, reject) => { const tx = db.transaction([VOICES_STORE, META_STORE], "readwrite"); const voicesStore = tx.objectStore(VOICES_STORE); const metaStore = tx.objectStore(META_STORE); @@ -418,7 +392,7 @@ export async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise< tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); - })); + }); await trimPersistentCacheToMaxBytes(); } From 6e1ef5efa3322b326e1a101c64abf88495554c04 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:06:18 -0400 Subject: [PATCH 07/19] revert #958 --- src/equicordplugins/messageLoggerEnhanced/utils/misc.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts index 43c5f4166..66a264abf 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts @@ -24,6 +24,7 @@ import { DBMessageStatus } from "../db"; import { LoggedMessageJSON } from "../types"; import { DEFAULT_IMAGE_CACHE_DIR } from "./constants"; import { DISCORD_EPOCH } from "./index"; +import { memoize } from "./memoize"; const MessageClass: any = findLazy(m => m?.prototype?.isEdited); const AuthorClass = findLazy(m => m?.prototype?.getAvatarURL); @@ -92,7 +93,7 @@ export const mapTimestamp = (m: any) => { return m; }; -export const messageJsonToMessageClass = (log: { message: LoggedMessageJSON; }) => { +export const messageJsonToMessageClass = memoize((log: { message: LoggedMessageJSON; }) => { // console.time("message populate"); if (!log?.message) return null; @@ -122,7 +123,7 @@ export const messageJsonToMessageClass = (log: { message: LoggedMessageJSON; }) // console.timeEnd("message populate"); return message; -}; +}); export function parseJSON(json?: string | null) { try { From b59f42cd183d205c92f6bd821cf9ed88d2843922 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:25:03 -0400 Subject: [PATCH 08/19] MLE: Try to ensure db exists --- src/equicordplugins/messageLoggerEnhanced/db.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 8d80e0213..7afb56fa9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -68,6 +68,7 @@ async function cacheRecord(record?: DBMessageRecord | null) { } export async function initIDB() { + if (db) return; db = await openDB(DB_NAME, DB_VERSION, { upgrade(db) { const messageStore = db.createObjectStore("messages", { keyPath: "message_id" }); @@ -211,6 +212,7 @@ export async function getMessagesByChannelAndAfterTimestampIDB(channel_id: strin export async function addMessageIDB(message: LoggedMessageJSON, status: DBMessageStatus) { stripTransientRenderState(message); + if (!db) await initIDB(); await db.put("messages", { channel_id: message.channel_id, message_id: message.id, From 87bcf6231ecadd4719b7ac2a7147463f9d789cd2 Mon Sep 17 00:00:00 2001 From: fckyough Date: Tue, 14 Apr 2026 09:44:31 +0800 Subject: [PATCH 09/19] fix irc colors for dms (#971) * fix irc colors for dms minimum difference if the colors are 45 deg or less apart * Update .gitignore * Update .gitignore * 1000 miles --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: creations --- src/plugins/ircColors/index.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts index bd3b72e8b..66bc716ac 100644 --- a/src/plugins/ircColors/index.ts +++ b/src/plugins/ircColors/index.ts @@ -22,6 +22,7 @@ import { hash as h64 } from "@intrnl/xxhash64"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { useMemo } from "@webpack/common"; +import { UserStore } from "@webpack/common"; // Calculate a CSS color string based on the user ID function calculateNameColorForUser(id?: string) { @@ -121,9 +122,25 @@ export default definePlugin({ return colorString; } - return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) + const dmColor = (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) ? color : colorString; + + // guarantee minimum difference in dms + if (context?.channel?.isPrivate?.() && dmColor && userId) { + const currentUserId = UserStore.getCurrentUser()?.id; + if (currentUserId && userId !== currentUserId) { + const currentUserColor = Number(h64(currentUserId) % 360n); + const otherUserColor = Number(h64(userId) % 360n); + const colorDiff = Math.min(Math.abs(currentUserColor - otherUserColor), 360 - Math.abs(currentUserColor - otherUserColor)); + if (colorDiff < 45) { + const newColor = (otherUserColor + 180) % 360; + return `hsl(${newColor}, 100%, ${settings.store.lightness}%)`; + } + } + } + + return dmColor; }, calculateNameColorForListContext(context: any) { From 8099f44349ba2558b39cf901bea9090518dff2aa Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:55:03 -0400 Subject: [PATCH 10/19] NoMosaic: Fix Patch --- src/plugins/noMosaic/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/noMosaic/index.ts b/src/plugins/noMosaic/index.ts index 4ec64cc39..c52e19a6b 100644 --- a/src/plugins/noMosaic/index.ts +++ b/src/plugins/noMosaic/index.ts @@ -29,8 +29,8 @@ export default definePlugin({ { find: '"PLAINTEXT_PREVIEW":"OTHER"', replacement: { - match: /=>"IMAGE"===\i\|\|"VIDEO"===\i(?:\|\|("VISUAL_PLACEHOLDER"===\i))?;/, - replace: (_, visualPlaceholderPred) => visualPlaceholderPred != null ? `=>${visualPlaceholderPred};` : "=>false;" + match: /return"IMAGE"===\i\|\|"VIDEO"===\i(?:\|\|("VISUAL_PLACEHOLDER"===\i)\|\|\i&&"CLIP"===\i)?/, + replace: (_, visualPlaceholderPred) => visualPlaceholderPred != null ? `return ${visualPlaceholderPred}` : "return false" } }, { From fd27b98a6cb2c25fbcc0d96785cc10558b344c57 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:58:39 -0400 Subject: [PATCH 11/19] MusicControls: Fix 1 Styling --- src/equicordplugins/musicControls/spotify/spotifyStyles.css | 2 +- src/equicordplugins/musicControls/tidal/tidalStyles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/musicControls/spotify/spotifyStyles.css b/src/equicordplugins/musicControls/spotify/spotifyStyles.css index 8f54fa623..d33f4fd47 100644 --- a/src/equicordplugins/musicControls/spotify/spotifyStyles.css +++ b/src/equicordplugins/musicControls/spotify/spotifyStyles.css @@ -70,7 +70,7 @@ position: absolute; top: 50%; left: 50%; - transform: translate(-50%, -50%); + transform: translate(-55%, -50%); } .vc-spotify-button-row { diff --git a/src/equicordplugins/musicControls/tidal/tidalStyles.css b/src/equicordplugins/musicControls/tidal/tidalStyles.css index e5e840cb1..746f6c680 100644 --- a/src/equicordplugins/musicControls/tidal/tidalStyles.css +++ b/src/equicordplugins/musicControls/tidal/tidalStyles.css @@ -135,7 +135,7 @@ position: absolute; top: 50%; left: 50%; - transform: translate(-50%, -50%); + transform: translate(-55%, -50%); } #eq-tdl-info-wrapper { From 45b9cc479be9173e43062d43bb546027bf8321c8 Mon Sep 17 00:00:00 2001 From: MasterPepe <56143265+Accentaro@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:33:23 +0100 Subject: [PATCH 12/19] Feat: CollapsibleUI plugin (#970) * Add CollapsibleUI plugin * Fix Conflicts * fix lint * lint * use node 22 --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- .github/workflows/build.yml | 4 +- .github/workflows/buildBranches.yml | 4 +- .github/workflows/nixosBuild.yml | 4 +- .github/workflows/publish.yml | 4 +- .github/workflows/reportBrokenPlugins.yml | 4 +- .github/workflows/test.yml | 4 +- src/equicordplugins/_api/userArea.ts | 4 +- src/equicordplugins/collapsibleUi/index.tsx | 148 ++++++++++++ src/equicordplugins/collapsibleUi/settings.ts | 70 ++++++ src/equicordplugins/collapsibleUi/style.css | 213 ++++++++++++++++++ src/equicordplugins/hideChatButtons/index.tsx | 2 +- .../messageLoggerEnhanced/db.ts | 6 +- src/plugins/ircColors/index.ts | 5 +- 13 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 src/equicordplugins/collapsibleUi/index.tsx create mode 100644 src/equicordplugins/collapsibleUi/settings.ts create mode 100644 src/equicordplugins/collapsibleUi/style.css diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd39145b0..13af317c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,10 +23,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/buildBranches.yml b/.github/workflows/buildBranches.yml index 568c8a379..e79258ea6 100644 --- a/.github/workflows/buildBranches.yml +++ b/.github/workflows/buildBranches.yml @@ -38,11 +38,11 @@ jobs: - uses: pnpm/action-setup@v3 if: github.ref_name != 'bun' - - name: Use Node.js 20 + - name: Use Node.js 22 if: github.ref_name != 'bun' uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index 827e4d9eb..4c70f85ac 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -20,10 +20,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2452e5fc8..9c2523eab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,11 +62,11 @@ jobs: if: steps.check.outputs.release == 'true' uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 if: steps.check.outputs.release == 'true' uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 117a9a70c..9eec2e800 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -33,10 +33,10 @@ jobs: - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 708974e68..b6ae47688 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,10 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/src/equicordplugins/_api/userArea.ts b/src/equicordplugins/_api/userArea.ts index 112ac9bce..6fbefa328 100644 --- a/src/equicordplugins/_api/userArea.ts +++ b/src/equicordplugins/_api/userArea.ts @@ -22,8 +22,8 @@ export default definePlugin({ find: ".DISPLAY_NAME_STYLES_COACHMARK)", replacement: [ { - match: /(?<=className:(\i)\.\i,style:\i,)children:\[/, - replace: "children:[...$self.renderButtons(arguments[0],$1)," + match: /children:\[(?=.{0,50}accountContainerRef:\i)/, + replace: "children:[...$self.renderButtons(arguments[0])," }, // fix discord weird shrink with extra buttons { diff --git a/src/equicordplugins/collapsibleUi/index.tsx b/src/equicordplugins/collapsibleUi/index.tsx new file mode 100644 index 000000000..d72033893 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/index.tsx @@ -0,0 +1,148 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ChannelToolbarButton } from "@api/HeaderBar"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { classes } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { ContextMenuApi, Menu } from "@webpack/common"; +import type { ReactNode, SVGProps } from "react"; + +import { PanelId, panelRegistry, settings, toolbarPanelOrder } from "./settings"; +import managedStyle from "./style.css?managed"; + +const cl = classNameFactory("vc-collapsible-ui-"); + +type StateStore = ReturnType; + +function PanelsIcon(props: SVGProps) { + return ( + + + + ); +} + +function getStateClasses(store: StateStore) { + const classNames = [cl("state"), cl(`transition-${store.transitionMs}`)]; + + for (const panelId of toolbarPanelOrder) { + const panel = panelRegistry[panelId]; + if (store[panel.collapsedKey]) classNames.push(cl(`${panel.classId}-collapsed`)); + } + + return classes(...classNames); +} + +function togglePanel(panelId: PanelId) { + const key = panelRegistry[panelId].collapsedKey; + settings.store[key] = !settings.store[key]; +} + +const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => { + const store = settings.use(); + + return ( + + {toolbarPanelOrder.map(panelId => { + const panel = panelRegistry[panelId]; + const collapsed = store[panel.collapsedKey]; + + return ( + togglePanel(panelId)} + /> + ); + })} + + ); +}, { noop: true }); + +const ToolbarButtons = ErrorBoundary.wrap(() => { + const store = settings.use(); + const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]); + + return ( + <> + + ContextMenuApi.openContextMenu(event, () => )} + onContextMenu={event => ContextMenuApi.openContextMenu(event, () => )} + /> + + ); +}, { noop: true }); + +const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => { + const { chatButtonsCollapsed } = settings.use(); + + if (buttons.length === 0) return <>{buttons}; + + return ( +
+
+ {buttons} +
+
+ ); +}, { noop: true }); + +export default definePlugin({ + name: "CollapsibleUI", + description: "Native collapsible channel, member, chat button, and user area surfaces.", + authors: [EquicordDevs.benjii], + tags: ["ui", "sidebar", "layout"], + managedStyle, + settings, + + headerBarButton: { + icon: PanelsIcon, + location: "channeltoolbar", + priority: 25, + render: () => , + }, + + patches: [ + { + find: ".DISPLAY_NAME_STYLES_COACHMARK)", + replacement: { + match: /className:(\i\.\i),(?=style:(\i),children:\[)/, + replace: "className:$self.userAreaControlsClass($1)," + } + }, + { + find: '"sticker")', + replacement: { + match: /(?<="div",\{.{0,15}children:)(.+?)\}/, + replace: "$self.wrapChatButtons($1)}" + } + } + ], + + wrapChatButtons(buttons: ReactNode[]) { + if (!Array.isArray(buttons) || buttons.length === 0) return buttons; + return ; + }, + + userAreaControlsClass(className: string) { + return classes(className, cl("user-area-controls")); + }, +}); diff --git a/src/equicordplugins/collapsibleUi/settings.ts b/src/equicordplugins/collapsibleUi/settings.ts new file mode 100644 index 000000000..dce2485b2 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/settings.ts @@ -0,0 +1,70 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +export const transitionMarkers = [0, 80, 120, 160, 200, 260, 320]; + +export const panelRegistry = { + channelList: { + id: "channelList", + classId: "channel-list", + label: "Channel List", + collapsedKey: "channelListCollapsed", + }, + membersList: { + id: "membersList", + classId: "members-list", + label: "Members List", + collapsedKey: "membersListCollapsed", + }, + chatButtons: { + id: "chatButtons", + classId: "chat-buttons", + label: "Message Buttons", + collapsedKey: "chatButtonsCollapsed", + }, + userArea: { + id: "userArea", + classId: "user-area", + label: "User Area", + collapsedKey: "userAreaCollapsed", + }, +} as const; + +export type PanelId = keyof typeof panelRegistry; + +export const toolbarPanelOrder = ["channelList", "membersList", "chatButtons", "userArea"] as const satisfies readonly PanelId[]; + +export const settings = definePluginSettings({ + transitionMs: { + type: OptionType.SLIDER, + description: "Panel transition speed in milliseconds.", + default: 160, + markers: transitionMarkers, + }, + channelListCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the channel list as collapsed.", + default: false, + }, + membersListCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the members list as collapsed.", + default: false, + }, + chatButtonsCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the message button row as collapsed.", + default: false, + }, + userAreaCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the user area as collapsed.", + default: false, + }, +}); diff --git a/src/equicordplugins/collapsibleUi/style.css b/src/equicordplugins/collapsibleUi/style.css new file mode 100644 index 000000000..0ff9d8cc3 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/style.css @@ -0,0 +1,213 @@ +body:has(.vc-collapsible-ui-state) { + --vc-cui-chat-buttons-width: min(320px, 36vw); + --vc-cui-collapsed-size: 12px; + --vc-cui-hover-peek: 18px; + --vc-cui-hover-delay: 120ms; + --vc-cui-user-area-collapsed-width: 52px; + --vc-cui-transition: 160ms; + --vc-cui-ease: cubic-bezier(.2, 0, 0, 1); +} + +body:has(.vc-collapsible-ui-transition-0) { + --vc-cui-transition: 0ms; +} + +body:has(.vc-collapsible-ui-transition-80) { + --vc-cui-transition: 80ms; +} + +body:has(.vc-collapsible-ui-transition-120) { + --vc-cui-transition: 120ms; +} + +body:has(.vc-collapsible-ui-transition-160) { + --vc-cui-transition: 160ms; +} + +body:has(.vc-collapsible-ui-transition-200) { + --vc-cui-transition: 200ms; +} + +body:has(.vc-collapsible-ui-transition-260) { + --vc-cui-transition: 260ms; +} + +body:has(.vc-collapsible-ui-transition-320) { + --vc-cui-transition: 320ms; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] { + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] > * { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] { + width: var(--vc-cui-collapsed-size) !important; + inline-size: var(--vc-cui-collapsed-size) !important; + min-width: var(--vc-cui-collapsed-size) !important; + min-inline-size: var(--vc-cui-collapsed-size) !important; + max-width: var(--vc-cui-collapsed-size) !important; + max-inline-size: var(--vc-cui-collapsed-size) !important; + flex: 0 0 var(--vc-cui-collapsed-size) !important; + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + inline-size var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + min-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease), + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + flex-basis var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] > * { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebar_"] > [class*="panels_"] { + width: var(--vc-cui-user-area-collapsed-width) !important; + inline-size: var(--vc-cui-user-area-collapsed-width) !important; + min-width: var(--vc-cui-user-area-collapsed-width) !important; + min-inline-size: var(--vc-cui-user-area-collapsed-width) !important; + max-width: var(--vc-cui-user-area-collapsed-width) !important; + max-inline-size: var(--vc-cui-user-area-collapsed-width) !important; + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + inline-size var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + min-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease), + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) .vc-collapsible-ui-user-area-controls { + max-width: 0 !important; + min-width: 0 !important; + overflow: hidden !important; + opacity: 0; + pointer-events: none; + transition: + max-width var(--vc-cui-transition) var(--vc-cui-ease), + opacity var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) .vc-collapsible-ui-user-area-controls { + opacity: 1; + pointer-events: auto; +} + +body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"] { + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"], +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] { + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"] > :where(*), +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] > :where(*) { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] { + width: var(--vc-cui-collapsed-size) !important; + min-width: var(--vc-cui-collapsed-size) !important; + max-width: var(--vc-cui-collapsed-size) !important; +} + +body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] > :where(*) { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) [class*="sidebar_"] > [class*="panels_"] > :where(*), +body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:focus-within) [class*="sidebar_"] > [class*="panels_"] > :where(*) { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] { + height: var(--vc-cui-hover-peek) !important; + block-size: var(--vc-cui-hover-peek) !important; + min-height: var(--vc-cui-hover-peek) !important; + min-block-size: var(--vc-cui-hover-peek) !important; + max-height: var(--vc-cui-hover-peek) !important; + max-block-size: var(--vc-cui-hover-peek) !important; + overflow: hidden !important; + border-top-width: 0 !important; + padding-block: 0 !important; + transition: + height var(--vc-cui-transition) var(--vc-cui-ease), + block-size var(--vc-cui-transition) var(--vc-cui-ease), + min-height var(--vc-cui-transition) var(--vc-cui-ease), + min-block-size var(--vc-cui-transition) var(--vc-cui-ease), + max-height var(--vc-cui-transition) var(--vc-cui-ease), + max-block-size var(--vc-cui-transition) var(--vc-cui-ease), + padding var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] > :where(*) { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +.vc-collapsible-ui-chat-buttons { + display: flex; + align-items: center; + min-inline-size: 0; +} + +.vc-collapsible-ui-chat-buttons-collapsed { + min-inline-size: var(--vc-cui-hover-peek); +} + +.vc-collapsible-ui-chat-buttons-items { + display: flex; + align-items: center; + min-inline-size: 0; + max-inline-size: var(--vc-cui-chat-buttons-width); + overflow: hidden; + transition: + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + opacity var(--vc-cui-transition) var(--vc-cui-ease), + margin var(--vc-cui-transition) var(--vc-cui-ease); +} + +.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items { + max-inline-size: 0; + opacity: 0; + pointer-events: none; +} + +.vc-collapsible-ui-chat-buttons-collapsed:hover .vc-collapsible-ui-chat-buttons-items { + max-inline-size: var(--vc-cui-chat-buttons-width); + opacity: 1; + pointer-events: auto; + transition-delay: var(--vc-cui-hover-delay); +} + +@media (prefers-reduced-motion: reduce) { + body:has(.vc-collapsible-ui-state) { + --vc-cui-transition: 0ms; + } +} diff --git a/src/equicordplugins/hideChatButtons/index.tsx b/src/equicordplugins/hideChatButtons/index.tsx index 9bdbfc98e..5f19fe715 100644 --- a/src/equicordplugins/hideChatButtons/index.tsx +++ b/src/equicordplugins/hideChatButtons/index.tsx @@ -74,7 +74,7 @@ function ButtonsInnerComponent({ buttons }: { buttons: ReactNode[]; }) { export default definePlugin({ name: "HideChatButtons", - description: "able to hide the chat buttons", + description: "Able to hide the chat buttons", settings: settings, authors: [EquicordDevs.iamme], patches: [ diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 7afb56fa9..a9824e3dc 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -256,9 +256,11 @@ export async function deleteMessagesBulkIDB(message_ids: string[]) { message_ids.forEach(id => cachedMessages.delete(id)); } -export async function clearMessagesIDB() { - await db.clear("messages"); +export async function clearMessagesIDB(showToast = true) { cachedMessages.clear(); + await db.clear("messages"); + if (!showToast) return; + Toasts.show({ type: Toasts.Type.MESSAGE, message: "Cleared message log database and cache.", diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts index 66bc716ac..c1d41fbfe 100644 --- a/src/plugins/ircColors/index.ts +++ b/src/plugins/ircColors/index.ts @@ -21,8 +21,7 @@ import { getCustomColorString } from "@equicordplugins/customUserColors"; import { hash as h64 } from "@intrnl/xxhash64"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { useMemo } from "@webpack/common"; -import { UserStore } from "@webpack/common"; +import { useMemo, UserStore } from "@webpack/common"; // Calculate a CSS color string based on the user ID function calculateNameColorForUser(id?: string) { @@ -139,7 +138,7 @@ export default definePlugin({ } } } - + return dmColor; }, From faedaaf88ca931d299786ca5e214eb510f82e36e Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:35:33 -0400 Subject: [PATCH 13/19] use node 24 for workflows --- .github/workflows/build.yml | 4 ++-- .github/workflows/buildBranches.yml | 4 ++-- .github/workflows/nixosBuild.yml | 4 ++-- .github/workflows/publish.yml | 4 ++-- .github/workflows/reportBrokenPlugins.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13af317c6..bd826a2eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,10 +23,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 22 + - name: Use Node.js 24 uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/buildBranches.yml b/.github/workflows/buildBranches.yml index e79258ea6..46a3c4e73 100644 --- a/.github/workflows/buildBranches.yml +++ b/.github/workflows/buildBranches.yml @@ -38,11 +38,11 @@ jobs: - uses: pnpm/action-setup@v3 if: github.ref_name != 'bun' - - name: Use Node.js 22 + - name: Use Node.js 24 if: github.ref_name != 'bun' uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index 4c70f85ac..bd2c85c8a 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -20,10 +20,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 22 + - name: Use Node.js 24 uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9c2523eab..694368312 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,11 +62,11 @@ jobs: if: steps.check.outputs.release == 'true' uses: pnpm/action-setup@v3 - - name: Use Node.js 22 + - name: Use Node.js 24 if: steps.check.outputs.release == 'true' uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 9eec2e800..ef0225a1f 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -33,10 +33,10 @@ jobs: - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 22 + - name: Use Node.js 24 uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6ae47688..21d7e8b6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,10 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 22 + - name: Use Node.js 24 uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: "pnpm" - name: Install dependencies From 961670eb1e4069bf731d2715a2c661322244a297 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:38:18 -0400 Subject: [PATCH 14/19] Build Branches: Fix Bun Targeting --- .github/workflows/buildBranches.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/buildBranches.yml b/.github/workflows/buildBranches.yml index 46a3c4e73..f4e98b2ac 100644 --- a/.github/workflows/buildBranches.yml +++ b/.github/workflows/buildBranches.yml @@ -1,4 +1,4 @@ -name: Build +name: Build Branches on: push: @@ -65,9 +65,8 @@ jobs: pnpm generateVencordPluginJson dist/vencordplugins.json pnpm generateDevsList dist/devs.json - - uses: actions/checkout@v4 - if: github.ref_name == 'bun' - uses: oven-sh/setup-bun@v2 + if: github.ref_name == 'bun' with: bun-version: latest - name: Install dependencies From 9070e4ea39bfe77dcebfb0c457214515db03645a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:41:09 -0400 Subject: [PATCH 15/19] Fix Matches --- src/equicordplugins/betterActivities/index.tsx | 2 +- src/webpack/common/components.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index bea8c1136..edb29a7c8 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -45,7 +45,7 @@ export default definePlugin({ // Show all activities in the user popout/sidebar find: /onOpenUserProfileModal:\i,onClose:\i\}\),null/, replacement: { - match: /((\i)=.{0,10}(\i)\.id\).*?,onInteraction:\i\}\),).{0,250}onClose:\i\}\)/, + match: /((\i)=.{0,10}(\i)\.id\).*?,onOpenUserProfileModal:\i\}\),).{0,250}onClose:\i\}\)/, replace: "$1$self.showAllActivitiesComponent({ activity: $2, user: $3 })" }, predicate: () => settings.store.userPopout diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 2a1ca4efb..a5723c001 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -58,8 +58,8 @@ export const TooltipContainer = TooltipContainerComponent as never; export const TextInput = waitForComponent("TextInput", filters.componentByCode("#{intl::MAXIMUM_LENGTH_ERROR}", '"input"')); export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); -export const Select = waitForComponent("Select", filters.componentByCode('"Select"')); -export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('"SearchableSelect"')); +export const Select = waitForComponent("Select", filters.componentByCode('selectionMode:"single",onSelectionChange:', "isSelected:")); +export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('?"multiple":"single"')); export const Slider = waitForComponent("Slider", filters.componentByCode("markDash", "this.renderMark(")); export const Popout = waitForComponent("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,")); export const Dialog = waitForComponent("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1')); From 6451387b0667b17f033b652e10fbd846b038c7d7 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:15:24 -0400 Subject: [PATCH 16/19] Fix Patches --- src/equicordplugins/dragify/index.tsx | 2 +- src/equicordplugins/questify/index.tsx | 2 +- src/equicordplugins/wallpaperFree/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/equicordplugins/dragify/index.tsx b/src/equicordplugins/dragify/index.tsx index ea819273e..d076ea7a7 100644 --- a/src/equicordplugins/dragify/index.tsx +++ b/src/equicordplugins/dragify/index.tsx @@ -155,7 +155,7 @@ export default definePlugin({ patches: [ // Voice user rows (voice channel sidebar list) { - find: '"system:click_outside","user:escape"', + find: ".VOICE_USER],shouldShow:", replacement: { match: /(?<=GuildChannelUserContextMenu.{0,150})"data-dnd-name":\i/, replace: "\"data-dragify-user\":!0,\"data-user-id\":arguments[0].user?.id,draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].user),$&" diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 616f3ab16..cf0d1c4ab 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -1447,7 +1447,7 @@ export default definePlugin({ group: true, replacement: [ { - match: /(?<=\i\)&&"xs"===\i;)/, + match: /(?<=null&&"xs"===\i;)/, replace: "const shouldHideMembersListActivelyPlayingIcon=$self.shouldHideMembersListActivelyPlayingIcon();" }, { diff --git a/src/equicordplugins/wallpaperFree/index.tsx b/src/equicordplugins/wallpaperFree/index.tsx index 9545ef8e7..cfc8fe99e 100644 --- a/src/equicordplugins/wallpaperFree/index.tsx +++ b/src/equicordplugins/wallpaperFree/index.tsx @@ -39,7 +39,7 @@ export default definePlugin({ group: true, replacement: [ { - match: /return.{1,150},(?=keyboardModeEnabled)/, + match: /return.{1,200},(?=keyboardModeEnabled)/, replace: "const vcWallpaperFreeUrl=$self.WallpaperState(arguments[0].channel);$&vcWallpaperFreeUrl," }, { From 41a46cc06b503e701028af1b8873d321ad14ff99 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:15:49 -0400 Subject: [PATCH 17/19] Remove NoDefaultHangStatus --- src/plugins/noDefaultHangStatus/README.md | 5 ----- src/plugins/noDefaultHangStatus/index.ts | 26 ----------------------- 2 files changed, 31 deletions(-) delete mode 100644 src/plugins/noDefaultHangStatus/README.md delete mode 100644 src/plugins/noDefaultHangStatus/index.ts diff --git a/src/plugins/noDefaultHangStatus/README.md b/src/plugins/noDefaultHangStatus/README.md deleted file mode 100644 index e6bc9f83a..000000000 --- a/src/plugins/noDefaultHangStatus/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# NoDefaultHangStatus - -Disable the default hang status when joining voice channels - -![Visualization](https://github.com/Vendicated/Vencord/assets/24937357/329a9742-236f-48f7-94ff-c3510eca505a) diff --git a/src/plugins/noDefaultHangStatus/index.ts b/src/plugins/noDefaultHangStatus/index.ts deleted file mode 100644 index 1301f0d25..000000000 --- a/src/plugins/noDefaultHangStatus/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { migrateSettingToPlugin } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -migrateSettingToPlugin("NoDefaultHangStatus", "EquicordHelper", "noDefaultHangStatus"); -export default definePlugin({ - name: "NoDefaultHangStatus", - description: "Disable the default hang status when joining voice channels", - authors: [Devs.D3SOX], - - patches: [ - { - find: ".CHILLING)", - replacement: { - match: /{enableHangStatus:(\i),/, - replace: "{_enableHangStatus:$1=false," - } - } - ] -}); From ecf6a37e1ec09d58cfd132d182c7f58e8dfba3b5 Mon Sep 17 00:00:00 2001 From: Key Date: Wed, 15 Apr 2026 06:41:36 +0100 Subject: [PATCH 18/19] FileUpload: add option to preserve original filename (#972) * feat(FileUpload): add option to preserve original filename * bleh --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/fileUpload/settings.tsx | 13 +++++++++++++ src/equicordplugins/fileUpload/utils/upload.ts | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx index b1aceb041..ab697c3fa 100644 --- a/src/equicordplugins/fileUpload/settings.tsx +++ b/src/equicordplugins/fileUpload/settings.tsx @@ -209,6 +209,12 @@ export const settings = definePluginSettings({ default: false, hidden: true }, + preserveOriginalFilename: { + type: OptionType.BOOLEAN, + description: "Preserve the original filename when uploading.", + default: true, + hidden: true + }, autoCopy: { type: OptionType.BOOLEAN, description: "Auto copy upload URL", @@ -508,6 +514,13 @@ export function SettingsComponent() { /> + + (store as { preserveOriginalFilename?: boolean; }).preserveOriginalFilename = v} + /> + + Date: Wed, 15 Apr 2026 08:58:42 -0400 Subject: [PATCH 19/19] ViewIcons: Fix Patch --- src/plugins/viewIcons/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 5dc2d0eda..573757bbf 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -196,7 +196,7 @@ export default definePlugin({ patches: [ // Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp { - find: "imageClassName:null!=", + find: ".AVATAR),{trackUserProfileAction:", replacement: { match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",.{0,100}className:\i,/, replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",