diff --git a/browser/VencordNativeStub.ts b/browser/VencordNativeStub.ts index 70978f31b..861aa00ca 100644 --- a/browser/VencordNativeStub.ts +++ b/browser/VencordNativeStub.ts @@ -27,7 +27,7 @@ import type { Settings } from "@api/Settings"; import { debounce } from "@shared/debounce"; import { localStorage } from "@utils/localStorage"; import { getStylusWebStoreUrl } from "@utils/web"; -import { EXTENSION_BASE_URL } from "@utils/web-metadata"; +import { EXTENSION_BASE_URL, metaReady, RENDERER_CSS_URL } from "@utils/web-metadata"; // listeners for ipc.on const cssListeners = new Set<(css: string) => void>(); @@ -55,7 +55,18 @@ window.VencordNative = { native: { getVersions: () => ({}), - openExternal: async (url) => void open(url, "_blank") + openExternal: async (url) => void open(url, "_blank"), + getRendererCss: async () => { + if (IS_USERSCRIPT) + // need to wait for next tick for _vcUserScriptRendererCss to be set + return Promise.resolve().then(() => window._vcUserScriptRendererCss); + + await metaReady; + + return fetch(RENDERER_CSS_URL) + .then(res => res.text()); + }, + onRendererCssUpdate: NOOP, }, updater: { @@ -92,18 +103,20 @@ window.VencordNative = { return; } - const { getTheme, Theme } = require("@utils/discord"); - win.baseUrl = EXTENSION_BASE_URL; win.setCss = setCssDebounced; win.getCurrentCss = () => VencordNative.quickCss.get(); - win.getTheme = () => - getTheme() === Theme.Light - ? "vs-light" - : "vs-dark"; + win.getTheme = this.getEditorTheme; win.document.write(monacoHtmlLocal); }, + getEditorTheme: () => { + const { getTheme, Theme } = require("@utils/discord"); + + return getTheme() === Theme.Light + ? "vs-light" + : "vs-dark"; + } }, settings: { diff --git a/browser/background.js b/browser/background.js index 1f2d5ec17..2489f8238 100644 --- a/browser/background.js +++ b/browser/background.js @@ -12,7 +12,7 @@ chrome.webRequest.onHeadersReceived.addListener( ({ responseHeaders, type, url }) => { if (!responseHeaders) return; - if (type === "main_frame") { + if (type === "main_frame" && url.includes("discord.com")) { // In main frame requests, the CSP needs to be removed to enable fetching of custom css // as desired by the user removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy"); diff --git a/browser/content.js b/browser/content.js index cd1f59793..8329cfa4d 100644 --- a/browser/content.js +++ b/browser/content.js @@ -2,20 +2,15 @@ if (typeof browser === "undefined") { var browser = chrome; } -const style = document.createElement("link"); -style.type = "text/css"; -style.rel = "stylesheet"; -style.href = browser.runtime.getURL("dist/Equicord.css"); - document.addEventListener( "DOMContentLoaded", () => { - document.body.insertAdjacentElement("afterend", style); window.postMessage({ type: "vencord:meta", meta: { EXTENSION_VERSION: browser.runtime.getManifest().version, EXTENSION_BASE_URL: browser.runtime.getURL(""), + RENDERER_CSS_URL: browser.runtime.getURL("dist/Equicord.css"), } }); }, diff --git a/browser/modifyResponseHeaders.json b/browser/modifyResponseHeaders.json index 5b0b3e376..ffe34adda 100644 --- a/browser/modifyResponseHeaders.json +++ b/browser/modifyResponseHeaders.json @@ -15,7 +15,8 @@ ] }, "condition": { - "resourceTypes": ["main_frame", "sub_frame"] + "resourceTypes": ["main_frame", "sub_frame"], + "urlFilter": "||discord.com^" } }, { @@ -32,7 +33,7 @@ }, "condition": { "resourceTypes": ["stylesheet"], - "urlFilter": "https://raw.githubusercontent.com/*" + "urlFilter": "||raw.githubusercontent.com^" } } ] diff --git a/eslint.config.mjs b/eslint.config.mjs index 544de86aa..eb26d3c5d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,13 +5,14 @@ */ import stylistic from "@stylistic/eslint-plugin"; +import { defineConfig } from "eslint/config"; import react from "eslint-plugin-react"; import header from "eslint-plugin-simple-header"; import simpleImportSort from "eslint-plugin-simple-import-sort"; import unusedImports from "eslint-plugin-unused-imports"; import tseslint from "typescript-eslint"; -export default tseslint.config( +export default defineConfig( { ignores: ["dist", "browser", "packages/vencord-types"] }, { files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"], @@ -86,6 +87,7 @@ export default tseslint.config( "@stylistic/object-curly-spacing": ["error", "always"], "@stylistic/spaced-comment": ["error", "always", { "markers": ["!"] }], "@stylistic/no-extra-semi": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxBOF": 0, "maxEOF": 0 }], // TS Rules "@stylistic/function-call-spacing": ["error", "never"], diff --git a/package.json b/package.json index c2850a15c..f23263e36 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.13.9.1", + "version": "1.13.11", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { diff --git a/packages/discord-types/CONTRIBUTING.md b/packages/discord-types/CONTRIBUTING.md index 3bef40926..9f2498476 100644 --- a/packages/discord-types/CONTRIBUTING.md +++ b/packages/discord-types/CONTRIBUTING.md @@ -1 +1,3 @@ -Hint: https://docs.discord.food is an incredible resource and allows you to copy paste complete enums and interfaces +# Contributing to @equicord/discord-types + + is an incredible resource and allows you to copy paste complete enums and interfaces diff --git a/packages/discord-types/README.md b/packages/discord-types/README.md index 82a98dd8d..cd3a92800 100644 --- a/packages/discord-types/README.md +++ b/packages/discord-types/README.md @@ -2,20 +2,20 @@ This package provides TypeScript types for the Webpack modules of Discord's web app. -While it was primarily created for Vencord, other client mods could also benefit from this, so it is published as a standalone package! +While it was primarily created for Equicord based on Vencord and its respective packages, other client mods could also benefit from this, so it is published as a standalone package! ## Installation ```bash -npm install -D @vencord/discord-types -yarn add -D @vencord/discord-types -pnpm add -D @vencord/discord-types +npm install -D @equicord/discord-types +yarn add -D @equicord/discord-types +pnpm add -D @equicord/discord-types ``` ## Example Usage ```ts -import type { UserStore } from "@vencord/discord-types"; +import type { UserStore } from "@equicord/discord-types"; const userStore: UserStore = findStore("UserStore"); // findStore is up to you to implement, this library only provides types and no runtime code ``` @@ -23,8 +23,9 @@ const userStore: UserStore = findStore("UserStore"); // findStore is up to you t ## Enums This library also exports some const enums that you can use from Typescript code: + ```ts -import { ApplicationCommandType } from "@vencord/discord-types/enums"; +import { ApplicationCommandType } from "@equicord/discord-types/enums"; console.log(ApplicationCommandType.CHAT_INPUT); // 1 ``` diff --git a/packages/discord-types/enums/activity.ts b/packages/discord-types/enums/activity.ts index 513a65dee..bc1b2a985 100644 --- a/packages/discord-types/enums/activity.ts +++ b/packages/discord-types/enums/activity.ts @@ -9,18 +9,15 @@ export const enum ActivityType { } export const enum ActivityFlags { - INSTANCE = 1 << 0, - JOIN = 1 << 1, - /** @deprecated */ - SPECTATE = 1 << 2, - /** @deprecated */ - JOIN_REQUEST = 1 << 3, - SYNC = 1 << 4, - PLAY = 1 << 5, - PARTY_PRIVACY_FRIENDS = 1 << 6, - PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7, - EMBEDDED = 1 << 8, - CONTEXTLESS = 1 << 9 + INSTANCE = 1, + JOIN = 2, + SYNC = 16, + PLAY = 32, + PARTY_PRIVACY_FRIENDS = 64, + PARTY_PRIVACY_VOICE_CHANNEL = 128, + EMBEDDED = 256, + CONTEXTLESS = 512, + SUPPORTS_GATEWAY_ACTIVITY_ACTION_JOIN = 1024, } export const enum ActivityStatusDisplayType { @@ -28,3 +25,26 @@ export const enum ActivityStatusDisplayType { STATE = 1, DETAILS = 2 } + +export const enum OrientationLockState { + UNLOCKED = 1, + PORTRAIT = 2, + LANDSCAPE = 3, +} + +export const enum ActivityGameMode { + PLAY = 0, + SPECTATE = 1, +} + +export const enum ActivityLayout { + FOCUSED = 0, + PIP = 1, + GRID = 2, +} + +export const enum ActivityLabelType { + NONE = 0, + NEW = 1, + UPDATED = 2, +} diff --git a/packages/discord-types/enums/application.ts b/packages/discord-types/enums/application.ts new file mode 100644 index 000000000..b032a2a8f --- /dev/null +++ b/packages/discord-types/enums/application.ts @@ -0,0 +1,43 @@ +export const enum ApplicationType { + DEPRECATED_GAME = 1, + TICKETED_EVENTS = 3, + GUILD_ROLE_SUBSCRIPTIONS = 4, + GAME = 5, + NON_GAME_DETECTABLE = 6, +} + +export const enum LinkedGameType { + LINKED = 1, + OFFICIAL = 2, + NVIDIA = 3, +} + +export const enum CarouselItemType { + IMG = 1, + YOUTUBE_VIDEO = 2, + VIDEO = 3, +} + +export const enum ApplicationFlags { + EMBEDDED_RELEASED = 2, + EMBEDDED_IAP = 8, + APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 64, + GAME_PROFILE_DISABLED = 128, + CONTEXTLESS_ACTIVITY = 512, + SOCIAL_LAYER_INTEGRATION_LIMITED = 1024, + CLOUD_GAMING_DEMO = 2048, + GATEWAY_PRESENCE = 4096, + GATEWAY_PRESENCE_LIMITED = 8192, + GATEWAY_GUILD_MEMBERS = 16384, + GATEWAY_GUILD_MEMBERS_LIMITED = 32768, + EMBEDDED = 131072, + GATEWAY_MESSAGE_CONTENT = 262144, + GATEWAY_MESSAGE_CONTENT_LIMITED = 524288, + EMBEDDED_FIRST_PARTY = 1048576, + APPLICATION_COMMAND_BADGE = 8388608, + SOCIAL_LAYER_INTEGRATION = 134217728, + PROMOTED = 536870912, + PARTNER = 1073741824, + PARENT = 8589934592, + DISABLE_RELATIONSHIPS_ACCESS = 17179869184, +} diff --git a/packages/discord-types/enums/channel.ts b/packages/discord-types/enums/channel.ts index 7aae546bd..85ce11f35 100644 --- a/packages/discord-types/enums/channel.ts +++ b/packages/discord-types/enums/channel.ts @@ -1,3 +1,58 @@ +export const enum PermissionOverwriteType { + ROLE = 0, + MEMBER = 1, + OWNER = 2 +} + +export const enum PermissionOverwriteRowType { + EMPTY_STATE = 0, + ADMINISTRATOR = 1, + ROLE = 2, + OWNER = 3, + MEMBER = 4, + USER = 5, + GUILD = 6, +} + +export const enum PermissionOverwriteSectionType { + ROLES = 0, + MEMBERS = 1, + USERS = 2, + GUILDS = 3, +} + +export const enum ForumLayout { + DEFAULT = 0, + LIST = 1, + GRID = 2, +} + +export const enum SafetyWarningType { + STRANGER_DANGER = 1, + INAPPROPRIATE_CONVERSATION_TIER_1 = 2, + INAPPROPRIATE_CONVERSATION_TIER_2 = 3, + LIKELY_ATO = 4, +} + +export const enum ChannelFlags { + GUILD_FEED_REMOVED = 1, + PINNED = 2, + ACTIVE_CHANNELS_REMOVED = 4, + REQUIRE_TAG = 16, + IS_SPAM = 32, + IS_GUILD_RESOURCE_CHANNEL = 128, + CLYDE_AI = 256, + IS_SCHEDULED_FOR_DELETION = 512, + IS_MEDIA_CHANNEL = 1024, + SUMMARIES_DISABLED = 2048, + IS_ROLE_SUBSCRIPTION_TEMPLATE_PREVIEW_CHANNEL = 8192, + IS_BROADCASTING = 16384, + HIDE_MEDIA_DOWNLOAD_OPTIONS = 32768, + IS_JOIN_REQUEST_INTERVIEW_CHANNEL = 65536, + OBFUSCATED = 131072, + IS_MODERATOR_REPORT_CHANNEL = 524288, +} + export const enum ChannelType { GUILD_TEXT = 0, DM = 1, @@ -5,11 +60,100 @@ export const enum ChannelType { GROUP_DM = 3, GUILD_CATEGORY = 4, GUILD_ANNOUNCEMENT = 5, + GUILD_STORE = 6, ANNOUNCEMENT_THREAD = 10, PUBLIC_THREAD = 11, PRIVATE_THREAD = 12, GUILD_STAGE_VOICE = 13, GUILD_DIRECTORY = 14, GUILD_FORUM = 15, - GUILD_MEDIA = 16 -} \ No newline at end of file + GUILD_MEDIA = 16, + LOBBY = 17, + DM_SDK = 18, + UNKNOWN = 10000, +} + +/** + * Pre-defined Sets of channel types for grouping related channels. + * Each property is a Set containing channel type numbers. + */ +export interface ChannelTypesSets { + /** DM and GROUP_DM, channels that support calling. */ + CALLABLE: Set; + /** Channels that support text messages. */ + TEXTUAL: Set; + /** Forum and media channels, threads only, no direct messages. */ + GUILD_THREADS_ONLY: Set; + /** Channels that support stickers. */ + STICKERS: Set; + /** Channels that can be read/viewed. */ + READABLE: Set; + /** All guild channel types including threads. */ + GUILD: Set; + /** Guild channels excluding threads. */ + GUILD_CHANNEL: Set; + /** All thread types (announcement, public, private). */ + THREADS: Set; + /** DM and GROUP_DM, private/non-guild channels. */ + PRIVATE_CHANNEL: Set; + /** Announcement and public threads only. */ + PUBLIC_THREADS: Set; + /** Guild channels that can have threads. */ + GUILD_THREADED: Set; + /** Guild channels that are persisted/stored. */ + GUILD_STORED: Set; + /** Guild channels with text capability. */ + GUILD_TEXTUAL: Set; + /** Guild voice and stage channels. */ + GUILD_VOCAL: Set; + /** Thread types that support voice. */ + VOCAL_THREAD: Set; + /** All channels with voice capability. */ + VOCAL: Set; + /** Channels that support voice effects. */ + VOICE_EFFECTS: Set; + /** Guild text-only channels (no voice). */ + GUILD_TEXT_ONLY: Set; + /** Channels with character-limited names. */ + LIMITED_CHANNEL_NAME: Set; + /** Channels that support message search. */ + SEARCHABLE: Set; + /** Guild channels with user-generated content. */ + GUILD_USER_CONTENT: Set; + /** Guild channels that can have topics. */ + GUILD_TOPICAL: Set; + /** Guild channels that support webhooks. */ + GUILD_WEBHOOKS: Set; + /** Guild channels usable as system message channel. */ + GUILD_SYSTEM_CHANNEL: Set; + /** Guild channels that can have a parent category. */ + GUILD_PARENTABLE: Set; + /** Guild channels subject to auto-moderation. */ + GUILD_AUTO_MODERATED: Set; + /** Basic guild channel types for creation. */ + GUILD_BASIC: Set; + /** Guild channel types that can be created. */ + CREATEABLE_GUILD_CHANNELS: Set; + /** GROUP_DM only, multi-user DMs. */ + MULTI_USER_DMS: Set; + /** DM and GROUP_DM. */ + ALL_DMS: Set; + /** Channels that support invites. */ + INVITABLE: Set; + /** Guild channels supporting feed-featurable messages. */ + GUILD_FEED_FEATURABLE_MESSAGES: Set; + /** Channels supporting role subscriptions. */ + ROLE_SUBSCRIPTIONS: Set; + /** Channels supporting icon emojis. */ + ICON_EMOJIS: Set; + /** Channels that can be summarized. */ + SUMMARIZEABLE: Set; + /** Channels supporting content entry embeds. */ + CONTENT_ENTRY_EMBEDS: Set; + /** Channels that support polls. */ + POLLS: Set; + /** Channels that can launch activities. */ + ACTIVITY_LAUNCHABLE: Set; + /** All known channel types. */ + ALL: Set; +} diff --git a/packages/discord-types/enums/commands.ts b/packages/discord-types/enums/commands.ts index 298f9b7a3..b85e5b220 100644 --- a/packages/discord-types/enums/commands.ts +++ b/packages/discord-types/enums/commands.ts @@ -21,12 +21,23 @@ export const enum ApplicationCommandInputType { } export const enum ApplicationCommandType { + /** + * @remarks Discord API calls this just CHAT + */ CHAT_INPUT = 1, USER = 2, MESSAGE = 3, + PRIMARY_ENTRY_POINT = 4, } export const enum ApplicationIntegrationType { GUILD_INSTALL = 0, - USER_INSTALL = 1 + USER_INSTALL = 1, +} + +export const enum InteractionType { + APPLICATION_COMMAND = 2, + MESSAGE_COMPONENT = 3, + APPLICATION_COMMAND_AUTOCOMPLETE = 4, + MODAL_SUBMIT = 5, } diff --git a/packages/discord-types/enums/guild.ts b/packages/discord-types/enums/guild.ts new file mode 100644 index 000000000..fdb0bfc26 --- /dev/null +++ b/packages/discord-types/enums/guild.ts @@ -0,0 +1,103 @@ +export const enum GuildScheduledEventStatus { + SCHEDULED = 1, + ACTIVE = 2, + COMPLETED = 3, + CANCELED = 4, +} + +export const enum GuildScheduledEventEntityType { + NONE = 0, + STAGE_INSTANCE = 1, + VOICE = 2, + EXTERNAL = 3, + PRIME_TIME = 4, +} + +export const enum GuildScheduledEventPrivacyLevel { + PUBLIC = 1, + GUILD_ONLY = 2, +} + +export const enum GuildNSFWLevel { + DEFAULT = 0, + EXPLICIT = 1, + SAFE = 2, + AGE_RESTRICTED = 3, +} + +export const enum GuildVerificationLevel { + NONE = 0, + LOW = 1, + MEDIUM = 2, + HIGH = 3, + VERY_HIGH = 4, +} + +export const enum GuildExplicitContentFilter { + DISABLED = 0, + MEMBERS_WITHOUT_ROLES = 1, + ALL_MEMBERS = 2, +} + +export const enum GuildMFALevel { + NONE = 0, + ELEVATED = 1, +} + +export const enum GuildDefaultMessageNotifications { + ALL_MESSAGES = 0, + ONLY_MENTIONS = 1, +} + +export const enum GuildPremiumTier { + NONE = 0, + TIER_1 = 1, + TIER_2 = 2, + TIER_3 = 3, +} + +export const enum GuildSettingsFlags { + UNREADS_ALL_MESSAGES = 2048, + UNREADS_ONLY_MENTIONS = 4096, + OPT_IN_CHANNELS_OFF = 8192, + OPT_IN_CHANNELS_ON = 16384, +} + +export const enum SystemChannelFlags { + SUPPRESS_JOIN_NOTIFICATIONS = 1, + SUPPRESS_PREMIUM_SUBSCRIPTIONS = 2, + SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 4, + SUPPRESS_JOIN_NOTIFICATION_REPLIES = 8, + SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 16, + SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATION_REPLIES = 32, + SUPPRESS_CHANNEL_PROMPT_DEADCHAT = 128, + SUPPRESS_UGC_ADDED_NOTIFICATIONS = 256, +} + +export const enum GuildMemberFlags { + DID_REJOIN = 1, + COMPLETED_ONBOARDING = 2, + BYPASSES_VERIFICATION = 4, + STARTED_ONBOARDING = 8, + IS_GUEST = 16, + STARTED_HOME_ACTIONS = 32, + COMPLETED_HOME_ACTIONS = 64, + AUTOMOD_QUARANTINED_USERNAME_OR_GUILD_NICKNAME = 128, + AUTOMOD_QUARANTINED_BIO = 256, + DM_SETTINGS_UPSELL_ACKNOWLEDGED = 512, + AUTOMOD_QUARANTINED_SERVER_TAG = 1024, +} + +export const enum RoleFlags { + IN_PROMPT = 1, +} + +export const enum RecurrenceRuleFrequency { + YEARLY = 0, + MONTHLY = 1, + WEEKLY = 2, + DAILY = 3, + HOURLY = 4, + MINUTELY = 5, + SECONDLY = 6, +} diff --git a/packages/discord-types/enums/index.ts b/packages/discord-types/enums/index.ts index a25ff3b5d..efa3f03e9 100644 --- a/packages/discord-types/enums/index.ts +++ b/packages/discord-types/enums/index.ts @@ -1,5 +1,10 @@ export * from "./activity"; +export * from "./application"; export * from "./channel"; export * from "./commands"; +export * from "./guild"; export * from "./messages"; export * from "./misc"; +export * from "./settings"; +export * from "./user"; +export * from "./voice"; diff --git a/packages/discord-types/enums/messages.ts b/packages/discord-types/enums/messages.ts index 8aa6fbb38..557e9bb28 100644 --- a/packages/discord-types/enums/messages.ts +++ b/packages/discord-types/enums/messages.ts @@ -1,3 +1,54 @@ +export const enum ComponentType { + UNKNOWN = -1, + ACTION_ROW = 1, + BUTTON = 2, + STRING_SELECT = 3, + TEXT_INPUT = 4, + USER_SELECT = 5, + ROLE_SELECT = 6, + MENTIONABLE_SELECT = 7, + CHANNEL_SELECT = 8, + SECTION = 9, + TEXT_DISPLAY = 10, + THUMBNAIL = 11, + MEDIA_GALLERY = 12, + FILE = 13, + SEPARATOR = 14, + CONTENT_INVENTORY_ENTRY = 16, + CONTAINER = 17, + LABEL = 18, + FILE_UPLOAD = 19, + CHECKPOINT_CARD = 20, +} + +export const enum ButtonStyle { + PRIMARY = 1, + SECONDARY = 2, + SUCCESS = 3, + DESTRUCTIVE = 4, + LINK = 5, + PREMIUM = 6, +} + +export const enum MessageReferenceType { + DEFAULT = 0, + FORWARD = 1, +} + +export const enum PollLayoutType { + UNKNOWN = 0, + DEFAULT = 1, + IMAGE_ONLY_ANSWERS = 2 +} + +export const enum MessageActivityType { + JOIN = 1, + LISTEN = 3, + WATCH = 4, + JOIN_REQUEST = 5, + STREAM_REQUEST = 6, +} + export const enum StickerType { /** an official sticker in a pack */ STANDARD = 1, @@ -92,6 +143,8 @@ export const enum MessageType { * Name: PREMIUM_GUILD_SUBSCRIPTION * Rendered Content: "{author} just boosted the server{content ? " {content} times"}!" * Deletable: true + * + * @remarks Discord API calls this GUILD_BOOST */ PREMIUM_GUILD_SUBSCRIPTION = 8, /** @@ -101,6 +154,8 @@ export const enum MessageType { * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_1 * Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 1! " * Deletable: true + * + * @remarks Discord API calls this GUILD_BOOST_TIER_1 */ PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9, /** @@ -110,6 +165,8 @@ export const enum MessageType { * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 2! " * Deletable: true + * + * @remarks Discord API calls this GUILD_BOOST_TIER_2 */ PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, /** @@ -119,6 +176,8 @@ export const enum MessageType { * Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_3 * Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 3! " * Deletable: true + * + * @remarks Discord API calls this GUILD_BOOST_TIER_3 */ PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, /** @@ -130,6 +189,14 @@ export const enum MessageType { * Deletable: true */ CHANNEL_FOLLOW_ADD = 12, + /** + * A message sent when a user starts a stream in a guild. + * + * Value: 13 + * Name: GUILD_STREAM + * Deletable: true + */ + GUILD_STREAM = 13, /** * A message sent when a guild is disqualified from discovery * @@ -301,6 +368,22 @@ export const enum MessageType { * Deletable: true */ GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32, + /** + * A message sent when an integration is added to a private channel + * + * Value: 33 + * Name: PRIVATE_CHANNEL_INTEGRATION_ADDED + * Deletable: true + */ + PRIVATE_CHANNEL_INTEGRATION_ADDED = 33, + /** + * A message sent when an integration is removed from a private channel + * + * Value: 34 + * Name: PRIVATE_CHANNEL_INTEGRATION_REMOVED + * Deletable: true + */ + PRIVATE_CHANNEL_INTEGRATION_REMOVED = 34, /** * A message sent when a user gifts a premium (Nitro) referral * @@ -380,6 +463,14 @@ export const enum MessageType { * Deletable: true */ PURCHASE_NOTIFICATION = 44, + /** + * A message sent as a voice hangout invite + * + * Value: 45 + * Name: VOICE_HANGOUT_INVITE + * Deletable: true + */ + VOICE_HANGOUT_INVITE = 45, /** * A message sent when a poll is finalized * @@ -470,6 +561,22 @@ export const enum MessageType { * Deletable: true */ HD_STREAMING_UPGRADED = 55, + /** + * A message sent when a chat wallpaper is set + * + * Value: 56 + * Name: CHAT_WALLPAPER_SET + * Deletable: true + */ + CHAT_WALLPAPER_SET = 56, + /** + * A message sent when a chat wallpaper is removed + * + * Value: 57 + * Name: CHAT_WALLPAPER_REMOVED + * Deletable: true + */ + CHAT_WALLPAPER_REMOVED = 57, /** * A message sent when a user resolves a moderation report by deleting the offending message * @@ -524,6 +631,14 @@ export const enum MessageType { * Deletable: true */ EMOJI_ADDED = 63, + /** + * A message sent as a premium group invite + * + * Value: 64 + * Name: PREMIUM_GROUP_INVITE + * Deletable: true + */ + PREMIUM_GROUP_INVITE = 64, } export const enum MessageFlags { diff --git a/packages/discord-types/enums/misc.ts b/packages/discord-types/enums/misc.ts index d1419ba3e..60b2298a0 100644 --- a/packages/discord-types/enums/misc.ts +++ b/packages/discord-types/enums/misc.ts @@ -2,3 +2,53 @@ export const enum CloudUploadPlatform { REACT_NATIVE = 0, WEB = 1, } + +export const enum DraftType { + ChannelMessage = 0, + ThreadSettings = 1, + FirstThreadMessage = 2, + ApplicationLauncherCommand = 3, + Poll = 4, + SlashCommand = 5, + ForwardContextMessage = 6, + InteractionModal = 7, +} + +export const enum EmojiIntention { + REACTION = 0, + STATUS = 1, + COMMUNITY_CONTENT = 2, + CHAT = 3, + GUILD_STICKER_RELATED_EMOJI = 4, + GUILD_ROLE_BENEFIT_EMOJI = 5, + SOUNDBOARD = 6, + VOICE_CHANNEL_TOPIC = 7, + GIFT = 8, + AUTO_SUGGESTION = 9, + POLLS = 10, + PROFILE = 11, + MESSAGE_CONFETTI = 12, + GUILD_PROFILE = 13, + CHANNEL_NAME = 14, + DEFAULT_REACT_EMOJI = 15, +} + +export const enum LoadState { + NOT_LOADED = 0, + LOADING = 1, + LOADED = 2, + ERROR = 3, +} + +export const enum InviteTargetType { + STREAM = 1, + EMBEDDED_APPLICATION = 2, + ROLE_SUBSCRIPTIONS_PURCHASE = 3, +} + +export const enum PremiumType { + NONE = 0, + TIER_1 = 1, + TIER_2 = 2, + TIER_0 = 3, +} diff --git a/packages/discord-types/enums/settings.ts b/packages/discord-types/enums/settings.ts new file mode 100644 index 000000000..8e3d9769e --- /dev/null +++ b/packages/discord-types/enums/settings.ts @@ -0,0 +1,42 @@ +export const enum Theme { + UNSET = 0, + DARK = 1, + LIGHT = 2, + DARKER = 3, + MIDNIGHT = 4, +} + +export const enum UIDensity { + UNSET = 0, + COMPACT = 1, + COZY = 2, + RESPONSIVE = 3, + DEFAULT = 4, +} + +export const enum StickerAnimationSetting { + ALWAYS = 0, + ON_INTERACTION = 1, + NEVER = 2, +} + +export const enum UserNotificationSetting { + ALL_MESSAGES = 0, + ONLY_MENTIONS = 1, + NO_MESSAGES = 2, +} + +export const enum ChannelOverrideFlags { + UNREADS_ONLY_MENTIONS = 512, + UNREADS_ALL_MESSAGES = 1024, + FAVORITED = 2048, + OPT_IN_ENABLED = 4096, + NEW_FORUM_THREADS_OFF = 8192, + NEW_FORUM_THREADS_ON = 16384, +} + +export const enum NotifyHighlights { + NULL = 0, + DISABLED = 1, + ENABLED = 2, +} diff --git a/packages/discord-types/enums/user.ts b/packages/discord-types/enums/user.ts new file mode 100644 index 000000000..b3fc25a19 --- /dev/null +++ b/packages/discord-types/enums/user.ts @@ -0,0 +1,53 @@ +export const enum RelationshipType { + NONE = 0, + FRIEND = 1, + BLOCKED = 2, + /** + * @remarks Discord API calls this PENDING_INCOMING + */ + INCOMING_REQUEST = 3, + /** + * @remarks Discord API calls this PENDING_OUTGOING + */ + OUTGOING_REQUEST = 4, + IMPLICIT = 5, + SUGGESTION = 6, +} + +export enum GiftIntentType { + FRIEND_ANNIVERSARY = 0 +} + +export const enum ReadStateType { + CHANNEL = 0, + GUILD_EVENT = 1, + NOTIFICATION_CENTER = 2, + GUILD_HOME = 3, + GUILD_ONBOARDING_QUESTION = 4, + MESSAGE_REQUESTS = 5, +} + +export const enum UserFlags { + STAFF = 1, + PARTNER = 2, + HYPESQUAD = 4, + BUG_HUNTER_LEVEL_1 = 8, + MFA_SMS = 16, + PREMIUM_PROMO_DISMISSED = 32, + HYPESQUAD_ONLINE_HOUSE_1 = 64, + HYPESQUAD_ONLINE_HOUSE_2 = 128, + HYPESQUAD_ONLINE_HOUSE_3 = 256, + PREMIUM_EARLY_SUPPORTER = 512, + HAS_UNREAD_URGENT_MESSAGES = 8192, + BUG_HUNTER_LEVEL_2 = 16384, + VERIFIED_BOT = 65536, + VERIFIED_DEVELOPER = 131072, + CERTIFIED_MODERATOR = 262144, + BOT_HTTP_INTERACTIONS = 524288, + SPAMMER = 1048576, + DISABLE_PREMIUM = 2097152, + PROVISIONAL_ACCOUNT = 8388608, + QUARANTINED = 17592186044416, + COLLABORATOR = 1125899906842624, + RESTRICTED_COLLABORATOR = 2251799813685248, +} diff --git a/packages/discord-types/enums/voice.ts b/packages/discord-types/enums/voice.ts new file mode 100644 index 000000000..724066cc8 --- /dev/null +++ b/packages/discord-types/enums/voice.ts @@ -0,0 +1,50 @@ +export const enum ParticipantType { + STREAM = 0, + HIDDEN_STREAM = 1, + USER = 2, + ACTIVITY = 3, +} + +export const enum RTCPlatform { + DESKTOP = 0, + MOBILE = 1, + XBOX = 2, + PLAYSTATION = 3, +} + +export const enum VideoSourceType { + VIDEO = 0, + CAMERA_PREVIEW = 1, +} + +export const enum ConnectionStatsFlags { + TRANSPORT = 1, + OUTBOUND = 2, + INBOUND = 4, + ALL = 7, +} + +export const enum SpeakingFlags { + NONE = 0, + VOICE = 1, + SOUNDSHARE = 2, + PRIORITY = 4, + HIDDEN = 8, +} + +export const enum GoLiveQualityMode { + AUTO = 1, + FULL = 2, +} + +export const enum VoiceProcessingStateReason { + CPU_OVERUSE = 1, + FAILED = 2, + VAD_CPU_OVERUSE = 3, + INITIALIZED = 4, +} + +export const enum VideoQualityMode { + AUTO = 1, + FULL = 2, +} diff --git a/packages/discord-types/package.json b/packages/discord-types/package.json index 8c95d6749..c57ffbff2 100644 --- a/packages/discord-types/package.json +++ b/packages/discord-types/package.json @@ -1,15 +1,15 @@ { - "name": "@vencord/discord-types", - "author": "Vencord Contributors", + "name": "@equicord/discord-types", + "author": "Equicord & Vencord Contributors", "private": false, "description": "Typescript definitions for the webpack modules of the Discord Web app", - "version": "1.0.0", + "version": "1.0.2", "license": "LGPL-3.0-or-later", "types": "src/index.d.ts", "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/Vendicated/Vencord.git", + "url": "git+https://github.com/Equicord/Equicord.git", "directory": "packages/discord-types" }, "dependencies": { @@ -19,4 +19,4 @@ "peerDependencies": { "@types/react": "^19.0.10" } -} +} \ No newline at end of file diff --git a/packages/discord-types/src/common/Activity.d.ts b/packages/discord-types/src/common/Activity.d.ts index 06e91e7bd..03426a396 100644 --- a/packages/discord-types/src/common/Activity.d.ts +++ b/packages/discord-types/src/common/Activity.d.ts @@ -1,5 +1,7 @@ import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "../../enums"; +export type ActivityPlatform = "desktop" | "mobile" | "web" | "embedded" | "xbox" | "playstation" | "samsung" | "ios" | "android"; + export interface ActivityAssets { large_image?: string; large_text?: string; @@ -18,8 +20,10 @@ export interface Activity { created_at?: number; id?: string; name: string; - application_id: string; + application_id?: string; type: ActivityType; + session_id?: string; + sync_id?: string; emoji?: { animated: boolean; id: string; @@ -41,7 +45,7 @@ export interface Activity { metadata?: { button_urls?: Array; }; - platform?: string; + platform?: ActivityPlatform; party?: { id?: string; size?: [number, number]; diff --git a/packages/discord-types/src/common/Application.d.ts b/packages/discord-types/src/common/Application.d.ts index d2ec1e7ed..cd2696e12 100644 --- a/packages/discord-types/src/common/Application.d.ts +++ b/packages/discord-types/src/common/Application.d.ts @@ -1,23 +1,136 @@ +import { ActivityLabelType, ApplicationFlags, ApplicationType, CarouselItemType, LinkedGameType, OrientationLockState } from "../../enums"; +import { Guild } from "./Guild"; import { User } from "./User"; -export interface Application { +export type EmbeddedActivityPlatform = "ios" | "android" | "web"; + +export interface EmbeddedActivityPlatformConfig { + label_type: ActivityLabelType; + label_from: string | null; + label_until: string | null; + release_phase: string; + omit_badge_from_surfaces: string[]; +} + +export interface EmbeddedActivityConfig { + application_id: string; + activity_preview_video_asset_id: string | null; + supported_platforms: EmbeddedActivityPlatform[]; + default_orientation_lock_state: OrientationLockState; + tablet_default_orientation_lock_state: OrientationLockState; + requires_age_gate: boolean; + legacy_responsive_aspect_ratio: boolean; + premium_tier_requirement: number | null; + free_period_starts_at: string | null; + free_period_ends_at: string | null; + client_platform_config: Partial>; + shelf_rank: number; + has_csp_exception: boolean; + displays_advertisements: boolean; + blocked_locales: string[]; + supported_locales: string[]; +} + +export interface ApplicationExecutable { + os: "win32" | "darwin" | "linux"; + name: string; + isLauncher: boolean; +} + +export interface ApplicationThirdPartySku { + id: string; + sku: string; + distributor: string; +} + +export interface ApplicationDeveloper { id: string; name: string; - description?: string | null; - type: number | null; - icon: string | null | undefined; - is_discoverable: boolean; - is_monetized: boolean; - is_verified: boolean; - bot?: User; - deeplink_uri?: string; - flags?: number; - privacy_policy_url?: string; - terms_of_service_url?: string; - install_params?: ApplicationInstallParams; } export interface ApplicationInstallParams { permissions: string | null; scopes: string[]; } + +export interface ApplicationIntegrationTypeConfig { + oauth2InstallParams: ApplicationInstallParams; +} + +export interface ApplicationDirectoryEntry { + guild_count?: number; + detailed_description?: string; + supported_locales?: string[]; + carousel_items?: { asset_id: string; type: CarouselItemType; }[]; +} + +export interface Application { + id: string; + name: string; + icon: string | null; + description: string; + type: ApplicationType | null; + coverImage: string | null; + primarySkuId: string | undefined; + bot: User | null; + splash: string | undefined; + thirdPartySkus: ApplicationThirdPartySku[]; + isMonetized: boolean; + isVerified: boolean; + roleConnectionsVerificationUrl: string | undefined; + parentId: string | undefined; + connectionEntrypointUrl: string | undefined; + overlay: boolean; + overlayWarn: boolean; + overlayCompatibilityHook: boolean; + overlayMethods: number; + hook: boolean; + aliases: string[]; + publishers: ApplicationDeveloper[]; + developers: ApplicationDeveloper[]; + storeListingSkuId: string | undefined; + guildId: string | null; + guild: Guild | undefined; + executables: ApplicationExecutable[]; + hashes: string[]; + eulaId: string | undefined; + slug: string | undefined; + flags: ApplicationFlags; + maxParticipants: number | null; + tags: string[]; + embeddedActivityConfig: EmbeddedActivityConfig | undefined; + team: ApplicationTeam | undefined; + integrationTypesConfig: Record; + storefront_available: boolean; + termsOfServiceUrl: string | undefined; + privacyPolicyUrl: string | undefined; + isDiscoverable: boolean; + customInstallUrl: string | undefined; + installParams: ApplicationInstallParams | undefined; + directoryEntry: ApplicationDirectoryEntry | undefined; + categories: string[] | undefined; + linkedGames: ApplicationLinkedGame[] | undefined; + deepLinkUri: string | undefined; +} + +export interface ApplicationLinkedGame { + id: string; + type: LinkedGameType; + application?: Application; +} + +export interface ApplicationTeam { + id: string; + name: string; + icon: string | null; + members: ApplicationTeamMember[]; + ownerUserId: string; +} + +export interface ApplicationTeamMember { + user: User; + teamId: string; + membershipState: number; + permissions: string[]; + role: string; +} diff --git a/packages/discord-types/src/common/Channel.d.ts b/packages/discord-types/src/common/Channel.d.ts index 8742c9c43..6ae5ba895 100644 --- a/packages/discord-types/src/common/Channel.d.ts +++ b/packages/discord-types/src/common/Channel.d.ts @@ -1,84 +1,368 @@ +import { ChannelFlags, ChannelType, ForumLayout, PermissionOverwriteType, SafetyWarningType, UserFlags, VideoQualityMode } from "../../enums"; import { DiscordRecord } from "./Record"; +/** Permission overwrite for a role or member. */ +export interface PermissionOverwrite { + id: string; + /** 0 = role, 1 = member. */ + type: PermissionOverwriteType; + deny: bigint; + allow: bigint; +} + +/** Avatar decoration data for user profiles. */ +export interface AvatarDecorationData { + asset: string; + expires_at: string | null; + sku_id: string; +} + +/** Clan identity information. */ +export interface ClanData { + badge: string; + identity_enabled: boolean; + identity_guild_id: string; + tag: string; +} + +/** Display name style configuration. */ +export interface DisplayNameStyles { + colors: number[]; + effect_id: number; + font_id: number; +} + +/** Collectible item data. */ +export interface CollectibleItem { + asset: string; + expires_at: string | null; + label: string; + palette: string; + sku_id: string; +} + +/** User collectibles configuration. */ +export interface Collectibles { + nameplate?: CollectibleItem; +} + +/** Raw recipient data from API. */ +export interface RawRecipient { + id: string; + avatar: string | null; + avatar_decoration_data: AvatarDecorationData | null; + bot: boolean; + clan: ClanData | null; + collectibles: Collectibles | null; + discriminator: string; + display_name: string | null; + display_name_styles: DisplayNameStyles | null; + global_name: string | null; + primary_guild: ClanData | null; + public_flags: UserFlags; + username: string; +} + +/** Thread metadata for thread channels. */ +export interface ThreadMetadata { + archived: boolean; + autoArchiveDuration: number; + archiveTimestamp: string | undefined; + createTimestamp: string | undefined; + locked: boolean; + invitable: boolean; +} + +/** Icon emoji for channels that support it. */ +export interface ChannelIconEmoji { + id: string | null; + name: string; +} + +/** Thread member info, present when user has joined a thread. */ +export interface ThreadMember { + flags: number; + joinTimestamp: string; + muted: boolean; + muteConfig: { + end_time: string | null; + selected_time_window: number; + } | null; +} + +/** Available tag for forum/media channels. */ +export interface ForumTag { + id: string; + name: string; + emojiId: string | null; + emojiName: string | null; + moderated: boolean; + color: number | null; +} + +/** Default reaction emoji for forum/media channels. */ +export interface DefaultReactionEmoji { + emojiId: string | null; + emojiName: string; +} + +/** Linked lobby for voice channels. */ +export interface LinkedLobby { + application_id: string; + lobby_id: string; + linked_by: string; + linked_at: string; + require_application_authorization: boolean; +} + +/** Safety warning for DM spam detection. */ +export interface SafetyWarning { + id: string; + type: SafetyWarningType; + expiry: string; + dismiss_timestamp: string | null; +} + +/** Sort order for forum/media channels. */ +export type ForumSortOrder = 0 | 1; + +/** Tag setting for forum/media channels. */ +export type ForumTagSetting = "match_all" | "match_some"; + +/** Discord channel object. */ export class Channel extends DiscordRecord { constructor(channel: object); - application_id: number | undefined; - bitrate: number; + + /** Application ID for bot-created channels. */ + application_id: string | undefined; + /** Applied tag IDs. Threads in forum/media channels only. */ + appliedTags: string[] | undefined; + /** Available tags for forum/media channels. */ + availableTags: ForumTag[] | undefined; + /** Internal, use bitrate getter. Voice channels only. */ + bitrate_: number | undefined; + /** Whether blocked user warning was dismissed. */ + blockedUserWarningDismissed: boolean | undefined; + /** Default auto-archive duration for threads in minutes. Guild text channels only. */ defaultAutoArchiveDuration: number | undefined; - flags: number; + /** Default layout for forum/media channels. */ + defaultForumLayout: ForumLayout | undefined; + /** Default reaction emoji for forum/media channels. */ + defaultReactionEmoji: DefaultReactionEmoji | undefined; + /** Default sort order for forum/media channels. */ + defaultSortOrder: ForumSortOrder | undefined; + /** Default tag setting for forum/media channels. */ + defaultTagSetting: ForumTagSetting | undefined; + /** Default slowmode for new threads. Guild text channels only. */ + defaultThreadRateLimitPerUser: number | undefined; + /** Internal, use flags getter. */ + flags_: ChannelFlags; + /** Guild ID. */ guild_id: string; - icon: string; + /** User ID who purchased HD streaming. */ + hdStreamingBuyerId: string | undefined; + /** HD streaming end timestamp. */ + hdStreamingUntil: string | undefined; + /** Icon hash for group DMs. */ + icon: string | undefined; + /** Icon emoji for channels that support it. */ + iconEmoji: ChannelIconEmoji | undefined; + /** Channel snowflake ID. */ id: string; - lastMessageId: string; + /** For DMs, whether this is a message request. */ + isMessageRequest: boolean; + /** Timestamp when message request was created. */ + isMessageRequestTimestamp: string | null; + /** Whether the DM is flagged as spam. */ + isSpam: boolean; + /** ID of the last message in the channel. */ + lastMessageId: string | null; + /** ISO timestamp of the last pinned message. */ lastPinTimestamp: string | undefined; - member: unknown; + /** Linked lobby for voice channels. */ + linkedLobby: LinkedLobby | null; + /** Thread member info if user joined thread. Threads only. */ + member: ThreadMember | undefined; + /** Approximate member count. Threads only. */ memberCount: number | undefined; + /** Preview of member IDs in the thread. Threads only. */ memberIdsPreview: string[] | undefined; - memberListId: unknown; + /** Member list ID for guild channels. */ + memberListId: string | undefined; + /** Approximate message count. Threads only. */ messageCount: number | undefined; + /** Channel name. */ name: string; - nicks: Record; - nsfw: boolean; - originChannelId: unknown; - ownerId: string; + /** Custom nicknames in group DMs, keyed by user ID. */ + nicks: Record; + /** Internal, use nsfw getter. Guild channels only. */ + nsfw_: boolean; + /** Origin channel ID for crossposted messages. */ + originChannelId: string | undefined; + /** Owner ID for group DMs and threads. */ + ownerId: string | undefined; + /** Parent category or channel ID. */ parent_id: string; - permissionOverwrites: { - [role: string]: { - id: string; - type: number; - deny: bigint; - allow: bigint; - }; - }; - position: number; - rateLimitPerUser: number; - rawRecipients: { - id: string; - avatar: string; - username: string; - public_flags: number; - discriminator: string; - }[]; + /** Parent channel type for threads. */ + parentChannelThreadType: ChannelType | undefined; + /** Internal, use permissionOverwrites getter. Guild channels only. */ + permissionOverwrites_: Record; + /** Internal, use position getter. Guild channels only. */ + position_: number; + /** Internal, use rateLimitPerUser getter. */ + rateLimitPerUser_: number; + /** Raw recipient data from API. DMs and group DMs. */ + rawRecipients: RawRecipient[]; + /** Recipient flags for DMs. */ + recipientFlags: number; + /** Recipient user IDs. DMs and group DMs. */ recipients: string[]; - rtcRegion: string; - threadMetadata: { - locked: boolean; - archived: boolean; - invitable: boolean; - createTimestamp: string | undefined; - autoArchiveDuration: number; - archiveTimestamp: string | undefined; - }; - topic: string; - type: number; - userLimit: number; - videoQualityMode: undefined; + /** RTC region for voice channels, null for automatic. Voice channels only. */ + rtcRegion: string | null; + /** Safety warnings for DM spam detection. */ + safetyWarnings: SafetyWarning[]; + /** Template string. */ + template: string | undefined; + /** Theme color for group DMs. */ + themeColor: number | undefined; + /** Thread metadata. Threads only. */ + threadMetadata: ThreadMetadata | undefined; + /** Internal, use topic getter. */ + topic_: string | null; + /** Total messages sent in thread. Threads only. */ + totalMessageSent: number | undefined; + /** Channel type from ChannelType enum. */ + type: ChannelType; + /** Internal, use userLimit getter. Voice channels only. */ + userLimit_: number | undefined; + /** Channel version number. */ + version: number | undefined; + /** Video quality mode for voice channels. Voice channels only. */ + videoQualityMode: VideoQualityMode | undefined; + /** Computed access permissions for the current user. */ get accessPermissions(): bigint; - get lastActiveTimestamp(): number; + /** Bitrate in bits per second. Voice channels only. */ + get bitrate(): number; + /** Channel flags bitmask. */ + get flags(): ChannelFlags; + /** Whether HD streaming splash is active. */ + get isHDStreamSplashed(): boolean; + /** Whether the channel is marked as NSFW. */ + get nsfw(): boolean; + /** Permission overwrites keyed by role or user ID. */ + get permissionOverwrites(): Record; + /** Channel position in the channel list. */ + get position(): number; + /** Slowmode delay in seconds. */ + get rateLimitPerUser(): number; + /** Channel topic or description. */ + get topic(): string; + /** Maximum users allowed in voice channel. 0 = unlimited. */ + get userLimit(): number; - computeLurkerPermissionsAllowList(): unknown; - getApplicationId(): unknown; + /** Computes allowed permissions for lurkers. */ + computeLurkerPermissionsAllowList(): bigint | undefined; + /** Gets the application ID for bot-created channels. */ + getApplicationId(): string | undefined; + /** Gets the default layout for forum channels. */ + getDefaultLayout(): ForumLayout; + /** Gets the default sort order for forum channels. */ + getDefaultSortOrder(): ForumSortOrder; + /** Gets the default tag setting for forum channels. */ + getDefaultTagSetting(): ForumTagSetting; + /** Gets the guild ID this channel belongs to. */ getGuildId(): string; - getRecipientId(): unknown; + /** Gets the recipient user ID for DMs. */ + getRecipientId(): string | undefined; + /** Checks if the channel has a specific flag. */ hasFlag(flag: number): boolean; + /** Returns a new Channel with the given properties merged. */ + merge(props: Record): this; + /** Returns a new Channel with the given property set. */ + set(key: string, value: unknown): this; + /** Converts the channel to a plain JavaScript object. */ + toJS(): Record; + /** Whether this is an active (non-archived) thread. */ isActiveThread(): boolean; + /** Whether this is an announcement thread. */ + isAnnouncementThread(): boolean; + /** Whether this is an archived and locked thread. */ + isArchivedLockedThread(): boolean; + /** Whether this is an archived thread. */ isArchivedThread(): boolean; + /** Whether this is a category channel. */ isCategory(): boolean; + /** Whether this is a DM channel. */ isDM(): boolean; + /** Whether this is a directory channel. */ isDirectory(): boolean; + /** Whether this is a forum channel. */ isForumChannel(): boolean; + /** Whether this is a forum-like channel (forum or media). */ + isForumLikeChannel(): boolean; + /** Whether this is a forum post thread. */ + isForumPost(): boolean; + /** Whether this is a group DM. */ isGroupDM(): boolean; + /** Whether this is a stage voice channel. */ isGuildStageVoice(): boolean; - isGuildVoice(): boolean; + /** Whether this is a guild voice or stage channel. */ isGuildVocal(): boolean; + /** Whether this is a guild vocal channel or vocal thread. */ + isGuildVocalOrThread(): boolean; + /** Whether this is a guild voice channel. */ + isGuildVoice(): boolean; + /** Whether this is a guild voice channel or voice thread. */ + isGuildVoiceOrThread(): boolean; + /** Whether this channel supports listen mode. */ isListenModeCapable(): boolean; + /** Whether this is a locked thread. */ + isLockedThread(): boolean; + /** Whether this channel is managed by an integration. */ isManaged(): boolean; + /** Whether this is a media channel. */ + isMediaChannel(): boolean; + /** Whether this is a media post thread. */ + isMediaPost(): boolean; + /** Whether this is a moderator report channel. */ + isModeratorReportChannel(): boolean; + /** Whether this is a multi-user DM (group DM). */ isMultiUserDM(): boolean; + /** Whether the channel is marked as NSFW. */ isNSFW(): boolean; - isOwner(): boolean; + /** + * Checks if the given user is the channel owner. + * @param userId User ID to check. + */ + isOwner(userId: string): boolean; + /** Whether this is a private channel (DM or group DM). */ isPrivate(): boolean; + /** Whether this channel supports ringing. */ + isRingable(): boolean; + /** Whether this is a role subscription template preview channel. */ + isRoleSubscriptionTemplatePreviewChannel(): boolean; + /** Whether this channel is scheduled for deletion. */ + isScheduledForDeletion(): boolean; + /** Whether this is a system DM (from Discord). */ isSystemDM(): boolean; + /** Whether this is any type of thread. */ isThread(): boolean; + /** Whether this channel supports voice. */ isVocal(): boolean; + /** Whether this is a vocal thread. */ + isVocalThread(): boolean; + + /** + * Adds a recipient to a group DM. + * @param userId User ID to add. + * @param nick Optional nickname for the user. + * @param currentUserId Current user's ID. + */ + addRecipient(userId: string, nick?: string | null, currentUserId?: string): this; + /** + * Removes a recipient from a group DM. + * @param userId User ID to remove. + */ + removeRecipient(userId: string): this; } diff --git a/packages/discord-types/src/common/Guild.d.ts b/packages/discord-types/src/common/Guild.d.ts index 65e973ffe..e5c8f2e37 100644 --- a/packages/discord-types/src/common/Guild.d.ts +++ b/packages/discord-types/src/common/Guild.d.ts @@ -1,9 +1,18 @@ -import { Role } from './Role'; import { DiscordRecord } from './Record'; +import { + GuildDefaultMessageNotifications, + GuildExplicitContentFilter, + GuildMFALevel, + GuildNSFWLevel, + GuildPremiumTier, + GuildVerificationLevel, + PermissionOverwriteType, + SystemChannelFlags, +} from "../../enums"; // copy(Object.keys(findByProps("CREATOR_MONETIZABLE")).map(JSON.stringify).join("|")) export type GuildFeatures = - "INVITE_SPLASH" | "VIP_REGIONS" | "VANITY_URL" | "MORE_EMOJI" | "MORE_STICKERS" | "MORE_SOUNDBOARD" | "VERIFIED" | "COMMERCE" | "DISCOVERABLE" | "COMMUNITY" | "FEATURABLE" | "NEWS" | "HUB" | "PARTNERED" | "ANIMATED_ICON" | "BANNER" | "ENABLED_DISCOVERABLE_BEFORE" | "WELCOME_SCREEN_ENABLED" | "MEMBER_VERIFICATION_GATE_ENABLED" | "PREVIEW_ENABLED" | "ROLE_SUBSCRIPTIONS_ENABLED" | "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" | "CREATOR_MONETIZABLE" | "CREATOR_MONETIZABLE_PROVISIONAL" | "CREATOR_MONETIZABLE_WHITEGLOVE" | "CREATOR_MONETIZABLE_DISABLED" | "CREATOR_MONETIZABLE_RESTRICTED" | "CREATOR_STORE_PAGE" | "CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING" | "PRODUCTS_AVAILABLE_FOR_PURCHASE" | "GUILD_WEB_PAGE_VANITY_URL" | "THREADS_ENABLED" | "THREADS_ENABLED_TESTING" | "NEW_THREAD_PERMISSIONS" | "ROLE_ICONS" | "TEXT_IN_STAGE_ENABLED" | "TEXT_IN_VOICE_ENABLED" | "HAS_DIRECTORY_ENTRY" | "ANIMATED_BANNER" | "LINKED_TO_HUB" | "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT" | "GUILD_HOME_DEPRECATION_OVERRIDE" | "GUILD_HOME_TEST" | "GUILD_HOME_OVERRIDE" | "GUILD_ONBOARDING" | "GUILD_ONBOARDING_EVER_ENABLED" | "GUILD_ONBOARDING_HAS_PROMPTS" | "GUILD_SERVER_GUIDE" | "INTERNAL_EMPLOYEE_ONLY" | "AUTO_MODERATION" | "INVITES_DISABLED" | "BURST_REACTIONS" | "SOUNDBOARD" | "SHARD" | "ACTIVITY_FEED_ENABLED_BY_USER" | "ACTIVITY_FEED_DISABLED_BY_USER" | "SUMMARIES_ENABLED_GA" | "LEADERBOARD_ENABLED" | "SUMMARIES_ENABLED_BY_USER" | "SUMMARIES_OPT_OUT_EXPERIENCE" | "CHANNEL_ICON_EMOJIS_GENERATED" | "NON_COMMUNITY_RAID_ALERTS" | "RAID_ALERTS_DISABLED" | "AUTOMOD_TRIGGER_USER_PROFILE" | "ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY" | "GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE" | "CLAN" | "MEMBER_VERIFICATION_MANUAL_APPROVAL" | "FORWARDING_DISABLED" | "MEMBER_VERIFICATION_ROLLOUT_TEST" | "AUDIO_BITRATE_128_KBPS" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS" | "VIDEO_BITRATE_ENHANCED" | "MAX_FILE_SIZE_50_MB" | "MAX_FILE_SIZE_100_MB" | "GUILD_TAGS" | "ENHANCED_ROLE_COLORS" | "PREMIUM_TIER_3_OVERRIDE" | "REPORT_TO_MOD_PILOT" | "TIERLESS_BOOSTING_SYSTEM_MESSAGE"; + "ACTIVITY_FEED_DISABLED_BY_USER" | "ACTIVITY_FEED_ENABLED_BY_USER" | "AGE_VERIFICATION_LARGE_GUILD" | "ANIMATED_BANNER" | "ANIMATED_ICON" | "AUDIO_BITRATE_128_KBPS" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS" | "AUTOMOD_TRIGGER_USER_PROFILE" | "AUTO_MODERATION" | "BANNER" | "BURST_REACTIONS" | "BYPASS_SLOWMODE_PERMISSION_MIGRATION_COMPLETE" | "CHANNEL_ICON_EMOJIS_GENERATED" | "CLAN" | "COMMERCE" | "COMMUNITY" | "CREATOR_MONETIZABLE" | "CREATOR_MONETIZABLE_DISABLED" | "CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING" | "CREATOR_MONETIZABLE_PROVISIONAL" | "CREATOR_MONETIZABLE_RESTRICTED" | "CREATOR_MONETIZABLE_WHITEGLOVE" | "CREATOR_STORE_PAGE" | "DISCOVERABLE" | "ENABLED_DISCOVERABLE_BEFORE" | "ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY" | "ENHANCED_ROLE_COLORS" | "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT" | "FEATURABLE" | "FORWARDING_DISABLED" | "GAME_SERVERS" | "GUILD_HOME_DEPRECATION_OVERRIDE" | "GUILD_HOME_OVERRIDE" | "GUILD_HOME_TEST" | "GUILD_ONBOARDING" | "GUILD_ONBOARDING_EVER_ENABLED" | "GUILD_ONBOARDING_HAS_PROMPTS" | "GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE" | "GUILD_SERVER_GUIDE" | "GUILD_TAGS" | "GUILD_TAGS_BADGE_PACK_FLEX" | "GUILD_TAGS_BADGE_PACK_PETS" | "GUILD_WEB_PAGE_VANITY_URL" | "HAS_DIRECTORY_ENTRY" | "HUB" | "INTERNAL_EMPLOYEE_ONLY" | "INVITES_DISABLED" | "INVITE_SPLASH" | "LINKED_TO_HUB" | "MAX_FILE_SIZE_100_MB" | "MAX_FILE_SIZE_50_MB" | "MEMBER_VERIFICATION_GATE_ENABLED" | "MEMBER_VERIFICATION_MANUAL_APPROVAL" | "MEMBER_VERIFICATION_ROLLOUT_TEST" | "MORE_EMOJI" | "MORE_SOUNDBOARD" | "MORE_STICKERS" | "NEWS" | "NEW_THREAD_PERMISSIONS" | "NON_COMMUNITY_RAID_ALERTS" | "PARTNERED" | "PIN_PERMISSION_MIGRATION_COMPLETE" | "PREMIUM_TIER_3_OVERRIDE" | "PREVIEW_ENABLED" | "PRODUCTS_AVAILABLE_FOR_PURCHASE" | "RAID_ALERTS_DISABLED" | "RELAY_ENABLED" | "REPORT_TO_MOD_PILOT" | "REPORT_TO_MOD_SURVEY" | "ROLE_ICONS" | "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" | "ROLE_SUBSCRIPTIONS_ENABLED" | "SOCIAL_LAYER_STOREFRONT" | "SOUNDBOARD" | "SUMMARIES_ENABLED_BY_USER" | "SUMMARIES_ENABLED_GA" | "SUMMARIES_OPT_OUT_EXPERIENCE" | "TEXT_IN_STAGE_ENABLED" | "TEXT_IN_VOICE_ENABLED" | "THREADS_ENABLED" | "THREADS_ENABLED_TESTING" | "TIERLESS_BOOSTING_SYSTEM_MESSAGE" | "VANITY_URL" | "VERIFIED" | "VIDEO_BITRATE_ENHANCED" | "VIP_REGIONS" | "WELCOME_SCREEN_ENABLED"; export type GuildPremiumFeatures = "ANIMATED_ICON" | "STAGE_CHANNEL_VIEWERS_150" | "ROLE_ICONS" | "GUILD_TAGS" | "BANNER" | "MAX_FILE_SIZE_50_MB" | "VIDEO_QUALITY_720_60FPS" | "STAGE_CHANNEL_VIEWERS_50" | "VIDEO_QUALITY_1080_60FPS" | "MAX_FILE_SIZE_100_MB" | "VANITY_URL" | "VIDEO_BITRATE_ENHANCED" | "STAGE_CHANNEL_VIEWERS_300" | "AUDIO_BITRATE_128_KBPS" | "ANIMATED_BANNER" | "TIERLESS_BOOSTING" | "ENHANCED_ROLE_COLORS" | "INVITE_SPLASH" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS"; @@ -11,20 +20,15 @@ export class Guild extends DiscordRecord { constructor(guild: object); afkChannelId: string | undefined; afkTimeout: number; - applicationCommandCounts: { - 0: number; - 1: number; - 2: number; - }; - application_id: unknown; + application_id: string | null; banner: string | undefined; - defaultMessageNotifications: number; + defaultMessageNotifications: GuildDefaultMessageNotifications; description: string | undefined; discoverySplash: string | undefined; - explicitContentFilter: number; + explicitContentFilter: GuildExplicitContentFilter; features: Set; homeHeader: string | undefined; - hubType: unknown; + hubType: number | null; icon: string | undefined; id: string; joinedAt: Date; @@ -32,10 +36,13 @@ export class Guild extends DiscordRecord { maxMembers: number; maxStageVideoChannelUsers: number; maxVideoChannelUsers: number; - mfaLevel: number; - moderatorReporting: unknown; + mfaLevel: GuildMFALevel; + moderatorReporting: { + moderatorReportingEnabled: boolean; + moderatorReportChannelId: string; + } | null; name: string; - nsfwLevel: number; + nsfwLevel: GuildNSFWLevel; ownerConfiguredContentLevel: number; ownerId: string; preferredLocale: string; @@ -47,18 +54,25 @@ export class Guild extends DiscordRecord { }; premiumProgressBarEnabled: boolean; premiumSubscriberCount: number; - premiumTier: 0 | 1 | 2 | 3; + premiumTier: GuildPremiumTier; profile: { badge: string | undefined; tag: string | undefined; } | undefined; publicUpdatesChannelId: string | undefined; - roles: Record; rulesChannelId: string | undefined; safetyAlertsChannelId: string | undefined; splash: string | undefined; - systemChannelFlags: number; + systemChannelFlags: SystemChannelFlags; systemChannelId: string | undefined; vanityURLCode: string | undefined; - verificationLevel: number; + verificationLevel: GuildVerificationLevel; +} + +export interface RoleOrUserPermission { + type: PermissionOverwriteType; + id?: string; + permissions?: bigint; + overwriteAllow?: bigint; + overwriteDeny?: bigint; } diff --git a/packages/discord-types/src/common/GuildMember.d.ts b/packages/discord-types/src/common/GuildMember.d.ts index 5cd476824..d3d16ccd4 100644 --- a/packages/discord-types/src/common/GuildMember.d.ts +++ b/packages/discord-types/src/common/GuildMember.d.ts @@ -1,26 +1,30 @@ +import { GuildMemberFlags } from "../../enums"; +import { Collectibles, DisplayNameStyles } from "./Channel"; + export interface GuildMember { - avatar: string | undefined; - avatarDecoration: string | undefined; - banner: string | undefined; - bio: string; + avatar: string | null | undefined; + avatarDecoration: string | null | undefined; + collectibles: Collectibles | null; colorRoleId: string | undefined; - colorString: string; + colorString: string | undefined; colorStrings: { primaryColor: string | undefined; secondaryColor: string | undefined; tertiaryColor: string | undefined; - }; - communicationDisabledUntil: string | undefined; - flags: number; - fullProfileLoadedTimestamp: number; + } | null; + communicationDisabledUntil: string | null | undefined; + displayNameStyles: DisplayNameStyles | null; + flags: GuildMemberFlags; + fullProfileLoadedTimestamp: number | undefined; guildId: string; - highestRoleId: string; - hoistRoleId: string; - iconRoleId: string; + highestRoleId: string | undefined; + hoistRoleId: string | undefined; + iconRoleId: string | undefined; isPending: boolean | undefined; joinedAt: string | undefined; - nick: string | undefined; - premiumSince: string | undefined; + nick: string | null | undefined; + premiumSince: string | null | undefined; roles: string[]; + unusualDMActivityUntil: string | undefined; userId: string; } diff --git a/packages/discord-types/src/common/Role.d.ts b/packages/discord-types/src/common/Role.d.ts index 7b0389844..1c73f53e1 100644 --- a/packages/discord-types/src/common/Role.d.ts +++ b/packages/discord-types/src/common/Role.d.ts @@ -1,3 +1,5 @@ +import { RoleFlags } from "../../enums"; + export interface Role { color: number; colorString: string | undefined; @@ -11,23 +13,25 @@ export interface Role { secondary_color: number | undefined; tertiary_color: number | undefined; }; - flags: number; + description: string | null; + flags: RoleFlags; + guildId: string; hoist: boolean; - icon: string | undefined; + icon: string | null; id: string; managed: boolean; mentionable: boolean; name: string; - originalPosition: number; permissions: bigint; position: number; - /** - * probably incomplete - */ tags: { - bot_id: string; - integration_id: string; - premium_subscriber: unknown; + bot_id?: string; + integration_id?: string; + premium_subscriber?: null; + subscription_listing_id?: string; + available_for_purchase?: null; + guild_connections?: null; } | undefined; - unicodeEmoji: string | undefined; + unicodeEmoji: string | null; + version: number | undefined; } diff --git a/packages/discord-types/src/common/User.d.ts b/packages/discord-types/src/common/User.d.ts index 06f7b4067..26047a7c6 100644 --- a/packages/discord-types/src/common/User.d.ts +++ b/packages/discord-types/src/common/User.d.ts @@ -1,66 +1,85 @@ // TODO: a lot of optional params can also be null, not just undef import { DiscordRecord } from "./Record"; +import { AvatarDecorationData, ClanData, Collectibles, DisplayNameStyles } from "./Channel"; +import { PremiumType, UserFlags } from "../../enums"; export class User extends DiscordRecord { constructor(user: object); - accentColor: number; + ageVerificationStatus: number; avatar: string; + avatarDecorationData: AvatarDecorationData | null; banner: string | null | undefined; - bio: string; bot: boolean; + collectibles: Collectibles | null; desktop: boolean; discriminator: string; + displayNameStyles: DisplayNameStyles | null; email: string | undefined; - flags: number; + flags: UserFlags; globalName: string | undefined; guildMemberAvatars: Record; + hasBouncedEmail: boolean; id: string; mfaEnabled: boolean; mobile: boolean; nsfwAllowed: boolean | undefined; + personalConnectionId: string | null; phone: string | undefined; - premiumType: number | undefined; + premiumState: { subscriptionId: string; } | null; + premiumType: PremiumType | undefined; premiumUsageFlags: number; - publicFlags: number; + primaryGuild: ClanData | null; + publicFlags: UserFlags; purchasedFlags: number; system: boolean; username: string; verified: boolean; + get avatarDecoration(): AvatarDecorationData | null; get createdAt(): Date; - get hasPremiumPerks(): boolean; + get isProvisional(): boolean; + get nameplate(): { asset: string; sku_id: string; } | null; + get premiumGroupRole(): { id: string; name: string; color: number; } | null; get tag(): string; - get usernameNormalized(): string; - addGuildAvatarHash(guildId: string, avatarHash: string): User; - getAvatarSource(guildId: string, canAnimate?: boolean): { uri: string; }; - getAvatarURL(guildId?: string | null, t?: unknown, canAnimate?: boolean): string; + addGuildAvatarHash(guildId: string, avatarHash: string): this; + getAvatarSource(guildId: string): { uri: string; }; + getAvatarURL(guildId?: string | null, size?: number, canAnimate?: boolean, format?: string): string; + hadPremiumSubscription(): boolean; hasAvatarForGuild(guildId: string): boolean; - hasDisabledPremium(): boolean; - hasFlag(flag: number): boolean; + hasFlag(flag: UserFlags): boolean; hasFreePremium(): boolean; - hasHadSKU(e: unknown): boolean; + hasHadPremium(): boolean; + hasHadSKU(sku: string): boolean; hasPremiumUsageFlag(flag: number): boolean; hasPurchasedFlag(flag: number): boolean; + hasUniqueUsername(): boolean; hasUrgentMessages(): boolean; + hasVerifiedEmailOrPhone(): boolean; isClaimed(): boolean; + isFractionalPremiumWithNoSubscription(): boolean; isLocalBot(): boolean; isNonUserBot(): boolean; isPhoneVerified(): boolean; - isStaff(): boolean; + isPremiumGroupMember(): boolean; + isPremiumGroupPrimary(): boolean; + isPremiumWithFractionalPremiumOnly(): boolean; + isPremiumWithPremiumGroup(): boolean; isSystemUser(): boolean; isVerifiedBot(): boolean; - removeGuildAvatarHash(guildId: string): User; + removeGuildAvatarHash(guildId: string): this; + toJS(): object; toString(): string; + update(props: Partial): this; } export interface UserJSON { avatar: string; - avatarDecoration: unknown | undefined; + avatarDecoration: AvatarDecorationData | null; discriminator: string; id: string; - publicFlags: number; + publicFlags: UserFlags; username: string; globalName: string | undefined; } diff --git a/packages/discord-types/src/common/messages/Embed.d.ts b/packages/discord-types/src/common/messages/Embed.d.ts index 4edd9ced1..1c06756f4 100644 --- a/packages/discord-types/src/common/messages/Embed.d.ts +++ b/packages/discord-types/src/common/messages/Embed.d.ts @@ -1,41 +1,74 @@ +export type EmbedType = + | "image" + | "video" + | "link" + | "article" + | "tweet" + | "rich" + | "gifv" + | "application_news" + | "auto_moderation_message" + | "auto_moderation_notification" + | "text" + | "post_preview" + | "gift" + | "safety_policy_notice" + | "safety_system_notification" + | "age_verification_system_notification" + | "voice_channel" + | "gaming_profile" + | "poll_result"; + +export interface EmbedMedia { + height: number; + width: number; + url: string; + proxyURL: string; + placeholder: string; + placeholderVersion: number; + description: string; + srcIsAnimated: boolean; + flags: number; + contentType: string; +} + +export interface EmbedField { + rawName: string; + rawValue: string; + inline: boolean; +} + export interface Embed { + id: string; + url: string; + type: EmbedType; + rawTitle: string; + rawDescription: string; + referenceId: string | undefined; + flags: number | undefined; + contentScanVersion: number; author?: { name: string; url: string; iconURL: string | undefined; iconProxyURL: string | undefined; }; - color: string; - fields: []; - id: string; - image?: { - height: number; - width: number; - url: string; - proxyURL: string; + footer?: { + text: string; + iconURL: string | undefined; + iconProxyURL: string | undefined; }; provider?: { name: string; url: string | undefined; }; - rawDescription: string; - rawTitle: string; - referenceId: unknown; - timestamp: string; - thumbnail?: { - height: number; - proxyURL: string | undefined; - url: string; - width: number; - }; - type: string; - url: string | undefined; - video?: { - height: number; - width: number; - url: string; - proxyURL: string | undefined; - }; + timestamp?: Date; + color: string; + thumbnail?: EmbedMedia; + image?: EmbedMedia; + images?: EmbedMedia[]; + video?: EmbedMedia; + fields: EmbedField[]; } export interface EmbedJSON { @@ -48,7 +81,7 @@ export interface EmbedJSON { title: string; color: string; description: string; - type: string; + type: EmbedType; url: string | undefined; provider?: { name: string; diff --git a/packages/discord-types/src/common/messages/Emoji.d.ts b/packages/discord-types/src/common/messages/Emoji.d.ts index 793e90f8e..98cfeae9c 100644 --- a/packages/discord-types/src/common/messages/Emoji.d.ts +++ b/packages/discord-types/src/common/messages/Emoji.d.ts @@ -1,42 +1,114 @@ +/** Union type for both custom (guild) emojis and unicode emojis. */ export type Emoji = CustomEmoji | UnicodeEmoji; +/** + * Custom emoji uploaded to a guild. + */ export interface CustomEmoji { + /** Discriminator for custom emojis. */ type: 1; - allNamesString: string; + /** Whether the emoji is animated (GIF). */ animated: boolean; + /** Whether the emoji is available for use. */ available: boolean; + /** Guild id this emoji belongs to. */ guildId: string; + /** Unique emoji id (snowflake). */ id: string; + /** Whether the emoji is managed by an integration (e.g. Twitch). */ managed: boolean; + /** Emoji name without colons. */ name: string; + /** Original name before any modifications. */ originalName?: string; + /** Whether the emoji requires colons to use. */ require_colons: boolean; + /** Role ids that can use this emoji (empty array means everyone). */ roles: string[]; + /** Version number, incremented when emoji is updated. */ + version?: number; } +/** + * Built-in unicode emoji. + */ export interface UnicodeEmoji { + /** Discriminator for unicode emojis. */ type: 0; - diversityChildren: Record; - emojiObject: { - names: string[]; - surrogates: string; - unicodeVersion: number; - }; + /** Skin tone variant emojis keyed by diversity surrogate code (e.g. "1f3fb" for light skin). */ + diversityChildren: Record; + /** Raw emoji data from Discord's emoji dataset. */ + emojiObject: EmojiObject; + /** Index position in the emoji list. */ index: number; + /** Unicode surrogate pair(s) for this emoji. */ surrogates: string; + /** Unique name identifier for this emoji. */ uniqueName: string; + /** Whether to render using sprite sheet. */ useSpriteSheet: boolean; + /** Original name if renamed in context. */ + originalName?: string; + /** Emoji id when used in custom emoji context. */ + id?: string; + /** Guild id when used in guild context. */ + guildId?: string; + /** Formatted string of all emoji names. */ get allNamesString(): string; - get animated(): boolean; - get defaultDiversityChild(): any; + /** Always false for unicode emojis. */ + get animated(): false; + /** Default skin tone variant or undefined if no diversity. */ + get defaultDiversityChild(): UnicodeEmoji | undefined; + /** Whether this emoji supports skin tone modifiers. */ get hasDiversity(): boolean | undefined; + /** Whether this emoji is a skin tone variant of another. */ get hasDiversityParent(): boolean | undefined; + /** Whether this emoji supports multiple diversity modifiers (e.g. handshake with two skin tones). */ get hasMultiDiversity(): boolean | undefined; + /** Whether this emoji is a multi-diversity variant of another. */ get hasMultiDiversityParent(): boolean | undefined; - get managed(): boolean; + /** Always true for unicode emojis. */ + get managed(): true; + /** Primary emoji name. */ get name(): string; + /** All names/aliases for this emoji. */ get names(): string[]; + /** Surrogate sequence with optional diversity modifier. */ get optionallyDiverseSequence(): string | undefined; + /** Unicode version when this emoji was added. */ get unicodeVersion(): number; + /** CDN url for emoji image. */ get url(): string; + /** + * Iterates over all diversity variants of this emoji. + * @param callback Function called for each diversity variant. + */ + forEachDiversity(callback: (emoji: UnicodeEmoji) => void): void; + /** + * Iterates over all names/aliases of this emoji. + * @param callback Function called for each name. + */ + forEachName(callback: (name: string) => void): void; +} + +/** + * Raw emoji data from Discord's emoji dataset. + */ +export interface EmojiObject { + /** All names/aliases for this emoji. */ + names: string[]; + /** Unicode surrogate pair(s). */ + surrogates: string; + /** Unicode version when this emoji was added. */ + unicodeVersion: number; + /** Index in the sprite sheet for rendering. */ + spriteIndex?: number; + /** Whether this emoji supports multiple skin tone modifiers. */ + hasMultiDiversity?: boolean; + /** Whether this emoji is a diversity variant with a multi-diversity parent. */ + hasMultiDiversityParent?: boolean; + /** Skin tone modifier codes for this variant (e.g. ["1f3fb"] or ["1f3fb", "1f3fc"]). */ + diversity?: string[]; + /** Sprite indices of diversity children for parent emojis. */ + diversityChildren?: number[]; } diff --git a/packages/discord-types/src/common/messages/Message.d.ts b/packages/discord-types/src/common/messages/Message.d.ts index 4a6f3e8b5..b347eecbd 100644 --- a/packages/discord-types/src/common/messages/Message.d.ts +++ b/packages/discord-types/src/common/messages/Message.d.ts @@ -2,60 +2,135 @@ import { CommandOption } from './Commands'; import { User, UserJSON } from '../User'; import { Embed, EmbedJSON } from './Embed'; import { DiscordRecord } from "../Record"; -import { ApplicationIntegrationType, MessageFlags, MessageType, StickerFormatType } from "../../../enums"; +import { ApplicationCommandOptionType, ApplicationCommandType, ApplicationIntegrationType, ButtonStyle, ComponentType, InteractionType, MessageActivityType, MessageFlags, MessageReferenceType, MessageType, PollLayoutType, StickerFormatType } from "../../../enums"; + +export type MessageState = "SENT" | "SENDING" | "SEND_FAILED"; + +export type CodedLinkType = + | "INVITE" + | "TEMPLATE" + | "BUILD_OVERRIDE" + | "MANUAL_BUILD_OVERRIDE" + | "EXPERIMENT" + | "EVENT" + | "CHANNEL_LINK" + | "APP_DIRECTORY_PROFILE" + | "APP_DIRECTORY_STOREFRONT" + | "APP_DIRECTORY_STOREFRONT_SKU" + | "APP_OAUTH2_LINK" + | "ACTIVITY_BOOKMARK" + | "EMBEDDED_ACTIVITY_INVITE" + | "GUILD_PRODUCT" + | "SERVER_SHOP" + | "SOCIAL_LAYER_STOREFRONT" + | "QUESTS_EMBED" + | "COLLECTIBLES_SHOP"; + +export interface MessageActivity { + type: MessageActivityType; + party_id?: string; +} + +export interface PollMedia { + text?: string; + emoji?: ReactionEmoji; +} + +export interface PollAnswer { + answer_id: number; + poll_media: PollMedia; +} + +export interface Poll { + question: PollMedia; + answers: PollAnswer[]; + expiry: string; + allow_multiselect: boolean; + layout_type: PollLayoutType; +} + +export interface RoleSubscriptionData { + role_subscription_listing_id: string; + tier_name: string; + total_months_subscribed: number; + is_renewal: boolean; +} + +export interface MessageComponent { + type: ComponentType; + id?: string; + custom_id?: string; + disabled?: boolean; + style?: ButtonStyle; + label?: string; + emoji?: ReactionEmoji; + url?: string; + options?: { label: string; value: string; description?: string; emoji?: ReactionEmoji; default?: boolean; }[]; + placeholder?: string; + min_values?: number; + max_values?: number; + components?: MessageComponent[]; +} /* * TODO: looks like discord has moved over to Date instead of Moment; */ export class Message extends DiscordRecord { constructor(message: object); - activity: unknown; - application: unknown; - applicationId: string | unknown; + activity: MessageActivity | null; + activityInstance: { applicationId: string; instanceId: string; } | null; + application: { id: string; name: string; icon: string | null; } | null; + applicationId: string | null; attachments: MessageAttachment[]; author: User; blocked: boolean; bot: boolean; call: { - duration: moment.Duration; - endedTimestamp: moment.Moment; + duration: number | null; + endedTimestamp: Date | null; participants: string[]; - }; + } | null; + changelogId: string | null; channel_id: string; /** * NOTE: not fully typed */ codedLinks: { code?: string; - type: string; + type: CodedLinkType; }[]; - colorString: unknown; - components: unknown[]; + colorString: string | null; + components: MessageComponent[]; content: string; - customRenderedContent: unknown; - editedTimestamp: Date; + customRenderedContent: { + content: React.ReactNode; + [key: string]: unknown; + } | null; + editedTimestamp: Date | null; embeds: Embed[]; flags: MessageFlags; giftCodes: string[]; + giftInfo: { sku_id: string; slug: string; } | null; + giftingPrompt: { recipientUserId: string; } | null; id: string; interaction: { id: string; name: string; - type: number; + type: InteractionType; user: User; - }[] | undefined; + }[] | null; interactionData: { application_command: { application_id: string; - default_member_permissions: unknown; + default_member_permissions: string | null; default_permission: boolean; description: string; - dm_permission: unknown; + dm_permission: boolean | null; id: string; name: string; options: CommandOption[]; - permissions: unknown[]; - type: number; + permissions: string[]; + type: ApplicationCommandType; version: string; }; attachments: MessageAttachment[]; @@ -63,19 +138,19 @@ export class Message extends DiscordRecord { id: string; name: string; options: { - focused: unknown; + focused: boolean | undefined; name: string; - type: number; + type: ApplicationCommandOptionType; value: string; }[]; - type: number; + type: InteractionType; version: string; - }[]; - interactionMetadata?: { + }[] | null; + interactionMetadata: { id: string; - type: number; + type: InteractionType; name?: string; - command_type?: number; + command_type?: ApplicationCommandType; ephemerality_reason?: number; user: User; authorizing_integration_owners: Record; @@ -83,16 +158,20 @@ export class Message extends DiscordRecord { interacted_message_id?: string; target_user?: User; target_message_id?: string; - }; - interactionError: unknown[]; + } | null; + interactionError: string[] | null; + ignored: boolean; isSearchHit: boolean; - loggingName: unknown; + isUnsupported: boolean; + loggingName: string | null; mentionChannels: string[]; mentionEveryone: boolean; + mentionGames: { id: string; name: string; }[]; mentionRoles: string[]; mentioned: boolean; mentions: string[]; messageReference: { + type?: MessageReferenceType; guild_id?: string; channel_id: string; message_id: string; @@ -100,18 +179,26 @@ export class Message extends DiscordRecord { messageSnapshots: { message: Message; }[]; - nick: unknown; // probably a string + nick: string | null; nonce: string | undefined; pinned: boolean; + poll: Poll | null; + potions: { id: string; expiresAt: string; }[]; + premiumGroupInviteId: string | null; + purchaseNotification: { type: number; guild_product_purchase?: { product_name: string; }; } | null; reactions: MessageReaction[]; - state: string; + referralTrialOfferId: string | null; + roleSubscriptionData: RoleSubscriptionData | null; + sharedClientTheme: { primary_color: number; secondary_color: number; } | null; + soundboardSounds: { sound_id: string; volume: number; emoji_id?: string; emoji_name?: string; }[]; + state: MessageState; stickerItems: { format_type: StickerFormatType; id: string; name: string; }[]; - stickers: unknown[]; - timestamp: moment.Moment; + stickers: { id: string; name: string; format_type: StickerFormatType; }[]; + timestamp: Date; tts: boolean; type: MessageType; webhookId: string | undefined; @@ -134,10 +221,18 @@ export class Message extends DiscordRecord { removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message; getChannelId(): string; + getContentMessage(): Message; hasFlag(flag: MessageFlags): boolean; + hasPotions(): boolean; isCommandType(): boolean; isEdited(): boolean; + isFirstMessageInForumPost(channel: unknown): boolean; + isInteractionPlaceholder(): boolean; + isPoll(): boolean; isSystemDM(): boolean; + canDeleteOwnMessage(userId: string): boolean; + userHasReactedWithEmoji(emoji: ReactionEmoji, burst?: boolean): boolean; + addReactionBatch(reactions: { emoji: ReactionEmoji; users: string[]; }[], burstReactions: { emoji: ReactionEmoji; users: string[]; }[]): Message; /** Vencord added */ deleted?: boolean; @@ -148,14 +243,14 @@ export interface MessageJSON { attachments: MessageAttachment[]; author: UserJSON; channel_id: string; - components: unknown[]; + components: MessageComponent[]; content: string; edited_timestamp: string; embeds: EmbedJSON[]; - flags: number; + flags: MessageFlags; guild_id: string | undefined; id: string; - loggingName: unknown; + loggingName: string | null; member: { avatar: string | undefined; communication_disabled_until: string | undefined; @@ -180,10 +275,10 @@ export interface MessageJSON { nonce: string | undefined; pinned: boolean; referenced_message: MessageJSON | undefined; - state: string; + state: MessageState; timestamp: string; tts: boolean; - type: number; + type: MessageType; } export interface MessageAttachment { diff --git a/packages/discord-types/src/common/messages/Sticker.d.ts b/packages/discord-types/src/common/messages/Sticker.d.ts index 744b06231..84be90158 100644 --- a/packages/discord-types/src/common/messages/Sticker.d.ts +++ b/packages/discord-types/src/common/messages/Sticker.d.ts @@ -1,7 +1,7 @@ import { StickerFormatType, StickerType } from "../../../enums"; interface BaseSticker { - asset: string; + asset?: string; available: boolean; description: string; format_type: StickerFormatType; diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index 4fb634a12..586aebef8 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -36,17 +36,19 @@ export type Button = ComponentType & { // #endregion -export type Tooltip = ComponentType<{ +export interface TooltipChildrenProps { + onClick(): void; + onMouseEnter(): void; + onMouseLeave(): void; + onContextMenu(): void; + onFocus(): void; + onBlur(): void; + "aria-label"?: string; +} + +export interface TooltipProps { text: ReactNode | ComponentType; - children: FunctionComponent<{ - onClick(): void; - onMouseEnter(): void; - onMouseLeave(): void; - onContextMenu(): void; - onFocus(): void; - onBlur(): void; - "aria-label"?: string; - }>; + children: FunctionComponent; "aria-label"?: string; allowOverflow?: boolean; @@ -63,7 +65,9 @@ export type Tooltip = ComponentType<{ tooltipClassName?: string; tooltipContentClassName?: string; -}> & { +} + +export type Tooltip = ComponentType & { Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>; }; diff --git a/packages/discord-types/src/passiveupdatestate.d.ts b/packages/discord-types/src/passiveupdatestate.d.ts index 31bed9ffa..b7f5e6a23 100644 --- a/packages/discord-types/src/passiveupdatestate.d.ts +++ b/packages/discord-types/src/passiveupdatestate.d.ts @@ -4,17 +4,20 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { GuildMemberFlags } from "../enums"; import { VoiceState } from "./stores"; +export type PassiveUpdateType = "PASSIVE_UPDATE_V2"; + export interface PassiveUpdateState { - type: string; + type: PassiveUpdateType; guildId: string; members?: ({ user: { avatar: null | string; communication_disabled_until: null | string; deaf: boolean; - flags: number; + flags: GuildMemberFlags; joined_at: string; mute: boolean; nick: string; @@ -27,7 +30,7 @@ export interface PassiveUpdateState { nick: string | null; mute: boolean; joined_at: string; - flags: number; + flags: GuildMemberFlags; deaf: boolean; communication_disabled_until: null | string; avatar: null | string; diff --git a/packages/discord-types/src/stores/AccessibilityStore.d.ts b/packages/discord-types/src/stores/AccessibilityStore.d.ts new file mode 100644 index 000000000..cf0248962 --- /dev/null +++ b/packages/discord-types/src/stores/AccessibilityStore.d.ts @@ -0,0 +1,74 @@ +import { FluxStore } from ".."; + +export type ReducedMotionPreference = "auto" | "reduce" | "no-preference"; +export type ForcedColorsPreference = "none" | "active"; +export type ContrastPreference = "no-preference" | "more" | "less" | "custom"; +export type RoleStyle = "username" | "dot" | "hidden"; +export type ContrastMode = "default" | "high"; + +export interface AccessibilityState { + fontSize: number; + zoom: number; + keyboardModeEnabled: boolean; + contrastMode: ContrastMode; + colorblindMode: boolean; + lowContrastMode: boolean; + saturation: number; + contrast: number; + desaturateUserColors: boolean; + forcedColorsModalSeen: boolean; + keyboardNavigationExplainerModalSeen: boolean; + messageGroupSpacing: number | null; + systemPrefersReducedMotion: ReducedMotionPreference; + systemPrefersCrossfades: boolean; + prefersReducedMotion: ReducedMotionPreference; + systemForcedColors: ForcedColorsPreference; + syncForcedColors: boolean; + systemPrefersContrast: ContrastPreference; + alwaysShowLinkDecorations: boolean; + roleStyle: RoleStyle; + displayNameStylesEnabled: boolean; + submitButtonEnabled: boolean; + syncProfileThemeWithUserTheme: boolean; + enableCustomCursor: boolean; + switchIconsEnabled: boolean; +} + +export class AccessibilityStore extends FluxStore { + get fontScale(): number; + get fontSize(): number; + get isFontScaledUp(): boolean; + get isFontScaledDown(): boolean; + get fontScaleClass(): string; + get zoom(): number; + get isZoomedIn(): boolean; + get isZoomedOut(): boolean; + get keyboardModeEnabled(): boolean; + get colorblindMode(): boolean; + get lowContrastMode(): boolean; + get saturation(): number; + get contrast(): number; + get desaturateUserColors(): boolean; + get forcedColorsModalSeen(): boolean; + get keyboardNavigationExplainerModalSeen(): boolean; + get messageGroupSpacing(): number; + get isMessageGroupSpacingIncreased(): boolean; + get isMessageGroupSpacingDecreased(): boolean; + get isSubmitButtonEnabled(): boolean; + get syncProfileThemeWithUserTheme(): boolean; + get systemPrefersReducedMotion(): ReducedMotionPreference; + get rawPrefersReducedMotion(): ReducedMotionPreference; + get useReducedMotion(): boolean; + get systemForcedColors(): ForcedColorsPreference; + get syncForcedColors(): boolean; + get useForcedColors(): boolean; + get systemPrefersContrast(): ContrastPreference; + get systemPrefersCrossfades(): boolean; + get alwaysShowLinkDecorations(): boolean; + get enableCustomCursor(): boolean; + get roleStyle(): RoleStyle; + get displayNameStylesEnabled(): boolean; + get isHighContrastModeEnabled(): boolean; + get isSwitchIconsEnabled(): boolean; + getUserAgnosticState(): AccessibilityState; +} diff --git a/packages/discord-types/src/stores/ActiveJoinedThreadsStore.d.ts b/packages/discord-types/src/stores/ActiveJoinedThreadsStore.d.ts new file mode 100644 index 000000000..71138a686 --- /dev/null +++ b/packages/discord-types/src/stores/ActiveJoinedThreadsStore.d.ts @@ -0,0 +1,33 @@ +import { Channel, FluxStore } from ".."; + +export interface ThreadJoined { + channel: Channel; + joinTimestamp: number; +} + +export type ThreadsForParent = Record; +export type ThreadsForGuild = Record; +export type AllActiveJoinedThreads = Record; + +export interface NewThreadCounts { + [parentChannelId: string]: number; +} + +export class ActiveJoinedThreadsStore extends FluxStore { + computeAllActiveJoinedThreads(guildId?: string | null): Channel[]; + getActiveJoinedRelevantThreadsForGuild(guildId: string): ThreadsForGuild; + getActiveJoinedRelevantThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent; + getActiveJoinedThreadsForGuild(guildId: string): ThreadsForGuild; + getActiveJoinedThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent; + getActiveJoinedUnreadThreadsForGuild(guildId: string): ThreadsForGuild; + getActiveJoinedUnreadThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent; + getActiveThreadCount(guildId: string, parentChannelId: string): number; + getActiveUnjoinedThreadsForGuild(guildId: string): ThreadsForGuild; + getActiveUnjoinedThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent; + getActiveUnjoinedUnreadThreadsForGuild(guildId: string): ThreadsForGuild; + getActiveUnjoinedUnreadThreadsForParent(guildId: string, parentChannelId: string): ThreadsForParent; + getAllActiveJoinedThreads(): AllActiveJoinedThreads; + getNewThreadCount(guildId: string, parentChannelId: string): number; + getNewThreadCountsForGuild(guildId: string): NewThreadCounts; + hasActiveJoinedUnreadThreads(guildId: string, parentChannelId: string): boolean; +} diff --git a/packages/discord-types/src/stores/ApplicationStore.d.ts b/packages/discord-types/src/stores/ApplicationStore.d.ts new file mode 100644 index 000000000..165f468b0 --- /dev/null +++ b/packages/discord-types/src/stores/ApplicationStore.d.ts @@ -0,0 +1,25 @@ +import { Application, FluxStore } from ".."; +import { ApplicationType } from "../../enums"; + +export interface ApplicationStoreState { + botUserIdToAppUsage: Record; +} + +export interface ApplicationUsage { + applicationId: string; + lastUsedMs: number; +} + +export class ApplicationStore extends FluxStore { + _getAllApplications(): Application[]; + getState(): ApplicationStoreState; + getApplication(applicationId: string): Application; + getApplicationByName(name: string): Application | undefined; + getApplicationLastUpdated(applicationId: string): number | undefined; + getGuildApplication(guildId: string, type: ApplicationType): Application | undefined; + getGuildApplicationIds(guildId: string): string[]; + getAppIdForBotUserId(botUserId: string): string | undefined; + getFetchingOrFailedFetchingIds(): string[]; + isFetchingApplication(applicationId: string): boolean; + didFetchingApplicationFail(applicationId: string): boolean; +} diff --git a/packages/discord-types/src/stores/CallStore.d.ts b/packages/discord-types/src/stores/CallStore.d.ts new file mode 100644 index 000000000..ea87ff704 --- /dev/null +++ b/packages/discord-types/src/stores/CallStore.d.ts @@ -0,0 +1,24 @@ +import { FluxStore } from ".."; + +export interface Call { + channelId: string; + messageId: string | null; + region: string | null; + ringing: string[]; + unavailable: boolean; + regionUpdated: boolean; +} + +export interface CallStoreState { + calls: Record; + enqueuedRings: Record; +} + +export class CallStore extends FluxStore { + getCall(channelId: string): Call; + getCalls(): Call[]; + getMessageId(channelId: string): string | null; + isCallActive(channelId: string, messageId?: string): boolean; + isCallUnavailable(channelId: string): boolean; + getInternalState(): CallStoreState; +} diff --git a/packages/discord-types/src/stores/ChannelRTCStore.d.ts b/packages/discord-types/src/stores/ChannelRTCStore.d.ts new file mode 100644 index 000000000..0c8271581 --- /dev/null +++ b/packages/discord-types/src/stores/ChannelRTCStore.d.ts @@ -0,0 +1,105 @@ +import { AvatarDecorationData, FluxStore, User, VoiceState } from ".."; +import { ActivityType, ParticipantType, RTCPlatform } from "../../enums"; +import { StreamType } from "./FriendsStore"; + +export type RTCLayout = "normal" | "minimum" | "no-chat" | "full-screen" | "haven"; +export type RTCMode = "video" | "voice"; +export type RTCLayoutContext = "OVERLAY" | "APP" | "POPOUT" | "CALL_TILE_POPOUT"; +export type ParticipantFilterType = "VIDEO" | "STREAM" | "FILTERED" | "SPEAKING" | "ACTIVITY" | "NOT_POPPED_OUT"; + +export interface StreamResolution { + height: number; + width: number; +} + +export interface Stream { + channelId: string; + guildId: string | null; + ownerId: string; + streamType: StreamType; +} + +export interface BaseParticipant { + id: string; + type: ParticipantType; + isPoppedOut?: boolean; +} + +export interface UserParticipant extends BaseParticipant { + type: ParticipantType.USER; + user: User; + voiceState: VoiceState | null; + voicePlatform: RTCPlatform | null; + speaking: boolean; + voiceDb: number; + latched: boolean; + lastSpoke: number; + soundsharing: boolean; + ringing: boolean; + userNick: string; + userAvatarDecoration: AvatarDecorationData | null; + localVideoDisabled: boolean; + userVideo?: boolean; + streamId?: string; +} + +export interface StreamParticipant extends BaseParticipant { + type: ParticipantType.STREAM | ParticipantType.HIDDEN_STREAM; + user: User; + userNick: string; + userVideo: boolean; + stream: Stream; + maxResolution?: StreamResolution; + maxFrameRate?: number; + streamId?: string; +} + +export interface ActivityParticipant extends BaseParticipant { + type: ParticipantType.ACTIVITY; + applicationId: string; + activityType: ActivityType; + activityUrl: string; + participants: string[]; + guildId: string | null; + sortKey: string; +} + +export type Participant = UserParticipant | StreamParticipant | ActivityParticipant; + +export interface SelectedParticipantStats { + view_mode_grid_duration_ms?: number; + view_mode_focus_duration_ms?: number; + view_mode_toggle_count?: number; +} + +export interface ChannelRTCState { + voiceParticipantsHidden: Record; +} + +export class ChannelRTCStore extends FluxStore { + getActivityParticipants(channelId: string): ActivityParticipant[]; + getAllChatOpen(): Record; + getChatOpen(channelId: string): boolean; + getFilteredParticipants(channelId: string): Participant[]; + getGuildRingingUsers(channelId: string): Set; + getLayout(channelId: string, context?: RTCLayoutContext): RTCLayout; + getMode(channelId: string): RTCMode; + getParticipant(channelId: string, participantId: string): Participant | null; + getParticipants(channelId: string): Participant[]; + getParticipantsListOpen(channelId: string): boolean; + getParticipantsOpen(channelId: string): boolean; + getParticipantsVersion(channelId: string): number; + getSelectedParticipant(channelId: string): Participant | null; + getSelectedParticipantId(channelId: string): string | null; + getSelectedParticipantStats(channelId: string): SelectedParticipantStats; + getSpeakingParticipants(channelId: string): UserParticipant[]; + getStageStreamSize(channelId: string): StreamResolution | undefined; + getStageVideoLimitBoostUpsellDismissed(channelId: string): boolean | undefined; + getState(): ChannelRTCState; + getStreamParticipants(channelId: string): StreamParticipant[]; + getUserParticipantCount(channelId: string): number; + getVideoParticipants(channelId: string): UserParticipant[]; + getVoiceParticipantsHidden(channelId: string): boolean; + isFullscreenInContext(): boolean; + isParticipantPoppedOut(channelId: string, participantId: string): boolean; +} diff --git a/packages/discord-types/src/stores/ChannelStore.d.ts b/packages/discord-types/src/stores/ChannelStore.d.ts index 1507ba5e7..dccd79e1a 100644 --- a/packages/discord-types/src/stores/ChannelStore.d.ts +++ b/packages/discord-types/src/stores/ChannelStore.d.ts @@ -1,24 +1,111 @@ import { Channel, FluxStore } from ".."; +/** Debug info for channel store state. */ +export interface ChannelStoreDebugInfo { + /** Guild IDs that have been loaded. */ + loadedGuildIds: string[]; + /** Guild IDs currently being loaded. */ + pendingGuildLoads: string[]; + /** String descriptions of guild channel counts. */ + guildSizes: string[]; +} + +/** + * Flux store managing all channel data including guild channels, + * threads, DMs, and group DMs. + */ export class ChannelStore extends FluxStore { + /** + * Gets a channel by ID. + * @param channelId Channel ID to look up. + */ getChannel(channelId: string): Channel; + /** + * Gets basic channel info by ID, returns undefined if not found. + * @param channelId Channel ID to look up. + */ getBasicChannel(channelId: string): Channel | undefined; + /** + * Checks if a channel exists in the store. + * @param channelId Channel ID to check. + */ hasChannel(channelId: string): boolean; + /** + * Gets all channel IDs, optionally filtered by guild. + * @param guildId Guild ID to filter by, or null/undefined for all channels. + */ getChannelIds(guildId?: string | null): string[]; + /** + * Gets basic guild channels as a mutable record. + * @param guildId Guild ID to get channels for. + * @returns Record keyed by channel ID. + */ getMutableBasicGuildChannelsForGuild(guildId: string): Record; + /** + * Gets all guild channels as a mutable record. + * @param guildId Guild ID to get channels for. + * @returns Record keyed by channel ID. + */ getMutableGuildChannelsForGuild(guildId: string): Record; + /** + * Gets all threads for a guild. + * @param guildId Guild ID to get threads for. + */ getAllThreadsForGuild(guildId: string): Channel[]; - getAllThreadsForParent(channelId: string): Channel[]; + /** + * Gets all threads under a parent channel. + * @param parentChannelId Parent channel ID. + */ + getAllThreadsForParent(parentChannelId: string): Channel[]; + /** + * Gets sorted linked channels for a guild, such as stage instances and events. + * @param guildId Guild ID. + */ + getSortedLinkedChannelsForGuild(guildId: string): Channel[]; - getDMFromUserId(userId: string): string; + /** + * Gets DM channel ID for a user. + * @param userId User ID to get DM for. + * @returns Channel ID, not the channel object. + */ + getDMFromUserId(userId: string): string | undefined; + /** + * Gets DM channel object for a user. + * @param userId User ID to get DM for. + */ getDMChannelFromUserId(userId: string): Channel | undefined; + /** Gets all user IDs that have DM channels. */ getDMUserIds(): string[]; + /** + * Gets mutable map of user ID to DM channel ID. + * @returns Record keyed by user ID. + */ getMutableDMsByUserIds(): Record; + /** + * Gets mutable map of all private channels. + * @returns Record keyed by channel ID. + */ getMutablePrivateChannels(): Record; + /** Gets private channels sorted by last message. */ getSortedPrivateChannels(): Channel[]; + /** + * Gets version number for guild channels, incremented on changes. + * @param guildId Guild ID. + */ getGuildChannelsVersion(guildId: string): number; + /** Gets version number for private channels, incremented on changes. */ getPrivateChannelsVersion(): number; + /** + * Gets initial channel state for overlay rendering. + * @returns Record keyed by channel ID. + */ getInitialOverlayState(): Record; + + /** Gets debug info about store state. */ + getDebugInfo(): ChannelStoreDebugInfo; + + /** Loads all guild and private channels from disk cache. */ + loadAllGuildAndPrivateChannelsFromDisk(): Promise; } diff --git a/packages/discord-types/src/stores/DraftStore.d.ts b/packages/discord-types/src/stores/DraftStore.d.ts index 98b34cdfb..89d50c43c 100644 --- a/packages/discord-types/src/stores/DraftStore.d.ts +++ b/packages/discord-types/src/stores/DraftStore.d.ts @@ -1,14 +1,5 @@ import { FluxStore } from ".."; - -export enum DraftType { - ChannelMessage = 0, - ThreadSettings = 1, - FirstThreadMessage = 2, - ApplicationLauncherCommand = 3, - Poll = 4, - SlashCommand = 5, - ForwardContextMessage = 6 -} +import { DraftType } from "../../enums"; export interface Draft { timestamp: number; diff --git a/packages/discord-types/src/stores/EmojiStore.d.ts b/packages/discord-types/src/stores/EmojiStore.d.ts index 93161b2ff..f22a70a13 100644 --- a/packages/discord-types/src/stores/EmojiStore.d.ts +++ b/packages/discord-types/src/stores/EmojiStore.d.ts @@ -1,57 +1,467 @@ import { Channel, CustomEmoji, Emoji, FluxStore } from ".."; +import { EmojiIntention, LoadState } from "../../enums"; -export class EmojiStore extends FluxStore { - getCustomEmojiById(id?: string | null): CustomEmoji | undefined; - getUsableCustomEmojiById(id?: string | null): CustomEmoji | undefined; - getGuilds(): Record; - getGuildEmoji(guildId?: string | null): CustomEmoji[]; - getNewlyAddedEmoji(guildId?: string | null): CustomEmoji[]; - getTopEmoji(guildId?: string | null): CustomEmoji[]; - getTopEmojisMetadata(guildId?: string | null): { - emojiIds: string[]; - topEmojisTTL: number; - }; - hasPendingUsage(): boolean; - hasUsableEmojiInAnyGuild(): boolean; - searchWithoutFetchingLatest(data: any): any; - getSearchResultsOrder(...args: any[]): any; - getState(): { - pendingUsages: { key: string, timestamp: number; }[]; - }; - searchWithoutFetchingLatest(data: { - channel: Channel; - query: string; - count?: number; - intention: number; - includeExternalGuilds?: boolean; - matchComparator?(name: string): boolean; - }): Record<"locked" | "unlocked", Emoji[]>; +/** Emoji picker category names. */ +export type EmojiCategory = + | "top guild emoji" + | "favorites" + | "recent" + | "custom" + | "people" + | "nature" + | "food" + | "activity" + | "travel" + | "objects" + | "symbols" + | "flags"; - getDisambiguatedEmojiContext(): { - backfillTopEmojis: Record; - customEmojis: Record; - emojisById: Record; - emojisByName: Record; - emoticonRegex: RegExp | null; - emoticonsByName: Record; - escapedEmoticonNames: string; - favoriteNamesAndIds?: any; - favorites?: any; - frequentlyUsed?: any; - groupedCustomEmojis: Record; - guildId?: string; - isFavoriteEmojiWithoutFetchingLatest(e: Emoji): boolean; - newlyAddedEmoji: Record; - topEmojis?: any; - unicodeAliases: Record; - get favoriteEmojisWithoutFetchingLatest(): Emoji[]; - }; +/** + * Tracks usage statistics for a single emoji to compute frecency scores. + */ +export interface EmojiUsageRecord { + /** Total number of times this emoji has been used. */ + totalUses: number; + /** Array of recent usage timestamps in milliseconds. */ + recentUses: number[]; + /** Computed frecency score combining frequency and recency, -1 when dirty. */ + frecency: number; + /** Raw score before frecency computation. */ + score: number; +} + +/** + * Options for tracking emoji usage. + */ +export interface TrackOptions { + /** Timestamp of the usage in milliseconds. */ + timestamp?: number; + /** Number of uses since last track call. */ + usesSinceLastTrack?: number; +} + +/** + * Frecency tracker for emoji usage, combines frequency and recency to rank emojis. + * Used by both regular emoji picker and reaction emoji picker. + */ +export interface EmojiFrecency { + /** True when data has been modified and needs recomputation. */ + dirty: boolean; + /** Cached array of frequently used emojis after computation. */ + _frequently: Emoji[]; + /** Maximum number of frequently used items to track (default 42). */ + numFrequentlyItems: number; + /** Maximum number of recent usage samples to keep per emoji (default 10). */ + maxSamples: number; + /** Computes bonus score for frecency calculation (returns 100). */ + computeBonus: () => number; + /** + * Computes weight multiplier based on recency index. + * Returns 100 for index <= 3, 70 for <= 15, 50 for <= 30, 30 for <= 45, 10 for <= 80. + */ + computeWeight: (index: number) => number; + /** + * Computes frecency score for an emoji. + * @param totalUses Total number of times emoji was used. + * @param score Raw score value. + * @param config Configuration for frecency calculation. + */ + computeFrecency: (totalUses: number, score: number, config: { + /** Number of recent uses to consider. */ + numOfRecentUses?: number; + /** Maximum total uses to cap at. */ + maxTotalUse?: number; + }) => number; + /** Whether to calculate max total use dynamically. */ + calculateMaxTotalUse: boolean; + /** + * Looks up an emoji by name or id. + * @param name Emoji name or id to look up. + * @returns The emoji if found. + */ + lookupKey: (name: string) => Emoji | undefined; + /** Usage history keyed by emoji name (for unicode) or id (for custom). */ + usageHistory: Record; + /** Callback invoked after frecency computation completes. */ + afterCompute: () => void; + + /** + * Overwrites the usage history with new data. + * @param history New usage history to set. + * @param pendingUsages Pending usages to track after overwriting. + */ + overwriteHistory(history: Record | null, pendingUsages?: PendingUsage[]): void; + /** Marks the frecency data as dirty, requiring recomputation. */ + markDirty(): void; + /** Returns whether the frecency data needs recomputation. */ + isDirty(): boolean; + /** + * Tracks usage of an emoji. + * @param key Emoji name or id. + * @param options Track options including timestamp. + */ + track(key: string, options?: TrackOptions): void; + /** + * Gets the usage record for an emoji, computing if dirty. + * @param key Emoji name or id. + * @returns The usage record or null if not found. + */ + getEntry(key: string): EmojiUsageRecord | null; + /** + * Gets the score for an emoji. + * @param key Emoji name or id. + * @returns The score or null if not found. + */ + getScore(key: string): number | null; + /** + * Gets the frecency for an emoji. + * @param key Emoji name or id. + * @returns The frecency or null if not found. + */ + getFrecency(key: string): number | null; + /** Recomputes frecency scores for all emojis. */ + compute(): void; + /** Gets the frequently used emojis, computing if necessary. */ + get frequently(): Emoji[]; +} + +/** + * Container for a guild's emoji collection with usability checks. + */ +export interface GuildEmojis { + /** Guild id this emoji collection belongs to. */ + id: string; + /** User id for permission checks. */ + _userId: string; + /** Internal emoji array. */ + _emojis: CustomEmoji[]; + /** Fast lookup map of emoji id to emoji. */ + _emojiMap: Record; + /** Internal emoticons array. */ + _emoticons: Emoticon[]; + /** Internal usable emojis cache. */ + _usableEmojis: CustomEmoji[]; + /** Whether user can see server subscription IAP. */ + _canSeeServerSubIAP: boolean; + /** All custom emojis in this guild. */ + get emojis(): CustomEmoji[]; + /** Custom emojis the current user can use in this guild. */ + get usableEmojis(): CustomEmoji[]; + /** Text emoticons configured for this guild. */ + get emoticons(): Emoticon[]; + /** + * Gets an emoji by id from this guild. + * @param id Emoji id to look up. + */ + getEmoji(id: string): CustomEmoji | undefined; + /** + * Gets a usable emoji by id from this guild. + * @param id Emoji id to look up. + */ + getUsableEmoji(id: string): CustomEmoji | undefined; + /** + * Checks if an emoji is usable by the current user. + * @param emoji Emoji to check. + */ + isUsable(emoji: CustomEmoji): boolean; + /** Returns array of all emoji ids in this guild. */ + emojiIds(): string[]; +} + +/** + * Text emoticon that can be converted to emoji. + */ +export interface Emoticon { + /** Names/aliases for this emoticon. */ + names: string[]; + /** The text representation (e.g. ":)" or ":D"). */ + surrogates: string; + /** Whether this emoticon should use sprite sheet rendering. */ + useSpriteSheet: boolean; +} + +/** + * Pending emoji usage waiting to be recorded. + */ +export interface PendingUsage { + /** Emoji key (name for unicode, id for custom). */ + key: string; + /** Timestamp in milliseconds when usage occurred. */ + timestamp: number; +} + +/** + * Serializable state for EmojiStore persistence. + */ +export interface EmojiStoreState { + /** Pending emoji usages not yet committed. */ + pendingUsages: PendingUsage[]; + /** Pending reaction emoji usages not yet committed. */ + emojiReactionPendingUsages: PendingUsage[]; + /** Guild ids with expanded emoji sections in picker. */ + expandedSectionsByGuildIds: Set; +} + +/** + * Context for emoji disambiguation, caching resolved emoji data for a guild context. + * Provides fast lookup of emojis without triggering data fetches. + */ +export interface DisambiguatedEmojiContext { + /** User's favorite emojis or null if not loaded. */ + favorites: Emoji[] | null; + /** Set of favorite emoji names and ids for fast lookup, or null if not loaded. */ + favoriteNamesAndIds: Set | null; + /** Top emojis for the current guild or null if not loaded. */ + topEmojis: Emoji[] | null; + /** Current guild id context or null for DMs. */ + guildId: string | null; + /** Regex-escaped emoticon names for matching. */ + escapedEmoticonNames: string; + /** All emojis with disambiguation applied (unique names). */ + disambiguatedEmoji: Emoji[]; + /** Compiled regex for matching emoticons or null if none. */ + emoticonRegex: RegExp | null; + /** Frequently used emojis or null if not loaded. */ + frequentlyUsed: Emoji[] | null; + /** Frequently used reaction emojis or null if not loaded. */ + frequentlyUsedReactionEmojis: Emoji[] | null; + /** Set of frequently used reaction emoji names and ids, or null if not loaded. */ + frequentlyUsedReactionNamesAndIds: Set | null; + /** Unicode emoji aliases keyed by alias name, maps to primary name. */ + unicodeAliases: Record; + /** Custom emojis keyed by emoji id. */ + customEmojis: Record; + /** Custom emojis grouped by guild id. */ + groupedCustomEmojis: Record; + /** Emoticons keyed by name for fast lookup. */ + emoticonsByName: Record; + /** All emojis keyed by name for fast lookup. */ + emojisByName: Record; + /** Custom emojis keyed by id for fast lookup. */ + emojisById: Record; + /** Newly added emojis grouped by guild id. */ + newlyAddedEmoji: Record; + /** + * Checks if an emoji is a favorite without triggering a fetch. + * @param emoji Emoji to check. + */ + isFavoriteEmojiWithoutFetchingLatest(emoji: Emoji): boolean; + + /** Gets favorite emojis without triggering a fetch. */ + get favoriteEmojisWithoutFetchingLatest(): Emoji[]; + /** Gets all disambiguated emojis. */ + getDisambiguatedEmoji(): Emoji[]; + /** Gets all custom emojis keyed by name. */ + getCustomEmoji(): Record; + /** Gets custom emojis grouped by guild id. */ + getGroupedCustomEmoji(): Record; + /** + * Gets an emoji by name. + * @param name Emoji name to look up. + */ + getByName(name: string): Emoji | undefined; + /** + * Gets an emoticon by name. + * @param name Emoticon name to look up. + */ + getEmoticonByName(name: string): Emoticon | undefined; + /** + * Gets an emoji by id. + * @param id Emoji id to look up. + */ + getById(id: string): Emoji | undefined; + /** + * Gets the regex for matching custom emoticons. + * @returns RegExp or null if no emoticons. + */ + getCustomEmoticonRegex(): RegExp | null; + /** Gets frequently used emojis without triggering a fetch. */ + getFrequentlyUsedEmojisWithoutFetchingLatest(): Emoji[]; + /** Rebuilds the frequently used reaction emojis cache and returns it. */ + rebuildFrequentlyUsedReactionsEmojisWithoutFetchingLatest(): { + frequentlyUsedReactionEmojis: Emoji[]; + frequentlyUsedReactionNamesAndIds: Set; + }; + /** Gets frequently used reaction emojis without triggering a fetch. */ + getFrequentlyUsedReactionEmojisWithoutFetchingLatest(): Emoji[]; + /** + * Checks if an emoji is frequently used for reactions. + * @param emoji Emoji to check. + */ + isFrequentlyUsedReactionEmojiWithoutFetchingLatest(emoji: Emoji): boolean; + /** Rebuilds the favorite emojis cache and returns it. */ + rebuildFavoriteEmojisWithoutFetchingLatest(): { + favorites: Emoji[]; + favoriteNamesAndIds: Set; + }; + /** + * Gets emojis in priority order (favorites, frequent, top) without fetching. + * @returns Array of emojis in priority order. + */ + getEmojiInPriorityOrderWithoutFetchingLatest(): Emoji[]; + /** + * Gets top emojis for a guild without triggering a fetch. + * @param guildId Guild id to get top emojis for. + */ + getTopEmojiWithoutFetchingLatest(guildId: string): Emoji[]; + /** + * Gets newly added emojis for a specific guild. + * @param guildId Guild id. + */ + getNewlyAddedEmojiForGuild(guildId: string): CustomEmoji[]; + /** Gets escaped custom emoticon names for regex matching. */ + getEscapedCustomEmoticonNames(): string; + /** + * Checks if a name matches an emoji name chain. + * @param name Name to match. + */ + nameMatchesChain(name: string): boolean; +} + +/** + * Search options for emoji search. + */ +export interface EmojiSearchOptions { + /** Channel context for permission checks. */ + channel: Channel; + /** Search query string. */ + query: string; + /** Maximum number of results to return. */ + count?: number; + /** Intention for using the emoji, affects availability filtering. */ + intention: EmojiIntention; + /** Whether to include emojis from guilds the user is not in. */ + includeExternalGuilds?: boolean; + /** Whether to only show unicode emojis in results. */ + showOnlyUnicode?: boolean; + /** + * Custom comparator for matching emoji names. + * @param name Emoji name to compare. + * @returns True if the name matches. + */ + matchComparator?(name: string): boolean; +} + +/** + * Search results split by availability. + */ +export interface EmojiSearchResults { + /** Emojis that are locked (require Nitro or permissions). */ + locked: Emoji[]; + /** Emojis that are available for use. */ + unlocked: Emoji[]; +} + +/** + * Metadata about top emojis for a guild. + */ +export interface TopEmojisMetadata { + /** Array of top emoji ids. */ + emojiIds: string[]; + /** Time-to-live for this data in milliseconds. */ + topEmojisTTL: number; +} + +/** + * Flux store managing all emoji data including custom guild emojis, + * unicode emojis, favorites, frecency, and search functionality. + */ +export class EmojiStore extends FluxStore { + /** Array of emoji category names for the picker. */ + get categories(): EmojiCategory[]; + /** + * Current skin tone modifier surrogate for emoji diversity. + * Empty string for default yellow, or skin tone modifier (🏻🏼🏽🏾🏿). + */ + get diversitySurrogate(): string; + /** Frecency tracker for emoji picker usage. */ + get emojiFrecencyWithoutFetchingLatest(): EmojiFrecency; + /** Frecency tracker for reaction emoji usage. */ + get emojiReactionFrecencyWithoutFetchingLatest(): EmojiFrecency; + /** Guild ids with expanded emoji sections in picker. */ + get expandedSectionsByGuildIds(): Set; + /** Current load state of the emoji store. */ + get loadState(): LoadState; + + /** + * Gets a custom emoji by its id. + * @param id Emoji id to look up. + * @returns The custom emoji if found. + */ + getCustomEmojiById(id?: string | null): CustomEmoji | undefined; + /** + * Gets a usable custom emoji by its id. + * @param id Emoji id to look up. + * @returns The custom emoji if found and usable by current user. + */ + getUsableCustomEmojiById(id?: string | null): CustomEmoji | undefined; + /** + * Gets all guild emoji collections keyed by guild id. + * @returns Record of guild id to GuildEmojis. + */ + getGuilds(): Record; + /** + * Gets all custom emojis for a guild. + * @param guildId Guild id to get emojis for, or null for all guilds. + * @returns Array of custom emojis. + */ + getGuildEmoji(guildId?: string | null): CustomEmoji[]; + /** + * Gets usable custom emojis for a guild. + * @param guildId Guild id to get emojis for. + * @returns Array of usable custom emojis. + */ + getUsableGuildEmoji(guildId?: string | null): CustomEmoji[]; + /** + * Gets newly added emojis for a guild. + * @param guildId Guild id to get emojis for. + * @returns Array of newly added custom emojis. + */ + getNewlyAddedEmoji(guildId?: string | null): CustomEmoji[]; + /** + * Gets top emojis for a guild based on usage. + * @param guildId Guild id to get emojis for. + * @returns Array of top custom emojis. + */ + getTopEmoji(guildId?: string | null): CustomEmoji[]; + /** + * Gets metadata about top emojis for a guild. + * @param guildId Guild id to get metadata for. + * @returns Metadata including emoji ids and TTL, or undefined if not cached. + */ + getTopEmojisMetadata(guildId?: string | null): TopEmojisMetadata | undefined; + /** + * Checks if user has any favorite emojis in a guild context. + * @param guildId Guild id to check. + * @returns True if user has favorites. + */ + hasFavoriteEmojis(guildId?: string | null): boolean; + /** + * Checks if there are pending emoji usages to be recorded. + * @returns True if there are pending usages. + */ + hasPendingUsage(): boolean; + /** + * Checks if user has any usable custom emojis in any guild. + * @returns True if user has usable emojis. + */ + hasUsableEmojiInAnyGuild(): boolean; + /** Internal method for ordering search results. */ + getSearchResultsOrder(...args: any[]): any; + /** + * Gets the serializable state for persistence. + * @returns Current store state. + */ + getState(): EmojiStoreState; + /** + * Searches for emojis without triggering data fetches. + * @param options Search options including query and filters. + * @returns Search results split by locked/unlocked. + */ + searchWithoutFetchingLatest(options: EmojiSearchOptions): EmojiSearchResults; + /** + * Gets the disambiguated emoji context for a guild. + * @param guildId Guild id to get context for, or null/undefined for global context. + */ + getDisambiguatedEmojiContext(guildId?: string | null): DisambiguatedEmojiContext; } diff --git a/packages/discord-types/src/stores/FluxStore.d.ts b/packages/discord-types/src/stores/FluxStore.d.ts index da55ac0b6..61328f521 100644 --- a/packages/discord-types/src/stores/FluxStore.d.ts +++ b/packages/discord-types/src/stores/FluxStore.d.ts @@ -1,6 +1,7 @@ import { FluxDispatcher, FluxEvents } from ".."; type Callback = () => void; +type SyncCallback = () => boolean | void; /* For some reason, this causes type errors when you try to destructure it: @@ -14,31 +15,75 @@ type Callback = () => void; export type FluxEvent = any; export type ActionHandler = (event: FluxEvent) => void; +/** keyed by FluxEvents action type */ export type ActionHandlers = Partial>; +/** + * Base class for all Discord Flux stores. + * Provides change notification, action handling, and store synchronization. + */ export class FluxStore { - constructor(dispatcher: FluxDispatcher, actionHandlers?: ActionHandlers); + /** + * @param dispatcher the FluxDispatcher instance to register with + * @param actionHandlers handlers for Flux actions, keyed by action type + * @param band priority band for action handling (default 2), lower runs first + */ + constructor(dispatcher: FluxDispatcher, actionHandlers?: ActionHandlers, band?: number); + /** returns displayName if set, otherwise constructor.name */ getName(): string; + /** adds listener to _changeCallbacks, invoked before react listeners and triggers syncWith processing */ addChangeListener(callback: Callback): void; - /** Listener will be removed once the callback returns false. */ + /** + * adds a listener that auto-removes when callback returns false. + * @param callback returning false removes the listener + * @param preemptive if true (default), calls callback immediately and skips adding if it returns false + */ addConditionalChangeListener(callback: () => boolean, preemptive?: boolean): void; + /** adds listener to _reactChangeCallbacks, invoked after all regular change listeners complete */ addReactChangeListener(callback: Callback): void; removeChangeListener(callback: Callback): void; removeReactChangeListener(callback: Callback): void; + /** called by dispatcher after action handlers run, marks changed if listeners exist and may resume paused dispatch */ doEmitChanges(event: FluxEvent): void; + /** marks store as changed for batched listener notification */ emitChange(): void; + /** unique token identifying this store in the dispatcher */ getDispatchToken(): string; + /** override to set up initial state, called once by initializeIfNeeded */ initialize(): void; + /** calls initialize() if not already initialized, adds performance mark if init takes >5ms */ initializeIfNeeded(): void; - /** this is a setter */ - mustEmitChanges(actionHandler: ActionHandler | undefined): void; - registerActionHandlers(actionHandlers: ActionHandlers): void; - syncWith(stores: FluxStore[], callback: Callback, timeout?: number): void; + /** + * sets callback to determine if changes must emit during paused dispatch. + * @param callback if omitted, defaults to () => true (always emit) + */ + mustEmitChanges(callback?: ActionHandler): void; + /** + * registers additional action handlers after construction. + * @param actionHandlers handlers keyed by action type + * @param band priority band, lower runs first + */ + registerActionHandlers(actionHandlers: ActionHandlers, band?: number): void; + /** + * syncs this store with other stores, re-emitting when they change. + * without timeout: synchronous, callback runs during emitNonReactOnce. + * with timeout: debounced, adds regular change listener to each source store. + * @param stores stores to sync with + * @param callback returning false skips emitChange on this store + * @param timeout if provided, debounces the sync callback + */ + syncWith(stores: FluxStore[], callback: SyncCallback, timeout?: number): void; + /** adds dispatcher dependencies so this store's handlers run after the specified stores */ waitFor(...stores: FluxStore[]): void; + /** initializes all registered stores, called once at app startup */ + static initialize(): void; + /** clears all registered stores and destroys the change listener system */ + static destroy(): void; + /** returns all registered FluxStore instances */ static getAll(): FluxStore[]; } diff --git a/packages/discord-types/src/stores/FriendsStore.d.ts b/packages/discord-types/src/stores/FriendsStore.d.ts new file mode 100644 index 000000000..4a1d1c71e --- /dev/null +++ b/packages/discord-types/src/stores/FriendsStore.d.ts @@ -0,0 +1,69 @@ +import { Activity, FluxStore, Guild, User } from ".."; +import { GiftIntentType, RelationshipType } from "../../enums"; + +export type FriendsSection = "ADD_FRIEND" | "ALL" | "ONLINE" | "PENDING" | "PENDING_IGNORED" | "SPAM" | "SUGGESTIONS"; + +export type StatusType = "online" | "offline" | "idle" | "dnd" | "invisible" | "streaming" | "unknown"; + +export type StreamType = "call" | "guild"; + +export interface ApplicationStream { + channelId: string; + guildId: string | null; + ownerId: string; + streamType: StreamType; +} + +export interface FriendsRow { + key: string; + userId: string; + /** + * 99 means contact based friend suggestions from FriendSuggestionStore, + * shown in SUGGESTIONS tab. different from RelationshipType.SUGGESTION + * which is for implicit suggestions in RelationshipStore + */ + type: RelationshipType | 99; + status: StatusType; + isMobile: boolean; + activities: Activity[]; + applicationStream: ApplicationStream | null; + user: User | null; + usernameLower: string | null; + mutualGuildsLength: number; + mutualGuilds: Guild[]; + nickname: string | null; + spam: boolean; + giftIntentType: GiftIntentType | undefined; + ignoredUser: boolean; + applicationId: string | undefined; + isGameRelationship: boolean; + comparator: [RelationshipType | 99, string | null]; +} + +export interface RelationshipCounts { + [RelationshipType.FRIEND]: number; + [RelationshipType.INCOMING_REQUEST]: number; + [RelationshipType.OUTGOING_REQUEST]: number; + [RelationshipType.BLOCKED]: number; + /** contact based friend suggestions from FriendSuggestionStore */ + 99: number; +} + +export interface FriendsRows { + _rows: FriendsRow[]; + reset(): FriendsRows; + clone(): FriendsRows; + update(updater: (userId: string) => Partial): boolean; + filter(section: FriendsSection, searchQuery?: string | null): FriendsRow[]; + getRelationshipCounts(): RelationshipCounts; +} + +export interface FriendsState { + fetching: boolean; + section: FriendsSection; + rows: FriendsRows; +} + +export class FriendsStore extends FluxStore { + getState(): FriendsState; +} diff --git a/packages/discord-types/src/stores/GuildChannelStore.d.ts b/packages/discord-types/src/stores/GuildChannelStore.d.ts new file mode 100644 index 000000000..7dbd81f14 --- /dev/null +++ b/packages/discord-types/src/stores/GuildChannelStore.d.ts @@ -0,0 +1,46 @@ +import { Channel, FluxStore, ThreadJoined } from ".."; +import { ChannelType } from "../../enums"; + +export interface ChannelWithComparator { + channel: Channel; + comparator: number; +} + +export interface GuildChannels { + [ChannelType.GUILD_CATEGORY]: ChannelWithComparator[]; + id: string; + SELECTABLE: ChannelWithComparator[] | ThreadJoined[]; + VOCAL: ChannelWithComparator[]; + count: number; +} + +export interface ChannelNameDisambiguation { + id: string; + name: string; +} + +export class GuildChannelStore extends FluxStore { + getAllGuilds(): Record; + getChannels(guildId: string): GuildChannels; + getDefaultChannel(guildId: string): Channel | null; + getDirectoryChannelIds(guildId: string): string[]; + getFirstChannel( + guildId: string, + predicate: (item: ChannelWithComparator) => boolean, + includeVocal?: boolean + ): Channel | null; + getFirstChannelOfType( + guildId: string, + predicate: (item: ChannelWithComparator) => boolean, + type: "SELECTABLE" | "VOCAL" | ChannelType.GUILD_CATEGORY + ): Channel | null; + getSFWDefaultChannel(guildId: string): Channel | null; + getSelectableChannelIds(guildId: string): string[]; + getSelectableChannels(guildId: string): ChannelWithComparator[]; + getTextChannelNameDisambiguations(guildId: string): Record; + getVocalChannelIds(guildId: string): string[]; + hasCategories(guildId: string): boolean; + hasChannels(guildId: string): boolean; + hasElevatedPermissions(guildId: string): boolean; + hasSelectableChannel(guildId: string, channelId: string): boolean; +} diff --git a/packages/discord-types/src/stores/GuildMemberCountStore.d.ts b/packages/discord-types/src/stores/GuildMemberCountStore.d.ts new file mode 100644 index 000000000..14b24ccf9 --- /dev/null +++ b/packages/discord-types/src/stores/GuildMemberCountStore.d.ts @@ -0,0 +1,7 @@ +import { FluxStore } from ".."; + +export class GuildMemberCountStore extends FluxStore { + getMemberCounts(): Record; + getMemberCount(guildId: string): number; + getOnlineCount(guildId: string): number; +} diff --git a/packages/discord-types/src/stores/GuildMemberStore.d.ts b/packages/discord-types/src/stores/GuildMemberStore.d.ts index 9dec139aa..38a9421d8 100644 --- a/packages/discord-types/src/stores/GuildMemberStore.d.ts +++ b/packages/discord-types/src/stores/GuildMemberStore.d.ts @@ -1,5 +1,10 @@ import { FluxStore, GuildMember } from ".."; +export interface PendingRoleUpdates { + added: string[]; + removed: string[]; +} + export class GuildMemberStore extends FluxStore { /** @returns Format: [guildId-userId: Timestamp (string)] */ getCommunicationDisabledUserMap(): Record; @@ -11,6 +16,10 @@ export class GuildMemberStore extends FluxStore { getTrueMember(guildId: string, userId: string): GuildMember | null; getMemberIds(guildId: string): string[]; getMembers(guildId: string): GuildMember[]; + getMemberVersion(): number; + getMemberRoleWithPendingUpdates(guildId: string, userId: string): string[]; + getPendingRoleUpdates(guildId: string): PendingRoleUpdates; + memberOf(userId: string): string[]; getCachedSelfMember(guildId: string): GuildMember | null; getSelfMember(guildId: string): GuildMember | null; @@ -20,7 +29,6 @@ export class GuildMemberStore extends FluxStore { getNicknameGuildsMapping(userId: string): Record; getNicknames(userId: string): string[]; - isMember(guildId: string, userId: string): boolean; isMember(guildId: string, userId: string): boolean; isGuestOrLurker(guildId: string, userId: string): boolean; isCurrentUserGuest(guildId: string): boolean; diff --git a/packages/discord-types/src/stores/GuildRoleStore.d.ts b/packages/discord-types/src/stores/GuildRoleStore.d.ts index dc822a33b..93d325b9b 100644 --- a/packages/discord-types/src/stores/GuildRoleStore.d.ts +++ b/packages/discord-types/src/stores/GuildRoleStore.d.ts @@ -1,13 +1,13 @@ import { FluxStore, Guild, Role } from ".."; -// TODO: add the rest of the methods for GuildRoleStore export class GuildRoleStore extends FluxStore { - serializeAllGuildRoles(): Array<{ partitionKey: string; values: Record; }>; - getUnsafeMutableRoles(guildId: string): Record; - getManyRoles(guildId: string, roleIds: string[]): Role[]; - getRole(guildId: string, roleId: string): Role; - getNumRoles(guildId: string): number; - getEveryoneRole(guild: Guild): Role; getRolesSnapshot(guildId: string): Record; getSortedRoles(guildId: string): Role[]; + + getEveryoneRole(guild: Guild): Role; + getManyRoles(guildId: string, roleIds: string[]): Role[]; + getNumRoles(guildId: string): number; + getRole(guildId: string, roleId: string): Role; + getUnsafeMutableRoles(guildId: string): Record; + serializeAllGuildRoles(): Array<{ partitionKey: string; values: Record; }>; } diff --git a/packages/discord-types/src/stores/GuildScheduledEventStore.d.ts b/packages/discord-types/src/stores/GuildScheduledEventStore.d.ts new file mode 100644 index 000000000..ea60f94bd --- /dev/null +++ b/packages/discord-types/src/stores/GuildScheduledEventStore.d.ts @@ -0,0 +1,67 @@ +import { FluxStore } from ".."; +import { GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel, GuildScheduledEventStatus, RecurrenceRuleFrequency } from "../../enums"; + +export interface GuildScheduledEventEntityMetadata { + location?: string; +} + +export interface GuildScheduledEventRecurrenceRule { + start: string; + end: string | null; + frequency: RecurrenceRuleFrequency; + interval: number; + byWeekday: number[] | null; + byNWeekday: { n: number; day: number; }[] | null; + byMonth: number[] | null; + byMonthDay: number[] | null; + byYearDay: number[] | null; + count: number | null; +} + +export interface GuildScheduledEvent { + id: string; + guild_id: string; + channel_id: string | null; + creator_id: string | null; + name: string; + description: string | null; + image: string | null; + scheduled_start_time: string; + scheduled_end_time: string | null; + privacy_level: GuildScheduledEventPrivacyLevel; + status: GuildScheduledEventStatus; + entity_type: GuildScheduledEventEntityType; + entity_id: string | null; + entity_metadata: GuildScheduledEventEntityMetadata | null; + sku_ids: string[]; + recurrence_rule: GuildScheduledEventRecurrenceRule | null; + // TODO: type + guild_scheduled_event_exceptions: any[]; + auto_start: boolean; +} + +export interface GuildScheduledEventRsvp { + guildScheduledEventId: string; + userId: string; + interested: boolean; +} + +export interface GuildScheduledEventUsers { + // TODO: finish typing + [userId: string]: any; +} + +export class GuildScheduledEventStore extends FluxStore { + getGuildScheduledEvent(eventId: string): GuildScheduledEvent | null; + getGuildScheduledEventsForGuild(guildId: string): GuildScheduledEvent[]; + getGuildScheduledEventsByIndex(status: GuildScheduledEventStatus): GuildScheduledEvent[]; + getGuildEventCountByIndex(status: GuildScheduledEventStatus): number; + getRsvpVersion(): number; + getRsvp(eventId: string, recurrenceId: string | null, userId: string | null): GuildScheduledEventRsvp | null; + isInterestedInEventRecurrence(eventId: string, recurrenceId: string | null): boolean; + getUserCount(eventId: string, recurrenceId: string | null): number; + hasUserCount(eventId: string, recurrenceId: string | null): boolean; + isActive(eventId: string): boolean; + getActiveEventByChannel(channelId: string): GuildScheduledEvent | null; + getUsersForGuildEvent(eventId: string, recurrenceId: string | null): GuildScheduledEventUsers; +} diff --git a/packages/discord-types/src/stores/GuildStore.d.ts b/packages/discord-types/src/stores/GuildStore.d.ts index d1a3b9b35..fd35053c2 100644 --- a/packages/discord-types/src/stores/GuildStore.d.ts +++ b/packages/discord-types/src/stores/GuildStore.d.ts @@ -4,5 +4,6 @@ export class GuildStore extends FluxStore { getGuild(guildId: string): Guild; getGuildCount(): number; getGuilds(): Record; + getGuildsArray(): Guild[]; getGuildIds(): string[]; } diff --git a/packages/discord-types/src/stores/InstantInviteStore.d.ts b/packages/discord-types/src/stores/InstantInviteStore.d.ts new file mode 100644 index 000000000..bc4af185e --- /dev/null +++ b/packages/discord-types/src/stores/InstantInviteStore.d.ts @@ -0,0 +1,17 @@ +import { FluxStore } from ".."; +import { Invite } from "./InviteStore"; + +export interface FriendInvite extends Invite { + max_age: number; + max_uses: number; + uses: number; + created_at: string; + revoked?: boolean; +} + +export class InstantInviteStore extends FluxStore { + getInvite(channelId: string): Invite; + getFriendInvite(): FriendInvite | null; + getFriendInvitesFetching(): boolean; + canRevokeFriendInvite(): boolean; +} diff --git a/packages/discord-types/src/stores/InviteStore.d.ts b/packages/discord-types/src/stores/InviteStore.d.ts new file mode 100644 index 000000000..2f27d8cad --- /dev/null +++ b/packages/discord-types/src/stores/InviteStore.d.ts @@ -0,0 +1,39 @@ +import { Channel, FluxStore, Guild, User } from ".."; +import { Application } from "../common/Application"; +import { GuildScheduledEvent } from "./GuildScheduledEventStore"; +import { GuildScheduledEventPrivacyLevel, InviteTargetType } from "../../enums"; + +export interface StageInstance { + id: string; + guild_id: string; + channel_id: string; + topic: string; + privacy_level: GuildScheduledEventPrivacyLevel; + discoverable_disabled: boolean; + guild_scheduled_event_id: string | null; +} + +export interface Invite { + code: string; + guild: Guild | null; + channel: Channel | null; + inviter: User | null; + approximate_member_count?: number; + approximate_presence_count?: number; + expires_at?: string | null; + flags?: number; + target_type?: InviteTargetType; + target_user?: User; + target_application?: Application; + stage_instance?: StageInstance; + guild_scheduled_event?: GuildScheduledEvent; +} + +export class InviteStore extends FluxStore { + getInvite(code: string): Invite; + // TODO: finish typing + getInviteError(code: string): any | undefined; + getInvites(): Record; + getInviteKeyForGuildId(guildId: string): string | undefined; + getFriendMemberIds(code: string): string[] | undefined; +} diff --git a/packages/discord-types/src/stores/LocaleStore.d.ts b/packages/discord-types/src/stores/LocaleStore.d.ts new file mode 100644 index 000000000..ca5915de2 --- /dev/null +++ b/packages/discord-types/src/stores/LocaleStore.d.ts @@ -0,0 +1,6 @@ +import { FluxStore } from ".."; + +export class LocaleStore extends FluxStore { + get locale(): string; + get systemLocale(): string; +} diff --git a/packages/discord-types/src/stores/MediaEngineStore.d.ts b/packages/discord-types/src/stores/MediaEngineStore.d.ts index 4ef81639d..6f358164c 100644 --- a/packages/discord-types/src/stores/MediaEngineStore.d.ts +++ b/packages/discord-types/src/stores/MediaEngineStore.d.ts @@ -1,44 +1,2742 @@ import { FluxStore } from ".."; +/** Context for media engine settings. */ +export type MediaEngineContextType = "default" | "stream"; + +/** Audio/video device type identifiers. */ +export type DeviceType = "audioinput" | "audiooutput" | "videoinput"; + +/** Voice activation mode for microphone input. */ +export type VoiceMode = "PUSH_TO_TALK" | "VOICE_ACTIVITY"; + +/** WebRTC connection state. */ +export type ConnectionState = + | "DISCONNECTED" + | "CONNECTING" + | "CONNECTED" + | "NO_ROUTE" + | "ICE_CHECKING" + | "DTLS_CONNECTING"; + +/** Video toggle state indicating why video was enabled/disabled. */ +export type VideoToggleState = + | "NONE" + | "video_manual_disable" + | "video_manual_enable" + | "video_manual_reenable" + | "video_auto_disable" + | "video_auto_enable" + | "video_auto_downgrade" + | "video_auto_upgrade"; + +/** Quality override for video streams. */ +export type VideoQualityOverride = "no_override" | "high" | "low"; + +/** Audio processing subsystem. */ +export type AudioSubsystem = "legacy" | "standard" | "experimental" | "automatic"; + +/** Media stream types. */ +export type MediaType = "audio" | "video" | "screen" | "test"; + +/** Keyframe interval calculation mode. */ +export type KeyframeIntervalMode = "fixed" | "source"; + +/** Bandwidth estimation algorithm type. */ +export type BandwidthEstimationType = "remb"; + +/** Media engine implementation type. */ +export type MediaEngineType = "NATIVE" | "WEBRTC" | "DUMMY"; + +/** Desktop capture source types. */ +export type DesktopSourceType = "WINDOW" | "SCREEN"; + +/** HDR capture mode for screen sharing. */ +export type HdrCaptureMode = "never" | "always" | "auto"; + +/** Input profile type for voice settings. */ +export type InputProfile = "DEFAULT" | "CUSTOM"; + +/** Media engine feature flags for capability checking. */ +export type MediaEngineFeature = + | "AUTO_ENABLE" + | "ATTENUATION" + | "AUDIO_INPUT_DEVICE" + | "AUDIO_OUTPUT_DEVICE" + | "VOICE_PROCESSING" + | "QOS" + | "NATIVE_PING" + | "LEGACY_AUDIO_SUBSYSTEM" + | "EXPERIMENTAL_AUDIO_SUBSYSTEM" + | "AUTOMATIC_AUDIO_SUBSYSTEM" + | "AUDIO_SUBSYSTEM_DEFERRED_SWITCH" + | "AUDIO_BYPASS_SYSTEM_INPUT_PROCESSING" + | "DEBUG_LOGGING" + | "AUTOMATIC_VAD" + | "VOICE_PANNING" + | "DIAGNOSTICS" + | "VIDEO" + | "DESKTOP_CAPTURE" + | "DESKTOP_CAPTURE_FORMAT" + | "DESKTOP_CAPTURE_APPLICATIONS" + | "SOUNDSHARE" + | "LOOPBACK" + | "VIDEO_HOOK" + | "EXPERIMENTAL_SOUNDSHARE" + | "WUMPUS_VIDEO" + | "ELEVATED_HOOK" + | "HYBRID_VIDEO" + | "REMOTE_LOCUS_NETWORK_CONTROL" + | "SCREEN_PREVIEWS" + | "WINDOW_PREVIEWS" + | "AUDIO_DEBUG_STATE" + | "AEC_DUMP" + | "DISABLE_VIDEO" + | "CONNECTION_REPLAY" + | "SIMULCAST" + | "RTC_REGION_RANKING" + | "ELECTRON_VIDEO" + | "MEDIAPIPE" + | "FIXED_KEYFRAME_INTERVAL" + | "SAMPLE_PLAYBACK" + | "FIRST_FRAME_CALLBACK" + | "REMOTE_USER_MULTI_STREAM" + | "NOISE_SUPPRESSION" + | "NOISE_CANCELLATION" + | "VOICE_FILTERS" + | "AUTOMATIC_GAIN_CONTROL" + | "CLIPS" + | "SPEED_TEST" + | "IMAGE_QUALITY_MEASUREMENT" + | "GO_LIVE_HARDWARE" + | "SCREEN_CAPTURE_KIT" + | "SCREEN_SOUNDSHARE" + | "NATIVE_SCREENSHARE_PICKER" + | "MLS_PAIRWISE_FINGERPRINTS" + | "OFFLOAD_ADM_CONTROLS" + | "SIDECHAIN_COMPRESSION" + | "VAAPI" + | "GAMESCOPE_CAPTURE" + | "ASYNC_VIDEO_INPUT_DEVICE_INIT" + | "ASYNC_CLIPS_SOURCE_DEINIT" + | "PORT_AWARE_LATENCY_TESTING"; + +/** Events emitted by the media engine. */ +export type MediaEngineEvent = + | "Destroy" + | "Silence" + | "Connection" + | "DeviceChange" + | "VolumeChange" + | "VoiceActivity" + | "WatchdogTimeout" + | "AudioPermission" + | "VideoPermission" + | "DesktopSourceEnd" + | "ConnectionStats" + | "VideoInputInitialized" + | "AudioInputInitialized" + | "ClipsRecordingRestartNeeded" + | "ClipsInitFailure" + | "ClipsRecordingEnded" + | "NativeScreenSharePickerUpdate" + | "NativeScreenSharePickerCancel" + | "NativeScreenSharePickerError" + | "AudioDeviceModuleError" + | "VoiceFiltersFailed" + | "VideoCodecError" + | "VoiceQueueMetrics" + | "SystemMicrophoneModeChange" + | "SelectedDeviceChange"; + +/** + * Audio input or output device. + */ +export interface AudioDevice { + /** unique device identifier from the system. */ + id: string; + /** device index in enumeration, -1 for default device. */ + index: number; + /** human readable device name. */ + name: string; + /** whether the device is disabled in system settings. */ + disabled: boolean; + /** camera facing direction if applicable, undefined for audio devices. */ + facing?: string; + /** windows device GUID for identification. */ + guid: string; + /** hardware identifier for the device. */ + hardwareId: string; + /** container identifier grouping related devices. */ + containerId: string; + /** audio effects supported by device, undefined if none. */ + effects?: string[]; +} + +/** + * Video input device (webcam). + */ +export interface VideoDevice { + /** unique device identifier from the system. */ + id: string; + /** device index in enumeration. */ + index: number; + /** human readable device name. */ + name: string; + /** whether the device is disabled in system settings. */ + disabled: boolean; + /** camera facing direction, "front", "back", or "unknown". */ + facing?: string; + /** windows device GUID for identification. */ + guid: string; + /** hardware identifier for the device. */ + hardwareId?: string; + /** container identifier grouping related devices. */ + containerId?: string; + /** video effects supported by device, undefined if none. */ + effects?: string[]; +} + +/** + * Quality settings for clips recording. + */ +export interface ClipsQuality { + /** recording frame rate in fps. */ + frameRate: number; + /** recording resolution height in pixels. */ + resolution: number; +} + +/** + * Desktop capture configuration for clips. + */ +export interface DesktopDescription { + /** desktop source identifier. */ + id: string; + /** soundshare source identifier for audio capture. */ + soundshareId: number; + /** whether to use loopback audio capture. */ + useLoopback: boolean; + /** whether to use video hook for capture. */ + useVideoHook: boolean; + /** whether to use windows graphics capture API. */ + useGraphicsCapture: boolean; + /** whether to use macOS quartz capturer. */ + useQuartzCapturer: boolean; + /** whether to allow macOS screencapturekit. */ + allowScreenCaptureKit: boolean; + /** HDR capture behavior. */ + hdrCaptureMode: HdrCaptureMode; +} + +/** + * Source configuration for clips recording. + */ +export interface ClipsSource { + /** quality settings for the recording. */ + quality: ClipsQuality; + /** desktop capture configuration. */ + desktopDescription: DesktopDescription; +} + +/** + * Desktop source for screen sharing. + */ +export interface DesktopSource { + /** source identifier string. */ + id: string; + /** process id of the source application, null if not applicable. */ + sourcePid: number | null; + /** soundshare identifier for audio capture, null if not capturing audio. */ + soundshareId: string | null; + /** soundshare session identifier, null if not active. */ + soundshareSession: string | null; +} + +/** + * Quality settings for go live streaming. + */ +export interface GoLiveQuality { + /** stream resolution height in pixels. */ + resolution: number; + /** stream frame rate in fps. */ + frameRate: number; +} + +/** + * Source configuration for go live streaming. + */ +export interface GoLiveSource { + /** desktop source being streamed. */ + desktopSource: DesktopSource; + /** quality settings for the stream. */ + quality: GoLiveQuality; +} + +/** + * Video stream parameter for simulcast layers. + */ +export interface VideoStreamParameter { + /** simulcast layer id, e.g. "100" for full quality, "50" for half. */ + rid: string; + /** type of media stream. */ + type: MediaType; + /** quality percentage 0-100. */ + quality: number; +} + +/** + * Stereo panning for a user's audio. + */ +export interface LocalPan { + /** left channel volume multiplier 0-1, default 1. */ + left: number; + /** right channel volume multiplier 0-1, default 1. */ + right: number; +} + +/** + * Voice activity detection and push-to-talk options. + */ +export interface ModeOptions { + /** VAD threshold in dB, default -60. */ + threshold: number; + /** whether to auto-adjust threshold based on noise floor, default true. */ + autoThreshold: boolean; + /** whether to use krisp for VAD instead of webrtc, default true. */ + vadUseKrisp: boolean; + /** krisp activation threshold 0-1, default 0.8. */ + vadKrispActivationThreshold: number; + /** frames of audio to keep before speech is detected, default 5. */ + vadLeading: number; + /** frames to keep transmitting after speech ends, default 25. */ + vadTrailing: number; + /** PTT release delay in milliseconds, default 20. */ + delay: number; + /** keyboard shortcut keys for PTT, default empty array. */ + shortcut: string[]; + /** whether to run VAD before audio processing, default false. */ + vadDuringPreProcess?: boolean; +} + +/** + * Options for audio loopback testing. + */ +export interface LoopbackOptions { + /** whether echo cancellation is enabled. */ + echoCancellation: boolean; + /** whether noise suppression is enabled. */ + noiseSuppression: boolean; + /** whether automatic gain control is enabled. */ + automaticGainControl: boolean; + /** whether krisp noise cancellation is enabled. */ + noiseCancellation: boolean; +} + +/** + * Screen capture preview thumbnail. + */ +export interface ScreenPreview { + /** screen source identifier. */ + id: string; + /** data URL of thumbnail image. */ + url: string; + /** display name, e.g. "Screen 1". */ + name: string; +} + +/** + * Window capture preview thumbnail. + */ +export interface WindowPreview { + /** window source identifier. */ + id: string; + /** data URL of thumbnail image. */ + url: string; + /** window title. */ + name: string; +} + +/** + * Krisp noise cancellation statistics. + */ +export interface NoiseCancellationStats { + /** milliseconds of detected voice audio. */ + voiceMs: number; + /** milliseconds of detected music audio. */ + musicMs: number; + /** milliseconds of detected noise audio. */ + noiseMs: number; +} + +/** + * MLS signing key for end-to-end encryption. + */ +export interface MLSSigningKey { + /** raw key bytes. */ + key: Uint8Array; + /** key signature bytes. */ + signature: Uint8Array; +} + +/** + * Codec capability info for a single codec. + */ +export interface CodecInfo { + /** codec name (H264, VP8, VP9, AV1, H265). */ + name: string; + /** whether encoding is supported, false if no hardware/software encoder available. */ + encode: boolean; + /** whether decoding is supported, false if no hardware/software decoder available. */ + decode: boolean; +} + +/** + * Metadata for saved clips. + */ +export interface ClipMetadata { + /** custom name for the clip. */ + name?: string; + /** description text for the clip. */ + description?: string; +} + +/** + * Result of saving a clip. + */ +export interface SavedClip { + /** unique clip identifier. */ + id: string; + /** path where clip was saved. */ + filepath: string; +} + +/** + * Result of saving a screenshot. + */ +export interface Screenshot { + /** path where screenshot was saved. */ + filepath: string; +} + +/** + * Settings for video background filters. + */ +export interface MediaFilterSettings { + /** whether background replacement is enabled. */ + backgroundEnabled: boolean; + /** background blur intensity 0-100. */ + backgroundBlur: number; + /** custom background image id, null for blur only. */ + backgroundId: string | null; +} + +/** + * Options for local audio recording. + */ +export interface AudioRecordingOptions { + /** whether to apply echo cancellation. */ + echoCancellation: boolean; + /** whether to apply noise suppression. */ + noiseSuppression: boolean; +} + +/** + * Options for raw audio sample recording. + */ +export interface RawSamplesOptions { + /** number of audio channels. */ + channels: number; + /** sample rate in hz. */ + sampleRate: number; +} + +/** + * Options for creating a voice connection. + */ +export interface ConnectionOptions { + /** whether to start muted. */ + selfMute: boolean; + /** whether to start deafened. */ + selfDeaf: boolean; + /** whether to start with video enabled. */ + selfVideo: boolean; +} + +/** + * Options for creating a replay connection. + */ +export interface ReplayConnectionOptions { + /** path to the replay file. */ + filePath: string; +} + +/** + * Info emitted when video input initializes. + */ +export interface VideoInputInitializationInfo { + /** device that was initialized. */ + description: VideoDevice; + /** time in seconds until first frame. */ + timeToFirstFrame: number; + /** whether initialization timed out. */ + initializationTimerExpired: boolean; + /** entropy value for the video feed. */ + entropy: number; +} + +/** + * Info emitted when audio input initializes. + */ +export interface AudioInputInitializationInfo { + /** device that was initialized. */ + description: AudioDevice; + /** time in seconds until initialized. */ + timeToInitialized: number; +} + +/** + * Video codec error details. + */ +export interface VideoCodecErrorInfo { + /** whether error occurred during encode or decode. */ + mode: "encode" | "decode"; + /** codec standard name. */ + codecStandard: string; + /** error message text. */ + message: string; + /** implementation name that failed. */ + implName: string; +} + +/** + * Codec information for connection setup. + */ +export interface ConnectionCodec { + /** codec name. */ + name: string; + /** payload type number. */ + payloadType: number; + /** priority order. */ + priority: number; + /** rtx payload type if applicable. */ + rtxPayloadType?: number; +} + +/** + * Connection transport initialization options. + */ +export interface ConnectionTransportOptions { + /** server address. */ + address: string; + /** server port. */ + port: number; + /** audio ssrc. */ + ssrc: number; + /** available encryption modes. */ + modes: string[]; + /** stream count. */ + streamCount?: number; + /** audio codecs. */ + audioCodec?: ConnectionCodec; + /** video codecs. */ + videoCodec?: ConnectionCodec; + /** rtx codecs. */ + rtxCodec?: ConnectionCodec; + /** experiment flags. */ + experiments?: string[]; +} + +/** + * Input mode options for voice activity or push-to-talk. + */ +export interface InputModeOptions { + /** VAD threshold in dB. */ + vadThreshold?: number; + /** whether to auto-adjust threshold. */ + vadAutoThreshold?: boolean; + /** whether to use krisp for VAD. */ + vadUseKrisp?: boolean; + /** krisp activation threshold. */ + vadKrispActivationThreshold?: number; + /** frames before speech detection. */ + vadLeading?: number; + /** frames after speech ends. */ + vadTrailing?: number; + /** PTT release delay in ms. */ + pttReleaseDelay?: number; +} + +/** + * Go live source configuration for streaming. + */ +export interface GoLiveSourceOptions { + /** quality settings. */ + quality: GoLiveQuality; + /** desktop description if streaming desktop. */ + desktopDescription?: DesktopDescription; + /** camera description if streaming camera. */ + cameraDescription?: { deviceId: string; }; +} + +/** + * Video stream parameter for simulcast configuration. + */ +export interface StreamParameter { + /** simulcast rid. */ + rid: string; + /** max bitrate. */ + maxBitrate?: number; + /** max framerate. */ + maxFrameRate?: number; + /** max resolution. */ + maxResolution?: { width: number; height: number; }; + /** quality percentage. */ + quality?: number; +} + +/** + * Automatic gain control configuration. + */ +export interface AutomaticGainControlConfig { + /** whether AGC is enabled, default true. */ + enabled: boolean; + /** whether to use AGC2 algorithm, default true. */ + useAGC2: boolean; + /** whether analog gain control is enabled, default false. */ + enableAnalog: boolean; + /** whether digital gain control is enabled, default true. */ + enableDigital: boolean; + /** headroom in decibels, default 5. */ + headroom_db: number; + /** maximum gain in decibels, default 50. */ + max_gain_db: number; + /** initial gain in decibels, default 15. */ + initial_gain_db: number; + /** max gain change per second in decibels, default 6. */ + max_gain_change_db_per_second: number; + /** max output noise level in dbfs, default -50. */ + max_output_noise_level_dbfs: number; + /** fixed gain in decibels, default 0. */ + fixed_gain_db: number; +} + +/** + * Active voice/video connection to a channel. + */ +export interface MediaEngineConnection { + /** context this connection belongs to. */ + context: MediaEngineContextType; + /** unique identifier for this connection. */ + mediaEngineConnectionId: string; + /** user id who owns this connection. */ + userId: string; + /** user id for stream context, undefined in default context. */ + streamUserId: string | undefined; + /** current connection state. */ + connectionState: ConnectionState; + /** whether self is muted. */ + selfMute: boolean; + /** whether self is deafened. */ + selfDeaf: boolean; + /** whether self video is enabled. */ + selfVideo: boolean; + /** whether this connection has been destroyed. */ + destroyed: boolean; + /** audio ssrc for this connection. */ + audioSSRC: number; + /** video ssrc for this connection. */ + videoSSRC: number; + /** local mute states keyed by user id. */ + localMutes: { [userId: string]: boolean; }; + /** local volume levels keyed by user id. */ + localVolumes: { [userId: string]: number; }; + /** local pan settings keyed by user id. */ + localPans: { [userId: string]: LocalPan; }; + /** disabled local video states keyed by user id. */ + disabledLocalVideos: { [userId: string]: boolean; }; + /** current voice bitrate in bps, default 64000. */ + voiceBitrate: number; + /** whether video is supported. */ + videoSupported: boolean; + /** video stream parameters. */ + videoStreamParameters: StreamParameter[]; + /** soundshare source id. */ + soundshareId: number | null; + /** whether soundshare is active. */ + soundshareActive: boolean; + /** whether echo cancellation is enabled. */ + echoCancellation: boolean; + /** whether noise suppression is enabled. */ + noiseSuppression: boolean; + /** automatic gain control configuration. */ + automaticGainControl: AutomaticGainControlConfig; + /** whether noise cancellation is enabled. */ + noiseCancellation: boolean; + /** whether QoS is enabled. */ + qos: boolean; + /** current input mode. */ + inputMode: VoiceMode; + /** VAD threshold in dB, default -60. */ + vadThreshold: number; + /** whether VAD auto threshold is enabled, default true. */ + vadAutoThreshold: boolean; + /** PTT release delay in ms, default 20. */ + pttReleaseDelay: number; + /** keyframe interval in ms, default 0. */ + keyframeInterval: number; + /** attenuation factor 0-1, default 1 (no attenuation). */ + attenuationFactor: number; + /** whether to attenuate while self speaking. */ + attenuateWhileSpeakingSelf: boolean; + /** whether to attenuate while others speaking. */ + attenuateWhileSpeakingOthers: boolean; + + /** + * Initializes the connection with transport options. + * @param options transport options. + */ + initialize(options: ConnectionTransportOptions): void; + /** destroys this connection and cleans up resources. */ + destroy(): void; + /** + * Sets codecs for the connection. + * @param audioCodec audio codec name. + * @param videoCodec video codec name. + * @param rtxCodec rtx codec name. + */ + setCodecs(audioCodec: string, videoCodec: string, rtxCodec: string): void; + /** + * Gets connection statistics. + * @returns promise resolving to stats or null. + */ + getStats(): Promise; + /** + * Creates a remote user in the connection. + * @param userId user id. + * @param audioSSRC audio ssrc. + * @param videoSSRC video ssrc. + */ + createUser(userId: string, audioSSRC: number, videoSSRC: number): void; + /** + * Destroys a remote user from the connection. + * @param userId user id. + */ + destroyUser(userId: string): void; + /** + * Sets self mute state. + * @param mute whether to mute. + */ + setSelfMute(mute: boolean): void; + /** + * Gets self mute state. + * @returns true if muted. + */ + getSelfMute(): boolean; + /** + * Gets self deaf state. + * @returns true if deafened. + */ + getSelfDeaf(): boolean; + /** + * Sets self deaf state. + * @param deaf whether to deafen. + */ + setSelfDeaf(deaf: boolean): void; + /** + * Sets soundshare source for this connection. + * @param soundshareId soundshare source id. + * @param active whether to enable. + */ + setSoundshareSource(soundshareId: number, active: boolean): void; + /** + * Sets local mute for a user. + * @param userId user to mute. + * @param muted whether to mute. + */ + setLocalMute(userId: string, muted: boolean): void; + /** performs a fast UDP reconnect. */ + fastUdpReconnect(): void; + /** + * Gets number of fast UDP reconnects. + * @returns reconnect count or null if unsupported. + */ + getNumFastUdpReconnects(): number | null; + /** checks if remote was disconnected. */ + wasRemoteDisconnected(): void; + /** + * Disables receiving video from a user. + * @param userId user to disable video for. + * @param disabled whether to disable. + */ + setLocalVideoDisabled(userId: string, disabled: boolean): void; + /** + * Sets minimum jitter buffer level. + * @param level jitter buffer level. + */ + setMinimumJitterBufferLevel(level: number): void; + /** + * Sets postpone decode level. + * @param level decode level. + */ + setPostponeDecodeLevel(level: number): void; + /** + * Sets clip recording for a user. + * @param userId user id. + * @param type clip type. + * @param enabled whether enabled. + */ + setClipRecordUser(userId: string, type: string, enabled: boolean): void; + /** + * Sets clips keyframe interval. + * @param interval interval in ms. + */ + setClipsKeyFrameInterval(interval: number): void; + /** + * Sets viewer side clip. + * @param enabled whether enabled. + */ + setViewerSideClip(enabled: boolean): void; + /** + * Sets remote audio history duration. + * @param durationMs duration in ms. + */ + setRemoteAudioHistory(durationMs: number): void; + /** + * Sets quality decoupling. + * @param enabled whether enabled. + */ + setQualityDecoupling(enabled: boolean): void; + /** + * Gets local volume for a user. + * @param userId user id. + * @returns volume level. + */ + getLocalVolume(userId: string): number; + /** + * Sets local volume for a user. + * @param userId user to adjust. + * @param volume volume level 0-200, 100 is normal. + */ + setLocalVolume(userId: string, volume: number): void; + /** + * Sets stereo pan for a user. + * @param userId user to adjust. + * @param left left channel 0-1. + * @param right right channel 0-1. + */ + setLocalPan(userId: string, left: number, right: number): void; + /** + * Checks if currently attenuating. + * @returns true if attenuating. + */ + isAttenuating(): boolean; + /** + * Sets attenuation settings. + * @param factor attenuation factor 0-100. + * @param whileSpeakingSelf attenuate while self speaking. + * @param whileSpeakingOthers attenuate while others speaking. + */ + setAttenuation(factor: number, whileSpeakingSelf: boolean, whileSpeakingOthers: boolean): void; + /** + * Sets whether user can have priority speaker. + * @param userId user id. + * @param canHavePriority whether can have priority. + */ + setCanHavePriority(userId: string, canHavePriority: boolean): void; + /** + * Sets voice bitrate. + * @param bitrate bitrate in bps. + */ + setBitRate(bitrate: number): void; + /** + * Sets voice bitrate. + * @param bitrate bitrate in bps. + */ + setVoiceBitRate(bitrate: number): void; + /** + * Sets camera bitrate. + * @param maxBitrate max bitrate. + * @param minBitrate min bitrate. + * @param targetBitrate target bitrate. + */ + setCameraBitRate(maxBitrate: number, minBitrate: number | null, targetBitrate: number | null): void; + /** + * Sets echo cancellation. + * @param enabled whether enabled. + */ + setEchoCancellation(enabled: boolean): void; + /** + * Sets noise suppression. + * @param enabled whether enabled. + */ + setNoiseSuppression(enabled: boolean): void; + /** + * Sets automatic gain control. + * @param config AGC configuration. + */ + setAutomaticGainControl(config: AutomaticGainControlConfig): void; + /** + * Sets noise cancellation. + * @param enabled whether enabled. + */ + setNoiseCancellation(enabled: boolean): void; + /** + * Sets noise cancellation during processing. + * @param enabled whether enabled. + */ + setNoiseCancellationDuringProcessing(enabled: boolean): void; + /** + * Sets noise cancellation after processing. + * @param enabled whether enabled. + */ + setNoiseCancellationAfterProcessing(enabled: boolean): void; + /** + * Sets VAD after WebRTC. + * @param enabled whether enabled. + */ + setVADAfterWebrtc(enabled: boolean): void; + /** + * Gets noise cancellation state. + * @returns true if enabled. + */ + getNoiseCancellation(): boolean; + /** + * Gets current voice filter id. + * @returns voice filter id or null. + */ + getVoiceFilterId(): string | null; + /** + * Sets voice filter id. + * @param filterId filter id or null. + */ + setVoiceFilterId(filterId: string | null): void; + /** + * Sets QoS enabled. + * @param enabled whether enabled. + */ + setQoS(enabled: boolean): void; + /** + * Sets soundshare discard rear channels. + * @param discard whether to discard. + */ + setSoundshareDiscardRearChannels(discard: boolean): void; + /** + * Sets input mode. + * @param mode input mode. + * @param options mode options. + */ + setInputMode(mode: string, options: InputModeOptions): void; + /** + * Sets silence threshold. + * @param threshold threshold value. + */ + setSilenceThreshold(threshold: number): void; + /** + * Sets force audio input. + * @param force whether to force. + * @param playTone whether to play tone. + * @param isSpeaking whether speaking. + */ + setForceAudioInput(force: boolean, playTone?: boolean, isSpeaking?: boolean): void; + /** + * Sets speaking flags for a user. + * @param userId user id. + * @param flags speaking flags. + */ + setSpeakingFlags(userId: string, flags: number): void; + /** clears all speaking states. */ + clearAllSpeaking(): void; + /** + * Sets encryption mode. + * @param mode encryption mode. + * @param secretKey secret key. + */ + setEncryption(mode: string, secretKey: Uint8Array): void; + /** + * Sets reconnect interval. + * @param interval interval in ms. + */ + setReconnectInterval(interval: number): void; + /** + * Sets keyframe interval. + * @param interval interval in ms. + */ + setKeyframeInterval(interval: number): void; + /** + * Sets video quality measurement. + * @param enabled whether enabled. + */ + setVideoQualityMeasurement(enabled: boolean): void; + /** + * Sets video encoder experiments. + * @param experiments experiment config. + */ + setVideoEncoderExperiments(experiments: object): void; + /** + * Sets video broadcast state. + * @param broadcast whether broadcasting. + */ + setVideoBroadcast(broadcast: boolean): void; + /** + * Sets go live source. + * @param source source options. + */ + setGoLiveSource(source: GoLiveSourceOptions): void; + /** clears go live devices. */ + clearGoLiveDevices(): void; + /** clears the desktop source from this connection. */ + clearDesktopSource(): void; + /** + * Sets desktop source status callback. + * @param callback callback function. + */ + setDesktopSourceStatusCallback(callback: (status: string) => void): void; + /** + * Checks if connection has a desktop source. + * @returns true if streaming desktop. + */ + hasDesktopSource(): boolean; + /** + * Sets desktop encoding options. + * @param width width. + * @param height height. + * @param framerate framerate. + */ + setDesktopEncodingOptions(width: number, height: number, framerate: number): void; + /** + * Sets SDP. + * @param sdp SDP string. + */ + setSDP(sdp: string): void; + /** + * Sets remote video sink wants. + * @param wants sink wants config. + */ + setRemoteVideoSinkWants(wants: object): void; + /** + * Sets local video sink wants. + * @param wants sink wants config. + */ + setLocalVideoSinkWants(wants: object): void; + /** + * Starts samples local playback. + * @param id playback id. + * @param buffer audio buffer. + * @param options playback options. + * @param callback completion callback. + */ + startSamplesLocalPlayback(id: string, buffer: AudioBuffer, options: object, callback: (error: number, message: string) => void): void; + /** stops all samples local playback. */ + stopAllSamplesLocalPlayback(): void; + /** + * Stops samples local playback. + * @param id playback id. + */ + stopSamplesLocalPlayback(id: string): void; + /** + * Sets bandwidth estimation experiments. + * @param experiments experiment config. + */ + setBandwidthEstimationExperiments(experiments: object): void; + /** + * Updates video quality core. + * @param options quality options. + * @param reason update reason. + */ + updateVideoQualityCore(options: object, reason: string): void; + /** + * Sets stream parameters. + * @param params stream parameters. + * @returns promise. + */ + setStreamParameters(params: StreamParameter[]): Promise; + /** applies video transport options. */ + applyVideoTransportOptions(): void; + /** + * Chooses encryption mode. + * @param preferred preferred modes. + * @param available available modes. + * @returns chosen mode. + */ + chooseEncryptionMode(preferred: string[], available: string[]): string; + /** + * Gets user options. + * @returns user options array. + */ + getUserOptions(): object[]; + /** + * Creates input mode options. + * @returns input mode options. + */ + createInputModeOptions(): InputModeOptions; + /** + * Gets attenuation options. + * @returns attenuation options. + */ + getAttenuationOptions(): object; + /** + * Gets codec params. + * @param codec codec name. + * @param isHardware whether hardware codec. + * @returns codec params. + */ + getCodecParams(codec: string, isHardware: boolean): object; + /** + * Gets codec options. + * @param audioCodec audio codec. + * @param videoCodec video codec. + * @param rtxCodec rtx codec. + * @returns codec options. + */ + getCodecOptions(audioCodec: string, videoCodec: string, rtxCodec: string): object; + /** + * Gets keyframe interval. + * @returns interval in ms. + */ + getKeyFrameInterval(): number; + /** + * Gets connection transport options. + * @returns transport options. + */ + getConnectionTransportOptions(): object; + /** + * Sets stream (not implemented). + * @param stream media stream. + */ + setStream(stream: MediaStream): void; + /** + * Gets user id by ssrc. + * @param ssrc ssrc value. + */ + getUserIdBySsrc(ssrc: number): void; + /** + * Prepares secure frames transition. + * @param transitionId transition id. + * @param epoch epoch number. + * @param callback callback function. + */ + prepareSecureFramesTransition(transitionId: number, epoch: number, callback: () => void): void; + /** + * Prepares secure frames epoch. + * @param epoch epoch number. + * @param data epoch data. + * @param callback callback function. + */ + prepareSecureFramesEpoch(epoch: number, data: Uint8Array, callback: () => void): void; + /** + * Executes secure frames transition. + * @param transitionId transition id. + */ + executeSecureFramesTransition(transitionId: number): void; + /** + * Gets MLS key package. + * @param callback callback receiving key package. + */ + getMLSKeyPackage(callback: (keyPackage: Uint8Array) => void): void; + /** + * Updates MLS external sender. + * @param sender external sender data. + */ + updateMLSExternalSender(sender: Uint8Array): void; + /** + * Processes MLS proposals. + * @param proposals proposals data. + * @param callback callback function. + */ + processMLSProposals(proposals: Uint8Array, callback: () => void): void; + /** + * Prepares MLS commit transition. + * @param transitionId transition id. + * @param commit commit data. + * @param callback callback function. + */ + prepareMLSCommitTransition(transitionId: number, commit: Uint8Array, callback: () => void): void; + /** + * Processes MLS welcome. + * @param transitionId transition id. + * @param welcome welcome data. + * @param callback callback function. + */ + processMLSWelcome(transitionId: number, welcome: Uint8Array, callback: () => void): void; + /** + * Gets MLS pairwise fingerprint. + * @param userId user id. + * @param version version. + * @param callback callback receiving fingerprint. + */ + getMLSPairwiseFingerprint(userId: string, version: number, callback: (fingerprint: Uint8Array) => void): void; + /** + * Presents desktop source picker. + * @param options picker options. + */ + presentDesktopSourcePicker(options: object): void; + /** + * Merges users. + * @param users user merge data. + */ + mergeUsers(users: object[]): void; + /** + * Gets whether there is an active video output sink. + * @param userId user id. + * @returns true if has active sink. + */ + getHasActiveVideoOutputSink(userId: string): boolean; + /** + * Sets whether there is an active video output sink. + * @param userId user id. + * @param hasActiveSink whether sink is active. + * @param reason reason for the change. + */ + setHasActiveVideoOutputSink(userId: string, hasActiveSink: boolean, reason: string): void; + /** + * Applies quality constraints to video. + * @param constraints quality constraints object. + * @param ssrc optional ssrc to apply to. + * @returns quality manager result. + */ + applyQualityConstraints(constraints?: object, ssrc?: number): object; + /** + * Applies video quality mode preset. + * @param mode quality mode to apply. + */ + applyVideoQualityMode(mode: number): void; + /** + * Configures go live simulcast settings. + * @param enabled whether simulcast is enabled. + * @param options simulcast options. + */ + configureGoLiveSimulcast(enabled: boolean, options: object): void; + /** + * Emits connection stats. + * @returns promise resolving to stats or null. + */ + emitStats(): Promise; + /** + * Gets whether active output sink tracking is enabled. + * @returns true if enabled. + */ + getActiveOutputSinkTrackingEnabled(): boolean; + /** + * Gets local mute state for a user. + * @param userId user id. + * @returns true if muted. + */ + getLocalMute(userId: string): boolean; + /** + * Gets local video disabled state for a user. + * @param userId user id. + * @returns true if disabled. + */ + getLocalVideoDisabled(userId: string): boolean; + /** + * Gets local video quality want for a ssrc. + * @param ssrc optional ssrc. + * @returns quality want object. + */ + getLocalWant(ssrc?: number): object; + /** + * Gets remote video sink pixel count for a user. + * @param userId user id. + * @returns pixel count. + */ + getRemoteVideoSinkPixelCount(userId: string): number; + /** + * Gets remote video sink wants for a user. + * @param userId user id. + * @returns sink wants object. + */ + getRemoteVideoSinkWants(userId: string): object; + /** + * Gets current stream parameters. + * @returns array of stream parameters. + */ + getStreamParameters(): StreamParameter[]; + /** + * Handles desktop source ended event. + * @param reason end reason. + * @param errorCode error code. + */ + handleDesktopSourceEnded(reason: string, errorCode: number): void; + /** + * Handles first frame received. + * @param userId user id. + * @param ssrc ssrc. + * @param stats stats object. + */ + handleFirstFrame(userId: string, ssrc: number, stats: object): void; + /** + * Handles first frame encrypted stats. + * @param stats stats object. + */ + handleFirstFrameEncryptedStats(stats: object): void; + /** + * Handles first frame stats. + * @param stats stats object. + */ + handleFirstFrameStats(stats: object): void; + /** + * Handles MLS failure. + * @param error error string. + * @param code error code. + */ + handleMLSFailure(error: string, code: number): void; + /** + * Handles native mute changed. + * @param muted new mute state. + */ + handleNativeMuteChanged(muted: boolean): void; + /** + * Handles native mute toggled from system. + */ + handleNativeMuteToggled(): void; + /** + * Handles new listener for native events. + * @param event event name. + */ + handleNewListenerNative(event: string): void; + /** + * Handles no input detected. + * @param hasInput whether input is detected. + */ + handleNoInput(hasInput: boolean): void; + /** + * Handles ping response. + * @param latency latency in ms. + * @param hostname hostname. + * @param port port number. + */ + handlePing(latency: number, hostname: string, port: number): void; + /** + * Handles ping timeout. + * @param hostname hostname. + * @param port port number. + * @param attempts attempt count. + * @param timeout timeout in ms. + */ + handlePingTimeout(hostname: string, port: number, attempts: number, timeout: number): void; + /** + * Handles RTCP message. + * @param type message type. + * @param data message data. + */ + handleRTCPMessage(type: string, data: string): void; + /** + * Handles soundshare attached. + * @param attached whether attached. + */ + handleSoundshare(attached: boolean): void; + /** + * Handles soundshare ended. + */ + handleSoundshareEnded(): void; + /** + * Handles soundshare failed. + * @param failureCode failure code. + * @param failureReason failure reason. + * @param willRetry whether it will retry. + */ + handleSoundshareFailed(failureCode: number, failureReason: string, willRetry: boolean): void; + /** + * Handles speaking flags change. + * @param userId user id. + * @param flags speaking flags. + * @param ssrc ssrc. + */ + handleSpeakingFlags(userId: string, flags: number, ssrc: number): void; + /** + * Handles native speaking event. + * @param userId user id. + * @param speaking speaking state or flags. + * @param ssrc ssrc. + */ + handleSpeakingNative(userId: string, speaking: boolean | number, ssrc: number): void; + /** + * Handles speaking while muted event. + */ + handleSpeakingWhileMuted(): void; + /** + * Handles stats received. + * @param stats stats object. + */ + handleStats(stats: object): void; + /** + * Handles video stream update. + * @param userId user id. + * @param ssrc ssrc. + * @param active whether active. + * @param streams stream array. + */ + handleVideo(userId: string, ssrc: number, active: boolean, streams: object[]): void; + /** + * Handles video encoder fallback. + * @param codecName codec that failed. + */ + handleVideoEncoderFallback(codecName: string): void; + /** + * Initializes stream parameters. + * @param parameters initial parameters. + */ + initializeStreamParameters(parameters: StreamParameter[]): void; + /** + * Callback when desktop encoding options are set. + * @param width width. + * @param height height. + * @param framerate framerate. + */ + onDesktopEncodingOptionsSet(width: number, height: number, framerate: number): void; + /** + * Overwrites quality for testing. + * @param quality quality value. + */ + overwriteQualityForTesting(quality: number): void; + /** + * Sets the connection state. + * @param state new connection state. + */ + setConnectionState(state: ConnectionState): void; + /** + * Sets an experiment flag. + * @param flag flag name. + * @param enabled whether enabled. + */ + setExperimentFlag(flag: string, enabled: boolean): void; + /** + * Sets whether to use electron video. + * @param use whether to use. + */ + setUseElectronVideo(use: boolean): void; + /** + * Updates video quality settings. + * @param ssrc optional ssrc to update. + */ + updateVideoQuality(ssrc?: number): void; +} + +/** + * Low-level media engine for audio/video processing. + * Handles device enumeration, encoding/decoding, and connections. + */ +export interface MediaEngine { + /** camera preview component. */ + Camera: React.ComponentType<{ disabled?: boolean; deviceId?: string; width?: number; height?: number; }>; + /** video display component. */ + Video: React.ComponentType & { onContainerResized: () => void; }; + /** set of active voice/video connections. */ + connections: Set; + + /** + * Registers a listener for device changes. + * @param event event name. + * @param listener callback receiving device lists. + */ + on(event: "DeviceChange", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this; + /** + * Registers a listener for volume changes. + * @param event event name. + * @param listener callback receiving input and output volumes. + */ + on(event: "VolumeChange", listener: (inputVolume: number, outputVolume: number) => void): this; + /** + * Registers a listener for voice activity. + * @param event event name. + * @param listener callback receiving user id and activity level. + */ + on(event: "VoiceActivity", listener: (userId: string, voiceActivity: number) => void): this; + /** + * Registers a listener for desktop source end. + * @param event event name. + * @param listener callback receiving reason and error code. + */ + on(event: "DesktopSourceEnd", listener: (reason: string, errorCode: number) => void): this; + /** + * Registers a listener for audio permission changes. + * @param event event name. + * @param listener callback receiving granted state. + */ + on(event: "AudioPermission", listener: (granted: boolean) => void): this; + /** + * Registers a listener for video permission changes. + * @param event event name. + * @param listener callback receiving granted state. + */ + on(event: "VideoPermission", listener: (granted: boolean) => void): this; + /** + * Registers a listener for video input initialization. + * @param event event name. + * @param listener callback receiving initialization info. + */ + on(event: "VideoInputInitialized", listener: (info: VideoInputInitializationInfo) => void): this; + /** + * Registers a listener for audio input initialization. + * @param event event name. + * @param listener callback receiving initialization info. + */ + on(event: "AudioInputInitialized", listener: (info: AudioInputInitializationInfo) => void): this; + /** + * Registers a listener for clips init failure. + * @param event event name. + * @param listener callback receiving error message and app name. + */ + on(event: "ClipsInitFailure", listener: (errorMessage: string, applicationName: string) => void): this; + /** + * Registers a listener for clips recording ended. + * @param event event name. + * @param listener callback receiving source id and soundshare id. + */ + on(event: "ClipsRecordingEnded", listener: (sourceId: string, soundshareId: number) => void): this; + /** + * Registers a listener for native screen share picker update. + * @param event event name. + * @param listener callback receiving existing state and content. + */ + on(event: "NativeScreenSharePickerUpdate", listener: (existing: boolean, content: string) => void): this; + /** + * Registers a listener for native screen share picker cancel. + * @param event event name. + * @param listener callback receiving existing state. + */ + on(event: "NativeScreenSharePickerCancel", listener: (existing: boolean) => void): this; + /** + * Registers a listener for native screen share picker error. + * @param event event name. + * @param listener callback receiving error string. + */ + on(event: "NativeScreenSharePickerError", listener: (error: string) => void): this; + /** + * Registers a listener for audio device module error. + * @param event event name. + * @param listener callback receiving module, code and device name. + */ + on(event: "AudioDeviceModuleError", listener: (module: string, code: number, deviceName: string) => void): this; + /** + * Registers a listener for video codec error. + * @param event event name. + * @param listener callback receiving error info. + */ + on(event: "VideoCodecError", listener: (info: VideoCodecErrorInfo) => void): this; + /** + * Registers a listener for system microphone mode change. + * @param event event name. + * @param listener callback receiving new mode. + */ + on(event: "SystemMicrophoneModeChange", listener: (mode: string) => void): this; + /** + * Registers a listener for events without arguments. + * @param event event name. + * @param listener callback with no arguments. + */ + on(event: "Destroy" | "Silence" | "WatchdogTimeout" | "ClipsRecordingRestartNeeded" | "VoiceFiltersFailed", listener: () => void): this; + /** + * Registers a one-time listener for device changes. + * @param event event name. + * @param listener callback receiving device lists. + */ + once(event: "DeviceChange", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this; + /** + * Registers a one-time listener for volume changes. + * @param event event name. + * @param listener callback receiving input and output volumes. + */ + once(event: "VolumeChange", listener: (inputVolume: number, outputVolume: number) => void): this; + /** + * Registers a one-time listener for events without arguments. + * @param event event name. + * @param listener callback with no arguments. + */ + once(event: "Destroy" | "Silence" | "WatchdogTimeout" | "ClipsRecordingRestartNeeded" | "VoiceFiltersFailed", listener: () => void): this; + /** + * Removes a listener for device changes. + * @param event event name. + * @param listener callback to remove. + */ + off(event: "DeviceChange", listener: (inputDevices: AudioDevice[], outputDevices: AudioDevice[], videoDevices: VideoDevice[]) => void): this; + /** + * Removes a listener for volume changes. + * @param event event name. + * @param listener callback to remove. + */ + off(event: "VolumeChange", listener: (inputVolume: number, outputVolume: number) => void): this; + /** + * Removes a listener for events without arguments. + * @param event event name. + * @param listener callback to remove. + */ + off(event: "Destroy" | "Silence" | "WatchdogTimeout" | "ClipsRecordingRestartNeeded" | "VoiceFiltersFailed", listener: () => void): this; + /** + * Removes all listeners for an event. + * @param event event name, or all if omitted. + */ + removeAllListeners(event?: MediaEngineEvent): this; + /** + * Gets the number of listeners for an event. + * @param event event name. + * @returns listener count. + */ + listenerCount(event: MediaEngineEvent): number; + + /** + * Applies video background filter settings. + * @param settings filter settings to apply. + */ + applyMediaFilterSettings(settings: MediaFilterSettings): Promise; + /** + * Creates a new voice connection. + * @param userId user id for the connection. + * @param channelId channel to connect to. + * @param options connection options. + * @returns the created connection. + */ + connect(userId: string, channelId: string, options: ConnectionOptions): MediaEngineConnection; + /** + * Checks if there are no active connections. + * @returns true if no connections exist. + */ + connectionsEmpty(): boolean; + /** + * Creates a replay connection from a file. + * @param userId user id for the connection. + * @param options replay options including file path. + * @returns the created connection or null if unsupported. + */ + createReplayConnection(userId: string, options: ReplayConnectionOptions): MediaEngineConnection | null; + /** destroys the media engine and all connections. */ + destroy(): void; + /** + * Iterates over all connections. + * @param callback called for each connection. + * @param context optional context filter, only iterates connections in this context. + */ + eachConnection(callback: (connection: MediaEngineConnection) => void, context?: MediaEngineContextType): void; + /** + * Enables the media engine. + * @returns promise that resolves when enabled. + */ + enable(): Promise; + /** + * Exports a clip as a blob. + * @param clipId clip identifier. + * @param userId user who owns the clip. + * @returns promise resolving to the clip blob. + */ + exportClip(clipId: string, userId: string): Promise; + /** + * Fetches async resources like DAVE keys. + * @param options fetch options. + */ + fetchAsyncResources(options: { fetchDave?: boolean; }): Promise; + + /** + * Gets available audio input devices. + * @returns promise resolving to device list. + */ + getAudioInputDevices(): Promise; + /** + * Gets the current audio layer name. + * @returns audio layer identifier. + */ + getAudioLayer(): string; + /** + * Gets available audio output devices. + * @returns promise resolving to device list. + */ + getAudioOutputDevices(): Promise; + /** + * Gets the current audio subsystem. + * @returns active audio subsystem. + */ + getAudioSubsystem(): AudioSubsystem; + /** + * Gets codec capabilities as JSON string. + * @param callback called with capabilities string. + */ + getCodecCapabilities(callback: (capabilities: string) => void): void; + /** + * Gets a survey of supported codecs. + * @returns promise resolving to codec info. + */ + getCodecSurvey(): Promise<{ codecs: CodecInfo[]; }>; + /** + * Gets whether debug logging is enabled. + * @returns true if enabled. + */ + getDebugLogging(): boolean; + /** + * Gets the current desktop source. + * @returns promise that rejects with NO_STREAM error if not streaming. + */ + getDesktopSource(): Promise; + /** + * Gets whether loopback is active. + * @returns always false for native engine. + */ + getLoopback(): boolean; + /** + * Gets MLS signing key for e2ee. + * @param userId user id. + * @param guildId guild id. + * @returns promise resolving to key and signature. + */ + getMLSSigningKey(userId: string, guildId: string): Promise; + /** + * Gets noise cancellation statistics. + * @returns promise resolving to stats or null if disabled. + */ + getNoiseCancellationStats(): Promise; + /** + * Gets screen preview thumbnails. + * @param width thumbnail width. + * @param height thumbnail height. + * @returns promise resolving to preview list. + */ + getScreenPreviews(width: number, height: number): Promise; + /** + * Gets supported bandwidth estimation experiments. + * @param callback called with experiment list. + */ + getSupportedBandwidthEstimationExperiments(callback: (experiments: string[]) => void): void; + /** + * Gets supported secure frames protocol version. + * @returns protocol version number. + */ + getSupportedSecureFramesProtocolVersion(): number; + /** + * Gets supported video codecs. + * @param callback called with codec name list. + */ + getSupportedVideoCodecs(callback: (codecs: string[]) => void): void; + /** + * Gets system microphone mode. + * @returns promise resolving to mode string. + */ + getSystemMicrophoneMode(): Promise; + /** + * Gets current video input device id. + * @returns device id or "disabled". + */ + getVideoInputDeviceId(): string; + /** + * Gets available video input devices. + * @returns promise resolving to device list. + */ + getVideoInputDevices(): Promise; + /** + * Gets window preview thumbnails. + * @param width thumbnail width. + * @param height thumbnail height. + * @returns promise resolving to preview list. + */ + getWindowPreviews(width: number, height: number): Promise; + + /** signals user interaction to enable autoplay. */ + interact(): void; + /** + * Shows native screen share picker. + * @param options picker options. + */ + presentNativeScreenSharePicker(options?: string): void; + /** + * Queues an audio subsystem switch. + * @param subsystem subsystem to switch to. + */ + queueAudioSubsystem(subsystem: AudioSubsystem): void; + /** + * Ranks RTC regions by latency. + * @param regions region ids to test. + * @returns promise resolving to sorted region ids. + */ + rankRtcRegions(regions: string[]): Promise; + /** releases native desktop video source picker stream. */ + releaseNativeDesktopVideoSourcePickerStream(): void; + + /** + * Saves a clip. + * @param clipId clip identifier. + * @param userId user who owns the clip. + * @returns promise resolving to saved clip info. + */ + saveClip(clipId: string, userId: string): Promise; + /** + * Saves a clip for another user. + * @param clipId clip identifier. + * @param userId user to save for. + * @param options clip metadata. + * @returns promise resolving to saved clip info. + */ + saveClipForUser(clipId: string, userId: string, options: ClipMetadata): Promise; + /** + * Saves a screenshot. + * @param channelId channel context. + * @param userId user context. + * @param width width or null for auto. + * @param height height or null for auto. + * @param options screenshot metadata. + * @returns promise resolving to screenshot info. + */ + saveScreenshot(channelId: string, userId: string, width: number | null, height: number | null, options: ClipMetadata): Promise; + + /** + * Enables or disables AEC dump. + * @param enabled whether to enable. + */ + setAecDump(enabled: boolean): void; + /** + * Sets callback for async clips source deinit. + * @param callback callback function. + */ + setAsyncClipsSourceDeinit(callback: () => void): void; + /** + * Sets callback for async video input device init. + * @param callback callback function. + */ + setAsyncVideoInputDeviceInit(callback: () => void): void; + /** + * Sets whether to bypass system audio input processing. + * @param bypass whether to bypass. + */ + setAudioInputBypassSystemProcessing(bypass: boolean): void; + /** + * Sets the audio input device. + * @param deviceId device identifier. + */ + setAudioInputDevice(deviceId: string): void; + /** + * Sets the audio output device. + * @param deviceId device identifier. + */ + setAudioOutputDevice(deviceId: string): void; + /** + * Sets the audio subsystem. + * @param subsystem subsystem to use. + */ + setAudioSubsystem(subsystem: AudioSubsystem): void; + /** + * Enables or disables AV1 codec. + * @param enabled whether to enable. + */ + setAv1Enabled(enabled: boolean): void; + /** + * Sets clip buffer length in seconds. + * @param seconds buffer duration. + */ + setClipBufferLength(seconds: number): void; + /** + * Enables or disables clips ML pipeline. + * @param enabled whether to enable. + */ + setClipsMLPipelineEnabled(enabled: boolean): void; + /** + * Enables or disables a clips ML pipeline type. + * @param type pipeline type. + * @param enabled whether to enable. + */ + setClipsMLPipelineTypeEnabled(type: string, enabled: boolean): void; + /** + * Sets clips quality settings. + * @param resolution resolution height. + * @param frameRate frame rate. + * @param hdr whether HDR is enabled. + * @returns true if settings were applied. + */ + setClipsQualitySettings(resolution: number, frameRate: number, hdr: boolean): boolean; + /** + * Sets or clears the clips source. + * @param source source config or null to clear. + */ + setClipsSource(source: ClipsSource | null): void; + /** + * Enables or disables debug logging. + * @param enabled whether to enable. + */ + setDebugLogging(enabled: boolean): void; + /** + * Sets or clears the go live source. + * @param source source config or null to clear. + * @param context context to apply to, defaults to "default". + */ + setGoLiveSource(source: GoLiveSource | null, context?: MediaEngineContextType): void; + /** + * Enables or disables H264 codec. + * @param enabled whether to enable. + */ + setH264Enabled(enabled: boolean): void; + /** + * Enables or disables H265 codec. + * @param enabled whether to enable. + */ + setH265Enabled(enabled: boolean): void; + /** + * Sets whether device has fullband performance. + * @param has whether it has fullband performance. + */ + setHasFullbandPerformance(has: boolean): void; + /** + * Sets input volume. + * @param volume volume 0-100. + */ + setInputVolume(volume: number): void; + /** + * Enables loopback for testing. + * @param reason reason for enabling loopback. + * @param options loopback audio options. + */ + setLoopback(reason: string, options: LoopbackOptions): void; + /** + * Sets max sync delay override. + * @param delay delay in milliseconds. + */ + setMaxSyncDelayOverride(delay: number): void; + /** + * Sets maybe preprocess mute state. + * @param mute whether to mute. + */ + setMaybePreprocessMute(mute: boolean): void; + /** + * Sets native desktop video source picker active state. + * @param active whether picker is active. + */ + setNativeDesktopVideoSourcePickerActive(active: boolean): void; + /** + * Enables or disables noise cancellation stats. + * @param enabled whether to enable. + */ + setNoiseCancellationEnableStats(enabled: boolean): void; + /** + * Sets whether to offload ADM controls. + * @param offload whether to offload. + */ + setOffloadAdmControls(offload: boolean): void; + /** + * Sets callback for video container resize. + * @param callback callback function. + */ + setOnVideoContainerResized(callback: () => void): void; + /** + * Sets output volume. + * @param volume volume 0-100. + */ + setOutputVolume(volume: number): void; + /** + * Enables or disables sidechain compression. + * @param enabled whether to enable. + */ + setSidechainCompression(enabled: boolean): void; + /** + * Sets sidechain compression strength. + * @param strength strength 0-100. + */ + setSidechainCompressionStrength(strength: number): void; + /** + * Sets soundshare source. + * @param soundshareId soundshare source id. + * @param active whether to enable. + * @param context context to apply to, defaults to "default". + */ + setSoundshareSource(soundshareId: number, active: boolean, context?: MediaEngineContextType): void; + /** + * Sets the video input device. + * @param deviceId device identifier. + */ + setVideoInputDevice(deviceId: string): Promise; + + /** + * Checks if a connection should broadcast video. + * @param connection connection to check. + * @returns true if should broadcast. + */ + shouldConnectionBroadcastVideo(connection: MediaEngineConnection): boolean; + /** + * Shows system capture configuration UI. + * @param options options including display id. + */ + showSystemCaptureConfigurationUI(options: { displayId?: string; }): void; + + /** starts AEC dump recording. */ + startAecDump(): void; + /** + * Starts local audio recording. + * @param options recording options. + */ + startLocalAudioRecording(options: AudioRecordingOptions): Promise; + /** + * Starts recording raw audio samples. + * @param options sample options. + */ + startRecordingRawSamples(options: RawSamplesOptions): void; + /** stops AEC dump recording. */ + stopAecDump(): void; + /** + * Stops local audio recording. + * @param callback called with success and filepath. + */ + stopLocalAudioRecording(callback: (success: boolean, filepath: string) => void): void; + /** stops recording raw audio samples. */ + stopRecordingRawSamples(): void; + + /** + * Checks if media engine is supported. + * @returns true if supported. + */ + supported(): boolean; + /** + * Checks if a feature is supported. + * @param feature feature to check. + * @returns true if supported. + */ + supports(feature: MediaEngineFeature): boolean; + /** + * Updates clip metadata. + * @param clipId clip identifier. + * @param metadata new metadata. + */ + updateClipMetadata(clipId: string, metadata: ClipMetadata): Promise; + /** ticks the watchdog timer. */ + watchdogTick(): void; + /** + * Writes audio debug state to file. + * @returns promise that resolves when written. + */ + writeAudioDebugState(): Promise; +} + +/** + * Persisted media engine settings for a context. + */ +export interface MediaEngineSettings { + /** current voice mode (PTT or VAD), default VOICE_ACTIVITY. */ + mode: VoiceMode; + /** voice mode configuration options. */ + modeOptions: ModeOptions; + /** settings version for vadUseKrisp migration. */ + vadUseKrispSettingVersion: number; + /** settings version for ncUseKrisp migration. */ + ncUseKrispSettingVersion: number; + /** settings version for ncUseKrispjs migration. */ + ncUseKrispjsSettingVersion: number; + /** whether self is muted, default false. */ + mute: boolean; + /** whether self is deafened, default false. */ + deaf: boolean; + /** whether echo cancellation is enabled, default false. */ + echoCancellation: boolean; + /** whether noise suppression is enabled, default false. */ + noiseSuppression: boolean; + /** whether automatic gain control is enabled, default true. */ + automaticGainControl: boolean; + /** whether krisp noise cancellation is enabled, default false. */ + noiseCancellation: boolean; + /** whether to bypass system audio input processing, default false. */ + bypassSystemInputProcessing: boolean; + /** most recently requested voice filter id, null if none. */ + mostRecentlyRequestedVoiceFilter: string | null; + /** whether voice filter playback is enabled, default false. */ + voiceFilterPlaybackEnabled: boolean; + /** version for hardware enabled migration. */ + hardwareEnabledVersion: number; + /** whether silence warning is enabled, default true. */ + silenceWarning: boolean; + /** attenuation level 0-100 for other users when speaking, default 0. */ + attenuation: number; + /** whether to attenuate others when self is speaking, default false. */ + attenuateWhileSpeakingSelf: boolean; + /** whether to attenuate others when others are speaking, default true. */ + attenuateWhileSpeakingOthers: boolean; + /** per-user local mute states, keyed by user id. */ + localMutes: { [userId: string]: boolean; }; + /** per-user disabled local video states, keyed by user id. */ + disabledLocalVideos: { [userId: string]: boolean; }; + /** per-user video toggle states, keyed by user id. */ + videoToggleStateMap: { [userId: string]: VideoToggleState; }; + /** per-user local volume levels 0-200, keyed by user id, default 100. */ + localVolumes: { [userId: string]: number; }; + /** per-user stereo pan settings, keyed by user id. */ + localPans: { [userId: string]: LocalPan; }; + /** microphone input volume 0-100, default 100. */ + inputVolume: number; + /** speaker output volume 0-100, default 100. */ + outputVolume: number; + /** selected audio input device id. */ + inputDeviceId: string; + /** selected audio output device id. */ + outputDeviceId: string; + /** selected video input device id. */ + videoDeviceId: string; + /** whether QoS packet priority is enabled. */ + qos: boolean; + /** whether QoS has been migrated. */ + qosMigrated: boolean; + /** whether video hook is enabled. */ + videoHook: boolean; + /** experimental soundshare setting, null if not set. */ + experimentalSoundshare2: boolean | null; + /** system screenshare picker setting, null if not set. */ + useSystemScreensharePicker: boolean | null; + /** whether H265 codec is enabled. */ + h265Enabled: boolean; + /** whether VAD threshold has been migrated. */ + vadThrehsoldMigrated: boolean; + /** whether AEC dump is enabled. */ + aecDumpEnabled: boolean; + /** whether sidechain compression is enabled. */ + sidechainCompression: boolean; + /** settings version for sidechain compression migration. */ + sidechainCompressionSettingVersion: number; + /** sidechain compression strength 0-100, default 50. */ + sidechainCompressionStrength: number; + /** whether automatic audio subsystem selection is enabled. */ + automaticAudioSubsystem: boolean; + /** active input profile or null. */ + activeInputProfile: InputProfile | null; +} + +/** + * Complete serializable state of MediaEngineStore. + */ +export interface MediaEngineState { + /** settings for each context type, keyed by context. */ + settingsByContext: { [context in MediaEngineContextType]: MediaEngineSettings; }; + /** available audio input devices, keyed by device id. */ + inputDevices: { [deviceId: string]: AudioDevice; }; + /** available audio output devices, keyed by device id. */ + outputDevices: { [deviceId: string]: AudioDevice; }; + /** supported features, keyed by feature name. */ + appSupported: { [feature in MediaEngineFeature]?: boolean; }; + /** whether krisp module is loaded. */ + krispModuleLoaded: boolean; + /** krisp module version or undefined. */ + krispVersion: string | undefined; + /** krisp suppression level or undefined. */ + krispSuppressionLevel: number | undefined; + /** current go live source or undefined. */ + goLiveSource: GoLiveSource | undefined; + /** context for go live. */ + goLiveContext: MediaEngineContextType; +} + +/** + * Keyboard shortcut binding. + */ +export interface Shortcut { + /** action the shortcut triggers. */ + action: string; + /** keys in the shortcut combination. */ + shortcut: string[]; +} + +/** + * Flux store managing audio/video settings, devices, and the media engine. + * Handles voice activity detection, noise cancellation, device selection, + * and go live streaming configuration. + */ export class MediaEngineStore extends FluxStore { + /** fetches async resources like DAVE keys. */ + fetchAsyncResources(): void; + /** + * Gets the active input profile. + * @returns current input profile. + */ + getActiveInputProfile(): InputProfile; + /** + * Gets the active voice filter id. + * @returns voice filter id or null if none active. + */ + getActiveVoiceFilter(): string | null; + /** + * Gets when the active voice filter was applied. + * @returns application date or null if none active. + */ + getActiveVoiceFilterAppliedAt(): Date | null; + /** + * Gets whether AEC dump is enabled. + * @returns true if enabled. + */ + getAecDump(): boolean; + /** + * Gets whether to attenuate while others are speaking. + * @returns true if enabled. + */ + getAttenuateWhileSpeakingOthers(): boolean; + /** + * Gets whether to attenuate while self is speaking. + * @returns true if enabled. + */ + getAttenuateWhileSpeakingSelf(): boolean; + /** + * Gets the attenuation level. + * @returns attenuation 0-100, default 0. + */ + getAttenuation(): number; + /** + * Gets the current audio subsystem. + * @returns active audio subsystem. + */ + getAudioSubsystem(): AudioSubsystem; + /** + * Gets whether automatic gain control is enabled. + * @returns true if enabled. + */ + getAutomaticGainControl(): boolean; + /** + * Gets whether system audio input processing is bypassed. + * @returns true if bypassed. + */ + getBypassSystemInputProcessing(): boolean; + /** + * Gets the camera preview component. + * @returns react component for camera preview. + */ + getCameraComponent(): React.ComponentType; + /** + * Gets whether debug logging is enabled. + * @returns true if enabled. + */ + getDebugLogging(): boolean; + /** + * Gets whether echo cancellation is enabled. + * @returns true if enabled. + */ + getEchoCancellation(): boolean; + /** + * Gets whether silence warning is enabled. + * @returns true if enabled. + */ + getEnableSilenceWarning(): boolean; + /** + * Gets whether user has ever spoken while muted. + * @returns true if has spoken while muted. + */ + getEverSpeakingWhileMuted(): boolean; + /** + * Gets whether experimental soundshare is enabled. + * @returns true if enabled. + */ + getExperimentalSoundshare(): boolean; + /** + * Gets the go live context. + * @returns current go live context. + */ + getGoLiveContext(): MediaEngineContextType; + /** + * Gets the current go live source. + * @returns go live source or null if not streaming. + */ + getGoLiveSource(): GoLiveSource | null; + /** + * Gets the GPU brand name. + * @returns GPU brand string. + */ + getGpuBrand(): string; + /** + * Gets whether H265 is enabled. + * @returns true if enabled. + */ + getH265Enabled(): boolean; + /** + * Gets whether hardware encoding is enabled. + * @returns true if enabled. + */ + getHardwareEncoding(): boolean; + /** + * Gets whether audio input is detected. + * @returns true if detected, false if not, null if unknown. + */ + getInputDetected(): boolean | null; + /** + * Gets the selected audio input device id. + * @returns device id. + */ + getInputDeviceId(): string; + /** + * Gets available audio input devices. + * @returns devices keyed by device id. + */ + getInputDevices(): { [deviceId: string]: AudioDevice; }; + /** + * Gets the input volume. + * @returns volume 0-100, default 100. + */ + getInputVolume(): number; + /** + * Gets whether krisp stats are enabled. + * @returns true if enabled. + */ + getKrispEnableStats(): boolean; + /** + * Gets the krisp model override. + * @returns model name or undefined if not set. + */ + getKrispModelOverride(): string | undefined; + /** + * Gets available krisp models. + * @returns array of model names. + */ + getKrispModels(): string[]; + /** + * Gets the krisp suppression level. + * @returns suppression level 0-100, default 100. + */ + getKrispSuppressionLevel(): number; + /** + * Gets the krisp VAD activation threshold. + * @returns threshold 0-1, default 0.8. + */ + getKrispVadActivationThreshold(): number; + /** + * Gets the timestamp of the last audio input device change. + * @returns timestamp in milliseconds. + */ + getLastAudioInputDeviceChangeTimestamp(): number; + /** + * Gets the stereo pan for a user. + * @param userId user to get pan for. + * @param context settings context, defaults to "default". + * @returns pan with left/right 0-1, default {left: 1, right: 1}. + */ + getLocalPan(userId: string, context?: MediaEngineContextType): LocalPan; + /** + * Gets the volume for a user. + * @param userId user to get volume for. + * @param context settings context, defaults to "default". + * @returns volume 0-200, default 100. + */ + getLocalVolume(userId: string, context?: MediaEngineContextType): number; + /** + * Gets whether loopback is enabled. + * @returns true if enabled. + */ + getLoopback(): boolean; + /** + * Gets the reasons loopback is enabled. + * @returns set of reason strings. + */ + getLoopbackReasons(): Set; + /** + * Gets the media engine instance. + * @returns the media engine. + */ + getMediaEngine(): MediaEngine; + /** + * Gets MLS signing key for e2ee. + * @param userId user id. + * @param guildId guild id. + * @returns promise resolving to key and signature. + */ + getMLSSigningKey(userId: string, guildId: string): Promise; + /** + * Gets the current voice mode. + * @param context settings context, defaults to "default". + * @returns current voice mode. + */ + getMode(context?: MediaEngineContextType): VoiceMode; + /** + * Gets the mode options. + * @param context settings context, defaults to "default". + * @returns current mode options. + */ + getModeOptions(context?: MediaEngineContextType): ModeOptions; + /** + * Gets the most recently requested voice filter. + * @returns voice filter id or null if none. + */ + getMostRecentlyRequestedVoiceFilter(): string | null; + /** + * Gets whether no input detected notice is shown. + * @returns true if shown. + */ + getNoInputDetectedNotice(): boolean; + /** + * Gets whether noise cancellation is enabled. + * @returns true if enabled. + */ + getNoiseCancellation(): boolean; + /** + * Gets whether noise suppression is enabled. + * @returns true if enabled. + */ + getNoiseSuppression(): boolean; + /** + * Gets the selected audio output device id. + * @returns device id. + */ + getOutputDeviceId(): string; + /** + * Gets available audio output devices. + * @returns devices keyed by device id. + */ + getOutputDevices(): { [deviceId: string]: AudioDevice; }; + /** + * Gets the output volume. + * @returns volume 0-100, default 100. + */ + getOutputVolume(): number; + /** + * Gets the packet delay. + * @returns delay in milliseconds. + */ + getPacketDelay(): number; + /** + * Gets the previous voice filter. + * @returns voice filter id or null if none. + */ + getPreviousVoiceFilter(): string | null; + /** + * Gets when the previous voice filter was applied. + * @returns application date or null if none. + */ + getPreviousVoiceFilterAppliedAt(): Date | null; + /** + * Gets whether QoS is enabled. + * @returns true if enabled. + */ + getQoS(): boolean; + /** + * Gets the settings for a context. + * @param context settings context, defaults to "default". + * @returns current settings. + */ + getSettings(context?: MediaEngineContextType): MediaEngineSettings; + /** + * Gets registered shortcuts. + * @returns shortcuts keyed by action. + */ + getShortcuts(): { [action: string]: Shortcut; }; + /** + * Gets whether sidechain compression is enabled. + * @returns true if enabled. + */ + getSidechainCompression(): boolean; + /** + * Gets the sidechain compression strength. + * @returns strength 0-100, default 50. + */ + getSidechainCompressionStrength(): number; + /** + * Gets whether currently speaking while muted. + * @returns true if speaking while muted. + */ + getSpeakingWhileMuted(): boolean; + /** + * Gets the complete store state. + * @returns current state. + */ + getState(): MediaEngineState; + /** + * Gets supported secure frames protocol version. + * @returns protocol version number. + */ + getSupportedSecureFramesProtocolVersion(): number; + /** + * Gets the system microphone mode. + * @returns mode string or undefined if not available. + */ + getSystemMicrophoneMode(): string | undefined; + /** + * Gets whether gamescope capture is used. + * @returns true if used. + */ + getUseGamescopeCapture(): boolean; + /** + * Gets whether system screenshare picker is used. + * @returns true if used. + */ + getUseSystemScreensharePicker(): boolean; + /** + * Gets whether VA-API encoder is used. + * @returns true if used. + */ + getUseVaapiEncoder(): boolean; + /** + * Gets the video display component. + * @returns react component for video display. + */ + getVideoComponent(): React.ComponentType; + /** + * Gets the selected video device id. + * @returns device id. + */ + getVideoDeviceId(): string; + /** + * Gets available video devices. + * @returns devices keyed by device id. + */ + getVideoDevices(): { [deviceId: string]: VideoDevice; }; + /** + * Gets whether video hook is enabled. + * @returns true if enabled. + */ + getVideoHook(): boolean; + /** + * Gets video stream parameters. + * @param context settings context, defaults to "default". + * @returns array of stream parameters. + */ + getVideoStreamParameters(context?: MediaEngineContextType): VideoStreamParameter[]; + /** + * Gets the video toggle state for a user. + * @param userId user to check. + * @param context settings context, defaults to "default". + * @returns toggle state, NONE if not in map. + */ + getVideoToggleState(userId: string, context?: MediaEngineContextType): VideoToggleState; + /** + * Gets whether voice filter playback is enabled. + * @returns true if enabled. + */ + getVoiceFilterPlaybackEnabled(): boolean; + + /** + * Gets whether go live simulcast is enabled. + * @returns true if enabled. + */ + goLiveSimulcastEnabled(): boolean; + + /** + * Checks if there is an active CallKit call. + * @returns true if active. + */ + hasActiveCallKitCall(): boolean; + /** + * Checks if there is a clips source. + * @returns true if has source. + */ + hasClipsSource(): boolean; + /** + * Checks if a context exists. + * @param context context to check. + * @returns true if exists. + */ + hasContext(context: MediaEngineContextType): boolean; + /** + * Checks if H265 hardware decode is available. + * @returns true if available. + */ + hasH265HardwareDecode(): boolean; + + /** + * Checks if advanced voice activity is supported. + * @returns true if supported. + */ isAdvancedVoiceActivitySupported(): boolean; + /** + * Checks if AEC dump is supported. + * @returns true if supported. + */ isAecDumpSupported(): boolean; - isAnyLocalVideoAutoDisabled(): boolean; + /** + * Checks if any local video is auto disabled. + * @param context settings context, defaults to "default". + * @returns true if any auto disabled. + */ + isAnyLocalVideoAutoDisabled(context?: MediaEngineContextType): boolean; + /** + * Checks if automatic gain control is supported. + * @returns true if supported. + */ isAutomaticGainControlSupported(): boolean; + /** + * Checks if self is deafened. + * @returns true if deafened. + */ isDeaf(): boolean; + /** + * Checks if hardware mute notice is enabled. + * @returns true if enabled. + */ isEnableHardwareMuteNotice(): boolean; + /** + * Checks if media engine is enabled. + * @returns true if enabled. + */ isEnabled(): boolean; - isExperimentalEncodersSupported(): boolean; - isHardwareMute(): boolean; + /** + * Checks if hardware mute is active. + * @param context settings context, defaults to "default". + * @returns true if hardware muted. + */ + isHardwareMute(context?: MediaEngineContextType): boolean; + /** + * Checks if input profile is custom. + * @returns true if custom. + */ isInputProfileCustom(): boolean; + /** + * Checks if user interaction is required. + * @returns true if required. + */ isInteractionRequired(): boolean; - isLocalMute(): boolean; - isLocalMute(userId): boolean; - isLocalVideoAutoDisabled(userId): boolean; - isLocalVideoDisabled(userId): boolean; - isLocalVideoDisabled(): boolean; + /** + * Checks if a user is locally muted. + * @param userId user to check. + * @param context settings context, defaults to "default". + * @returns true if muted. + */ + isLocalMute(userId: string, context?: MediaEngineContextType): boolean; + /** + * Checks if a user's video is auto disabled. + * @param userId user to check. + * @param context settings context, defaults to "default". + * @returns true if auto disabled. + */ + isLocalVideoAutoDisabled(userId: string, context?: MediaEngineContextType): boolean; + /** + * Checks if a user's video is disabled. + * @param userId user to check. + * @param context settings context, defaults to "default". + * @returns true if disabled. + */ + isLocalVideoDisabled(userId: string, context?: MediaEngineContextType): boolean; + /** + * Checks if media filter settings are loading. + * @returns true if loading. + */ isMediaFilterSettingLoading(): boolean; + /** + * Checks if self is muted. + * @returns true if muted. + */ isMute(): boolean; + /** + * Checks if native audio permission is ready. + * @returns true if ready. + */ isNativeAudioPermissionReady(): boolean; + /** + * Checks if there was a noise cancellation error. + * @returns true if error occurred. + */ isNoiseCancellationError(): boolean; + /** + * Checks if noise cancellation is supported. + * @returns true if supported. + */ isNoiseCancellationSupported(): boolean; + /** + * Checks if noise suppression is supported. + * @returns true if supported. + */ isNoiseSuppressionSupported(): boolean; - isScreenSharing(): boolean; - isSelfDeaf(): boolean; - isSelfMute(): boolean; - isSelfMutedTemporarily(): boolean; + /** + * Checks if screen sharing is active. + * @param context settings context, defaults to "default". + * @returns true if sharing. + */ + isScreenSharing(context?: MediaEngineContextType): boolean; + /** + * Checks if self is deafened in context. + * @param context settings context, defaults to "default". + * @returns true if deafened. + */ + isSelfDeaf(context?: MediaEngineContextType): boolean; + /** + * Checks if self is muted in context. + * @param context settings context, defaults to "default". + * @returns true if muted. + */ + isSelfMute(context?: MediaEngineContextType): boolean; + /** + * Checks if self is temporarily muted. + * @param context settings context, defaults to "default". + * @returns true if temporarily muted. + */ + isSelfMutedTemporarily(context?: MediaEngineContextType): boolean; + /** + * Checks if simulcast is supported. + * @returns true if supported. + */ isSimulcastSupported(): boolean; - isSoundSharing(): boolean; + /** + * Checks if sound sharing is active. + * @param context settings context, defaults to "default". + * @returns true if sharing. + */ + isSoundSharing(context?: MediaEngineContextType): boolean; + /** + * Checks if media engine is supported. + * @returns true if supported. + */ isSupported(): boolean; + /** + * Checks if video is available. + * @returns true if available. + */ isVideoAvailable(): boolean; + /** + * Checks if video is enabled. + * @returns true if enabled. + */ isVideoEnabled(): boolean; - setAv1Enabled(AV1: boolean): void; - setH265Enabled(H265: boolean): void; - setH264Enabled(H264: boolean): void; + /** notifies that mute/unmute sound was skipped. */ + notifyMuteUnmuteSoundWasSkipped(): void; + /** + * Sets whether a user can have priority speaker. + * @param userId user to set. + * @param canHavePriority whether can have priority. + */ + setCanHavePriority(userId: string, canHavePriority: boolean): void; + /** + * Sets whether there is an active CallKit call. + * @param active whether active. + */ + setHasActiveCallKitCall(active: boolean): void; + /** + * Checks if manual subsystem selection should be offered. + * @returns true if should offer. + */ + shouldOfferManualSubsystemSelection(): boolean; + /** + * Checks if mute/unmute sound should be skipped. + * @returns true if should skip. + */ + shouldSkipMuteUnmuteSound(): boolean; + /** + * Checks if bypass system input processing should be shown. + * @returns true if should show. + */ + showBypassSystemInputProcessing(): boolean; - getAutomaticGainControl(): boolean; - getInputDeviceId(): string; - getMediaEngine(): any; - getCodecCapabilities(e: any): any; + /** starts preloading DAVE encryption. */ + startDavePreload(): void; + + /** + * Checks if a feature is supported. + * @param feature feature to check. + * @returns true if supported. + */ + supports(feature: MediaEngineFeature): boolean; + /** + * Checks if disable local video is supported. + * @returns true if supported. + */ + supportsDisableLocalVideo(): boolean; + /** + * Checks if experimental soundshare is supported. + * @returns true if supported. + */ + supportsExperimentalSoundshare(): boolean; + /** + * Checks if hook soundshare is supported. + * @returns true if supported. + */ + supportsHookSoundshare(): boolean; + /** + * Checks if in-app capture is supported for an app. + * @param appName application name. + * @returns true if supported. + */ + supportsInApp(appName: string): boolean; + /** + * Checks if screen soundshare is supported. + * @returns true if supported. + */ + supportsScreenSoundshare(): boolean; + /** + * Checks if system screenshare picker is supported. + * @returns true if supported. + */ + supportsSystemScreensharePicker(): boolean; + /** + * Checks if video hook is supported. + * @returns true if supported. + */ + supportsVideoHook(): boolean; } diff --git a/packages/discord-types/src/stores/MessageStore.d.ts b/packages/discord-types/src/stores/MessageStore.d.ts index d4823fc81..e8a095e7a 100644 --- a/packages/discord-types/src/stores/MessageStore.d.ts +++ b/packages/discord-types/src/stores/MessageStore.d.ts @@ -1,13 +1,55 @@ -import { MessageJSON, FluxStore, Message } from ".."; +import { FluxStore, Message } from ".."; + +export type JumpType = "ANIMATED" | "INSTANT"; + +export interface MessageCache { + _messages: Message[]; + _map: Record; + _wasAtEdge: boolean; + _isCacheBefore: boolean; +} + +export interface ChannelMessages { + channelId: string; + ready: boolean; + cached: boolean; + jumpType: JumpType; + jumpTargetId: string | null; + jumpTargetOffset: number; + jumpSequenceId: number; + jumped: boolean; + jumpedToPresent: boolean; + jumpFlash: boolean; + jumpReturnTargetId: string | null; + focusTargetId: string | null; + focusSequenceId: number; + initialScrollSequenceId: number; + hasMoreBefore: boolean; + hasMoreAfter: boolean; + loadingMore: boolean; + revealedMessageId: string | null; + hasFetched: boolean; + error: boolean; + _array: Message[]; + _before: MessageCache; + _after: MessageCache; + _map: Record; +} export class MessageStore extends FluxStore { + focusedMessageId(channelId: string): string | undefined; + getLastChatCommandMessage(channelId: string): Message | undefined; + getLastEditableMessage(channelId: string): Message | undefined; + getLastMessage(channelId: string): Message | undefined; + getLastNonCurrentUserMessage(channelId: string): Message | undefined; getMessage(channelId: string, messageId: string): Message; - /** @returns This return object is fucking huge; I'll type it later. */ - getMessages(channelId: string): unknown; - getRawMessages(channelId: string): Record; + /** @see {@link ChannelMessages} */ + getMessages(channelId: string): ChannelMessages; hasCurrentUserSentMessage(channelId: string): boolean; + hasCurrentUserSentMessageSinceAppStart(channelId: string): boolean; hasPresent(channelId: string): boolean; isLoadingMessages(channelId: string): boolean; + isReady(channelId: string): boolean; jumpedMessageId(channelId: string): string | undefined; whenReady(channelId: string, callback: () => void): void; } diff --git a/packages/discord-types/src/stores/NotificationSettingsStore.d.ts b/packages/discord-types/src/stores/NotificationSettingsStore.d.ts new file mode 100644 index 000000000..e82596a7e --- /dev/null +++ b/packages/discord-types/src/stores/NotificationSettingsStore.d.ts @@ -0,0 +1,26 @@ +import { FluxStore } from ".."; + +export type DesktopNotificationType = "ALL" | "ONLY_MENTIONS" | "NEVER"; +export type TTSNotificationType = "ALL" | "ONLY_MENTIONS" | "NEVER"; + +export interface NotificationSettingsState { + desktopType: DesktopNotificationType; + disableAllSounds: boolean; + disabledSounds: string[]; + ttsType: TTSNotificationType; + disableUnreadBadge: boolean; + taskbarFlash: boolean; + notifyMessagesInSelectedChannel: boolean; +} + +export class NotificationSettingsStore extends FluxStore { + get taskbarFlash(): boolean; + getUserAgnosticState(): NotificationSettingsState; + getDesktopType(): DesktopNotificationType; + getTTSType(): TTSNotificationType; + getDisabledSounds(): string[]; + getDisableAllSounds(): boolean; + getDisableUnreadBadge(): boolean; + getNotifyMessagesInSelectedChannel(): boolean; + isSoundDisabled(sound: string): boolean; +} diff --git a/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts b/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts new file mode 100644 index 000000000..b9c3adca4 --- /dev/null +++ b/packages/discord-types/src/stores/OverridePremiumTypeStore.d.ts @@ -0,0 +1,16 @@ +import { FluxStore } from ".."; +import { PremiumType } from "../../enums"; + +export interface OverridePremiumTypeState { + createdAtOverride: Date | undefined; + premiumTypeActual: PremiumType | null; + premiumTypeOverride: PremiumType | undefined; +} + +export class OverridePremiumTypeStore extends FluxStore { + getState(): OverridePremiumTypeState; + getCreatedAtOverride(): Date | undefined; + getPremiumTypeActual(): PremiumType | null; + getPremiumTypeOverride(): PremiumType | undefined; + get premiumType(): PremiumType | undefined; +} diff --git a/packages/discord-types/src/stores/PermissionStore.d.ts b/packages/discord-types/src/stores/PermissionStore.d.ts new file mode 100644 index 000000000..d338ae63b --- /dev/null +++ b/packages/discord-types/src/stores/PermissionStore.d.ts @@ -0,0 +1,57 @@ +import { Channel, Guild, Role, FluxStore } from ".."; + +export interface GuildPermissionProps { + canManageGuild: boolean; + canManageChannels: boolean; + canManageRoles: boolean; + canManageBans: boolean; + canManageNicknames: boolean; + canManageGuildExpressions: boolean; + canViewAuditLog: boolean; + canViewAuditLogV2: boolean; + canManageWebhooks: boolean; + canViewGuildAnalytics: boolean; + canAccessMembersPage: boolean; + isGuildAdmin: boolean; + isOwner: boolean; + isOwnerWithRequiredMfaLevel: boolean; + guild: Guild; +} + +export interface PartialChannelContext { + channelId: string; +} + +export interface PartialGuildContext { + guildId: string; +} + +export type PartialContext = PartialChannelContext | PartialGuildContext; + +type PartialChannel = Channel | { id: string; }; +type PartialGuild = Guild | { id: string; }; + +export class PermissionStore extends FluxStore { + // TODO: finish typing these + can(permission: bigint, channelOrGuild: PartialChannel | PartialGuild, guildId?: string, overwrites?: Record, userId?: string): boolean; + canBasicChannel(permission: bigint, channel: PartialChannel, guildId?: string, overwrites?: Record, userId?: string): boolean; + canWithPartialContext(permission: bigint, context: PartialContext): boolean; + canManageUser(permission: bigint, userOrUserId: string, guild: PartialGuild): boolean; + canAccessGuildSettings(guild: PartialGuild): boolean; + canAccessMemberSafetyPage(guild: PartialGuild): boolean; + canImpersonateRole(guild: PartialGuild, role: Role): boolean; + + // TODO: finish typing + computePermissions(channel: PartialChannel, guildId?: string, overwrites?: Record, userId?: string): bigint; + computeBasicPermissions(channel: PartialChannel): number; + + getChannelPermissions(channel: PartialChannel): bigint; + getGuildPermissions(guild: PartialGuild): bigint; + getGuildPermissionProps(guild: PartialGuild): GuildPermissionProps; + + getHighestRole(guild: PartialGuild): Role | null; + isRoleHigher(guild: PartialGuild, firstRole: Role | null, secondRole: Role | null): boolean; + + getGuildVersion(guildId: string): number; + getChannelsVersion(): number; +} diff --git a/packages/discord-types/src/stores/PresenceStore.d.ts b/packages/discord-types/src/stores/PresenceStore.d.ts index 3f245aca9..c0462dce9 100644 --- a/packages/discord-types/src/stores/PresenceStore.d.ts +++ b/packages/discord-types/src/stores/PresenceStore.d.ts @@ -8,17 +8,29 @@ export interface UserAndActivity { export type DiscordPlatform = "desktop" | "mobile" | "web" | "embedded"; +export interface PresenceStoreState { + presencesForGuilds: Record>; }>>; + statuses: Record; + activities: Record; + filteredActivities: Record; + hiddenActivities: Record; + // TODO: finish typing + activityMetadata: Record; + clientStatuses: Record>>; +} + export class PresenceStore extends FluxStore { findActivity(userId: string, predicate: (activity: Activity) => boolean, guildId?: string): Activity | undefined; getActivities(userId: string, guildId?: string): Activity[]; + // TODO: finish typing getActivityMetadata(userId: string): any; getAllApplicationActivities(applicationId: string): UserAndActivity[]; getApplicationActivity(userId: string, applicationId: string, guildId?: string): Activity | null; getClientStatus(userId: string): Record; - getHiddenActivities(): any; + getHiddenActivities(): Activity[]; /** literally just getActivities(...)[0] */ getPrimaryActivity(userId: string, guildId?: string): Activity | null; - getState(): any; + getState(): PresenceStoreState; getStatus(userId: string, guildId?: string | null, defaultStatus?: OnlineStatus): OnlineStatus; getUnfilteredActivities(userId: string, guildId?: string): Activity[]; getUserIds(): string[]; diff --git a/packages/discord-types/src/stores/RTCConnectionStore.d.ts b/packages/discord-types/src/stores/RTCConnectionStore.d.ts new file mode 100644 index 000000000..3dac744ae --- /dev/null +++ b/packages/discord-types/src/stores/RTCConnectionStore.d.ts @@ -0,0 +1,83 @@ +import { FluxStore } from ".."; + +export type RTCConnectionState = + | "DISCONNECTED" + | "AWAITING_ENDPOINT" + | "AUTHENTICATING" + | "CONNECTING" + | "RTC_DISCONNECTED" + | "RTC_CONNECTING" + | "RTC_CONNECTED" + | "NO_ROUTE" + | "ICE_CHECKING" + | "DTLS_CONNECTING"; + +export type RTCConnectionQuality = "unknown" | "bad" | "average" | "fine"; + +export interface LastRTCConnectionState { + duration: number | null; + mediaSessionId: string | null; + rtcConnectionId: string | null; + wasEverMultiParticipant: boolean; + wasEverRtcConnected: boolean; + // TODO: type + voiceStateAnalytics: any; + channelId: string; +} + +export interface RTCConnectionPacketStats { + inbound: number; + outbound: number; + lost: number; +} + +export interface VoiceStateStats { + max_voice_state_count: number; +} + +export interface SecureFramesState { + version: number; + epochAuthenticator: string; +} + +export interface SecureFramesRosterMapEntry { + pendingVerifyState: number; + verifiedState: number; +} + +export class RTCConnectionStore extends FluxStore { + // TODO: type + getRTCConnection(): any | null; + getState(): RTCConnectionState; + isConnected(): boolean; + isDisconnected(): boolean; + getRemoteDisconnectVoiceChannelId(): string | null; + getLastSessionVoiceChannelId(): string | null; + setLastSessionVoiceChannelId(channelId: string | null): void; + getGuildId(): string | undefined; + getChannelId(): string | undefined; + getHostname(): string; + getQuality(): RTCConnectionQuality; + getPings(): number[]; + getAveragePing(): number; + getLastPing(): number | undefined; + getOutboundLossRate(): number | undefined; + getMediaSessionId(): string | undefined; + getRTCConnectionId(): string | undefined; + getDuration(): number | undefined; + getLastRTCConnectionState(): LastRTCConnectionState | null; + getVoiceFilterSpeakingDurationMs(): number | undefined; + getPacketStats(): RTCConnectionPacketStats | undefined; + getVoiceStateStats(): VoiceStateStats | undefined; + // TODO: finish typing + getUserVoiceSettingsStats(userId: string): any | undefined; + getWasEverMultiParticipant(): boolean; + getWasEverRtcConnected(): boolean; + getUserIds(): string[] | undefined; + getJoinVoiceId(): string | null; + isUserConnected(userId: string): boolean | undefined; + getSecureFramesState(): SecureFramesState | undefined; + getSecureFramesRosterMapEntry(oderId: string): SecureFramesRosterMapEntry | undefined; + getLastNonZeroRemoteVideoSinkWantsTime(): number | null; + getWasMoved(): boolean; +} diff --git a/packages/discord-types/src/stores/ReadStateStore.d.ts b/packages/discord-types/src/stores/ReadStateStore.d.ts new file mode 100644 index 000000000..6dcde5f1a --- /dev/null +++ b/packages/discord-types/src/stores/ReadStateStore.d.ts @@ -0,0 +1,70 @@ +import { Channel, FluxStore } from ".."; +import { ReadStateType } from "../../enums"; + +export interface GuildChannelUnreadState { + mentionCount: number; + unread: boolean; + isMentionLowImportance: boolean; +} + +export interface ReadStateSnapshot { + unread: boolean; + mentionCount: number; + guildUnread: boolean | null; + guildMentionCount: number | null; + takenAt: number; +} + +export interface SerializedReadState { + channelId: string; + type: ReadStateType; + _guildId: string; + _persisted: boolean; + _lastMessageId: string; + _lastMessageTimestamp: number; + _ackMessageId: string; + _ackMessageTimestamp: number; + ackPinTimestamp: number; + lastPinTimestamp: number; + _mentionCount: number; + flags: number; + lastViewed: number; +} + +export class ReadStateStore extends FluxStore { + ackMessageId(channelId: string, type?: ReadStateType): string | null; + getAllReadStates(includePrivate?: boolean): SerializedReadState[]; + getChannelIdsForWindowId(windowId: string): string[]; + getForDebugging(channelId: string): object | undefined; + getGuildChannelUnreadState( + channel: Channel, + isOptInEnabled: boolean, + guildHasActiveThreads: boolean, + isChannelMuted: boolean, + isGuildHome: boolean + ): GuildChannelUnreadState; + getGuildUnreadsSentinel(guildId: string): number; + getIsMentionLowImportance(channelId: string, type?: ReadStateType): boolean; + getMentionChannelIds(): string[]; + getMentionCount(channelId: string, type?: ReadStateType): number; + getNonChannelAckId(type: ReadStateType): string | null; + getNotifCenterReadState(channelId: string): object | undefined; + getOldestUnreadMessageId(channelId: string, type?: ReadStateType): string | null; + getOldestUnreadTimestamp(channelId: string, type?: ReadStateType): number; + getReadStatesByChannel(): Record; + getSnapshot(channelId: string, maxAge: number): ReadStateSnapshot; + getTrackedAckMessageId(channelId: string, type?: ReadStateType): string | null; + getUnreadCount(channelId: string, type?: ReadStateType): number; + hasOpenedThread(channelId: string): boolean; + hasRecentlyVisitedAndRead(channelId: string): boolean; + hasTrackedUnread(channelId: string): boolean; + hasUnread(channelId: string, type?: ReadStateType): boolean; + hasUnreadOrMentions(channelId: string, type?: ReadStateType): boolean; + hasUnreadPins(channelId: string): boolean; + isEstimated(channelId: string, type?: ReadStateType): boolean; + isForumPostUnread(channelId: string): boolean; + isNewForumThread(threadId: string, parentChannelId: string, guildId: string): boolean; + lastMessageId(channelId: string, type?: ReadStateType): string | null; + lastMessageTimestamp(channelId: string, type?: ReadStateType): number; + lastPinTimestamp(channelId: string): number; +} diff --git a/packages/discord-types/src/stores/RelationshipStore.d.ts b/packages/discord-types/src/stores/RelationshipStore.d.ts index 5874a85fc..d63d094cc 100644 --- a/packages/discord-types/src/stores/RelationshipStore.d.ts +++ b/packages/discord-types/src/stores/RelationshipStore.d.ts @@ -1,38 +1,43 @@ import { FluxStore } from ".."; +import { Message, MessageJSON } from "../common/messages"; +import { RelationshipType } from "../../enums"; export class RelationshipStore extends FluxStore { - getFriendIDs(): string[]; - getIgnoredIDs(): string[]; getBlockedIDs(): string[]; getBlockedOrIgnoredIDs(): string[]; - - getPendingCount(): number; - getRelationshipCount(): number; - - /** Related to friend nicknames. */ - getNickname(userId: string): string; - - /** @returns Enum value from constants.RelationshipTypes */ - getRelationshipType(userId: string): number; - isBlockedOrIgnored(userId: string): boolean; - isBlockedOrIgnoredForMessage(userId: string): boolean; - isBlocked(userId: string): boolean; - isBlockedForMessage(userId: string): boolean; - isFriend(userId: string): boolean; - isIgnored(userId: string): boolean; - isIgnoredForMessage(userId: string): boolean; - isUnfilteredPendingIncoming(userId: string): boolean; - isSpam(userId): boolean; - getSince(userId: string): string; - getSinces(): Record; - getFriendCount(): number; - getRelationshipCount(): number; + getFriendIDs(): string[]; + getIgnoredIDs(): string[]; + + getMutableRelationships(): Map; + getNickname(userId: string): string; + getOriginApplicationId(userId: string): string | undefined; getOutgoingCount(): number; getPendingCount(): number; getPendingIgnoredCount(): number; - getSpamCount(): number; + getRelationshipCount(): number; - getMutableRelationships(): Map; + /** @returns Enum value from constants.RelationshipTypes */ + getRelationshipType(userId: string): RelationshipType; + getSince(userId: string): string; + getSinces(): Record; + getSpamCount(): number; getVersion(): number; + + isBlocked(userId: string): boolean; + isBlockedForMessage(message: Message | MessageJSON): boolean; + + /** + * @see {@link isBlocked} + * @see {@link isIgnored} + */ + isBlockedOrIgnored(userId: string): boolean; + isBlockedOrIgnoredForMessage(message: Message | MessageJSON): boolean; + + isFriend(userId: string): boolean; + isIgnored(userId: string): boolean; + isIgnoredForMessage(message: Message | MessageJSON): boolean; + isSpam(userId: string): boolean; + isStranger(userId: string): boolean; + isUnfilteredPendingIncoming(userId: string): boolean; } diff --git a/packages/discord-types/src/stores/RunningGameStore.d.ts b/packages/discord-types/src/stores/RunningGameStore.d.ts new file mode 100644 index 000000000..99b2987b0 --- /dev/null +++ b/packages/discord-types/src/stores/RunningGameStore.d.ts @@ -0,0 +1,62 @@ +import { FluxStore } from ".."; + +export interface RunningGame { + id?: string; + name: string; + exePath: string; + cmdLine: string; + distributor: string; + lastFocused: number; + lastLaunched: number; + nativeProcessObserverId: number; + pid?: number; + hidden?: boolean; + isLauncher?: boolean; + elevated?: boolean; + sandboxed?: boolean; +} + +export interface GameOverlayStatus { + enabledLegacy: boolean; + enabledOOP: boolean; +} + +export type SystemServiceState = "unknown" | "running" | "stopped" | "error"; + +export interface SystemServiceStatus { + state: SystemServiceState; +} + +export class RunningGameStore extends FluxStore { + canShowAdminWarning: boolean; + + addExecutableTrackedByAnalytics(exe: string): void; + getCandidateGames(): RunningGame[]; + getCurrentGameForAnalytics(): RunningGame | null; + getCurrentNonGameForAnalytics(): RunningGame | null; + getGameForName(name: string): RunningGame | null; + getGameForPID(pid: number): RunningGame | null; + getGameOrTransformedSubgameForPID(pid: number): RunningGame | null; + getGameOverlayStatus(game: RunningGame): GameOverlayStatus | null; + getGamesSeen(includeHidden?: boolean): RunningGame[]; + getLauncherForPID(pid: number): RunningGame | null; + getObservedAppNameForWindow(windowHandle: number): string | null; + getOverlayEnabledForGame(game: RunningGame): boolean; + getOverlayOptionsForPID(pid: number): object | null; + getOverrideForGame(game: RunningGame): object | null; + getOverrides(): object[]; + getRunningDiscordApplicationIds(): string[]; + getRunningGames(): RunningGame[]; + getRunningNonGames(): RunningGame[]; + getRunningVerifiedApplicationIds(): string[]; + getSeenGameByName(name: string): RunningGame | null; + getSystemServiceStatus(service: string): SystemServiceStatus; + getVisibleGame(): RunningGame | null; + getVisibleRunningGames(): RunningGame[]; + isDetectionEnabled(type?: string): boolean; + isGamesSeenLoaded(): boolean; + isObservedAppRunning(app: string): boolean; + isSystemServiceInitialized(service: string): boolean; + shouldContinueWithoutElevatedProcessForPID(pid: number): boolean; + shouldElevateProcessForPID(pid: number): boolean; +} diff --git a/packages/discord-types/src/stores/SelectedChannelStore.d.ts b/packages/discord-types/src/stores/SelectedChannelStore.d.ts index 13ac98ac3..3237a3eb0 100644 --- a/packages/discord-types/src/stores/SelectedChannelStore.d.ts +++ b/packages/discord-types/src/stores/SelectedChannelStore.d.ts @@ -1,14 +1,16 @@ import { FluxStore } from ".."; +export interface ChannelFollowingDestination { + guildId?: string; + channelId?: string; +} + export class SelectedChannelStore extends FluxStore { getChannelId(guildId?: string | null): string; getVoiceChannelId(): string | undefined; getCurrentlySelectedChannelId(guildId?: string): string | undefined; getMostRecentSelectedTextChannelId(guildId: string): string | undefined; getLastSelectedChannelId(guildId?: string): string; - // yes this returns a string getLastSelectedChannels(guildId?: string): string; - - /** If you follow an announcement channel, this will return whichever channel you chose as destination */ - getLastChannelFollowingDestination(): { guildId?: string; channelId?: string; } | undefined; + getLastChannelFollowingDestination(): ChannelFollowingDestination | undefined; } diff --git a/packages/discord-types/src/stores/SoundboardStore.d.ts b/packages/discord-types/src/stores/SoundboardStore.d.ts new file mode 100644 index 000000000..22065c725 --- /dev/null +++ b/packages/discord-types/src/stores/SoundboardStore.d.ts @@ -0,0 +1,47 @@ +import { FluxStore } from ".."; + +export interface SoundboardSound { + soundId: string; + name: string; + volume: number; + emojiId: string | null; + emojiName: string | null; + available: boolean; + guildId: string; + userId?: string; +} + +export interface TopSoundForGuild { + soundId: string; + rank: number; +} + +export interface SoundboardOverlayState { + soundboardSounds: Record; + favoritedSoundIds: string[]; + localSoundboardMutes: string[]; +} + +export class SoundboardStore extends FluxStore { + getOverlaySerializedState(): SoundboardOverlayState; + getSounds(): Map; + getSoundsForGuild(guildId: string): SoundboardSound[] | null; + getSound(guildId: string, soundId: string): SoundboardSound; + getSoundById(soundId: string): SoundboardSound; + isFetchingSounds(): boolean; + isFetchingDefaultSounds(): boolean; + isFetching(): boolean; + shouldFetchDefaultSounds(): boolean; + hasFetchedDefaultSounds(): boolean; + isUserPlayingSounds(userId: string): boolean; + isPlayingSound(soundId: string): boolean; + isFavoriteSound(soundId: string): boolean; + getFavorites(): Set; + getAllTopSoundsForGuilds(): Map; + isLocalSoundboardMuted(userId: string): boolean; + hasHadOtherUserPlaySoundInSession(): boolean; + shouldFetchTopSoundsForGuilds(): boolean; + hasFetchedTopSoundsForGuilds(): boolean; + hasFetchedAllSounds(): boolean; + isFetchingAnySounds(): boolean; +} diff --git a/packages/discord-types/src/stores/SpellCheckStore.d.ts b/packages/discord-types/src/stores/SpellCheckStore.d.ts index ff3148e24..ff8ecaddc 100644 --- a/packages/discord-types/src/stores/SpellCheckStore.d.ts +++ b/packages/discord-types/src/stores/SpellCheckStore.d.ts @@ -1,5 +1,6 @@ import { FluxStore } from ".."; export class SpellCheckStore extends FluxStore { + hasLearnedWord(word: string): boolean; isEnabled(): boolean; } diff --git a/packages/discord-types/src/stores/SpotifyStore.d.ts b/packages/discord-types/src/stores/SpotifyStore.d.ts new file mode 100644 index 000000000..b37da9ac3 --- /dev/null +++ b/packages/discord-types/src/stores/SpotifyStore.d.ts @@ -0,0 +1,111 @@ +import { FluxStore } from ".."; +import { ActivityFlags } from "../../enums"; + +export type SpotifyDeviceType = "computer" | "smartphone" | "speaker" | "tv" | "game_console" | "automobile" | "unknown"; +export type SpotifyMediaType = "track" | "episode"; +export type SpotifyAlbumType = "album" | "single" | "compilation"; + +export interface SpotifyDevice { + id: string; + is_active: boolean; + is_private_session: boolean; + is_restricted: boolean; + name: string; + supports_volume: boolean; + type: SpotifyDeviceType; + volume_percent: number; +} + +export interface SpotifySocket { + accessToken: string; + accountId: string; + connectionId: string; + isPremium: boolean; + socket: WebSocket; +} + +export interface SpotifySocketAndDevice { + socket: SpotifySocket; + device: SpotifyDevice; +} + +export interface SpotifyArtist { + id: string; + name: string; +} + +export interface SpotifyImage { + url: string; + height: number; + width: number; +} + +export interface SpotifyAlbum { + id: string; + name: string; + type: SpotifyAlbumType; + image: SpotifyImage | null; +} + +export interface SpotifyTrack { + id: string; + name: string; + duration: number; + isLocal: boolean; + type: SpotifyMediaType; + album: SpotifyAlbum; + artists: SpotifyArtist[]; +} + +export interface SpotifyPlayerState { + track: SpotifyTrack; + startTime: number; + context: { uri: string } | null; +} + +export interface SpotifyActivity { + name: string; + assets: { + large_image?: string; + large_text?: string; + }; + details: string; + state: string | undefined; + timestamps: { + start: number; + end: number; + }; + party: { + id: string; + }; + sync_id?: string; + flags?: ActivityFlags; + metadata?: { + context_uri: string | undefined; + album_id: string; + artist_ids: string[]; + type: SpotifyMediaType; + button_urls: string[]; + }; +} + +export interface SpotifySyncingWith { + oderId: string; + partyId: string; + sessionId: string; + userId: string; +} + +export class SpotifyStore extends FluxStore { + hasConnectedAccount(): boolean; + getActiveSocketAndDevice(): SpotifySocketAndDevice | null; + getPlayableComputerDevices(): SpotifySocketAndDevice[]; + canPlay(deviceId: string): boolean; + getSyncingWith(): SpotifySyncingWith | undefined; + wasAutoPaused(): boolean; + getLastPlayedTrackId(): string | undefined; + getTrack(): SpotifyTrack | null; + getPlayerState(accountId: string): SpotifyPlayerState | null; + shouldShowActivity(): boolean; + getActivity(): SpotifyActivity | null; +} diff --git a/packages/discord-types/src/stores/StickersStore.d.ts b/packages/discord-types/src/stores/StickersStore.d.ts index fd1d60fbd..639e62da0 100644 --- a/packages/discord-types/src/stores/StickersStore.d.ts +++ b/packages/discord-types/src/stores/StickersStore.d.ts @@ -1,6 +1,8 @@ import { FluxStore, GuildSticker, PremiumStickerPack, Sticker } from ".."; +import { LoadState } from "../../enums"; export type StickerGuildMap = Map; +export type StickerPackMap = Map; export interface StickerPackMeta { id: string; @@ -33,15 +35,24 @@ export interface StickerCategoryType { iconUrl?: string; } -export class StickersStore extends FluxStore { - getAllGuildStickers(): StickerGuildMap; - getRawStickersByGuild(): StickerGuildMap; - getPremiumPacks(): PremiumStickerPack[]; - getAllStickersIterator(): IterableIterator; +export interface StickerMetadataEntry { + type: number; + value: string; +} +export class StickersStore extends FluxStore { + hasLoadedStickerPacks: boolean; + isFetchingStickerPacks: boolean; + isLoaded: boolean; + loadState: LoadState; + + getAllGuildStickers(): StickerGuildMap; + getAllPackStickers(): StickerPackMap; + getPremiumPacks(): PremiumStickerPack[]; + getRawStickersByGuild(): StickerGuildMap; getStickerById(id: string): Sticker | undefined; + getStickerMetadataArrays(): [Record, Record]; getStickerPack(id: string): PremiumStickerPack | undefined; getStickersByGuildId(guildId: string): Sticker[] | undefined; - isPremiumPack(id: string): boolean; } diff --git a/packages/discord-types/src/stores/StreamerModeStore.d.ts b/packages/discord-types/src/stores/StreamerModeStore.d.ts index efa6cfc9e..73e2eaea6 100644 --- a/packages/discord-types/src/stores/StreamerModeStore.d.ts +++ b/packages/discord-types/src/stores/StreamerModeStore.d.ts @@ -1,4 +1,14 @@ -import { FluxStore } from "@vencord/discord-types"; +import { FluxStore } from ".."; + +export interface StreamerModeSettings { + enabled: boolean; + autoToggle: boolean; + hideInstantInvites: boolean; + hidePersonalInformation: boolean; + disableSounds: boolean; + disableNotifications: boolean; + enableContentProtection: boolean; +} export class StreamerModeStore extends FluxStore { get autoToggle(): boolean; @@ -8,4 +18,7 @@ export class StreamerModeStore extends FluxStore { get enabled(): boolean; get hideInstantInvites(): boolean; get hidePersonalInformation(): boolean; + + getSettings(): StreamerModeSettings; + getState(): Record; } diff --git a/packages/discord-types/src/stores/ThemeStore.d.ts b/packages/discord-types/src/stores/ThemeStore.d.ts index 2900f7f6d..c0dbcf592 100644 --- a/packages/discord-types/src/stores/ThemeStore.d.ts +++ b/packages/discord-types/src/stores/ThemeStore.d.ts @@ -6,13 +6,14 @@ export type Theme = "light" | "dark" | "darker" | "midnight"; export interface ThemeState { theme: Theme; + /** 0 = not loaded, 1 = loaded */ status: 0 | 1; preferences: Record; } export class ThemeStore extends FluxStore { - get theme(): Theme; - get darkSidebar(): boolean; get systemTheme(): SystemTheme; - themePreferenceForSystemTheme(preference: ThemePreference): Theme; + get theme(): Theme; + getState(): ThemeState; + themePreferenceForSystemTheme(preference: ThemePreference): Theme; } diff --git a/packages/discord-types/src/stores/UploadAttachmentStore.d.ts b/packages/discord-types/src/stores/UploadAttachmentStore.d.ts index 99490aff7..c29e7c075 100644 --- a/packages/discord-types/src/stores/UploadAttachmentStore.d.ts +++ b/packages/discord-types/src/stores/UploadAttachmentStore.d.ts @@ -1,11 +1,11 @@ -import { CloudUpload, DraftType, FluxStore } from ".."; +import { CloudUpload, FluxStore } from ".."; +import { DraftType } from "../../enums"; export class UploadAttachmentStore extends FluxStore { - getFirstUpload(channelId: string, draftType: string): CloudUpload; - hasAdditionalUploads(channelId: string, draftType: string): boolean; + getFirstUpload(channelId: string, draftType: DraftType): CloudUpload | null; + hasAdditionalUploads(channelId: string, draftType: DraftType): boolean; getUploads(channelId: string, draftType: DraftType): CloudUpload[]; getUploadCount(channelId: string, draftType: DraftType): number; - getUpload(channelId: string, commandName: string, draftType: DraftType): CloudUpload; - findUpload(channelId: string, draftType: string, predicate: (upload: CloudUpload) => boolean): CloudUpload | undefined; - + getUpload(channelId: string, uploadId: string, draftType: DraftType): CloudUpload; + findUpload(channelId: string, draftType: DraftType, predicate: (upload: CloudUpload) => boolean): CloudUpload | undefined; } diff --git a/packages/discord-types/src/stores/UserGuildSettingsStore.d.ts b/packages/discord-types/src/stores/UserGuildSettingsStore.d.ts new file mode 100644 index 000000000..9e5e03873 --- /dev/null +++ b/packages/discord-types/src/stores/UserGuildSettingsStore.d.ts @@ -0,0 +1,92 @@ +import { Channel, FluxStore, Guild } from ".."; +import { ChannelOverrideFlags, GuildSettingsFlags, NotifyHighlights, UserNotificationSetting } from "../../enums"; + +export interface MuteConfig { + selected_time_window: number; + end_time: string | null; +} + +export interface ChannelOverride { + muted: boolean; + mute_config: MuteConfig | null; + message_notifications: UserNotificationSetting; + flags: ChannelOverrideFlags; + collapsed: boolean; + channel_id: string; +} + +export interface GuildSettings { + suppress_everyone: boolean; + suppress_roles: boolean; + mute_scheduled_events: boolean; + mobile_push: boolean; + muted: boolean; + message_notifications: UserNotificationSetting; + flags: GuildSettingsFlags; + channel_overrides: Record; + notify_highlights: NotifyHighlights; + hide_muted_channels: boolean; + version: number; + mute_config: MuteConfig | null; + guild_id: string; +} + +export interface AccountNotificationSettings { + flags: number; +} + +export interface UserGuildSettingsState { + useNewNotifications: boolean; +} + +export class UserGuildSettingsStore extends FluxStore { + get accountNotificationSettings(): AccountNotificationSettings; + get mentionOnAllMessages(): boolean; + get useNewNotifications(): boolean; + + allowAllMessages(channel: Channel): boolean; + allowNoMessages(channel: Channel): boolean; + getAddedToMessages(): string[]; + // TODO: finish typing + getAllSettings(): { userGuildSettings: Record; }; + getChannelFlags(channel: Channel): ChannelOverrideFlags; + getChannelIdFlags(guildId: string, channelId: string): ChannelOverrideFlags; + getChannelMessageNotifications(guildId: string, channelId: string): UserNotificationSetting | null; + getChannelMuteConfig(guildId: string, channelId: string): MuteConfig | null; + getChannelOverrides(guildId: string): Record; + getChannelRecordUnreadSetting(channel: Channel): number; + getChannelUnreadSetting(guildId: string, channelId: string): number; + getGuildFavorites(guildId: string): string[]; + getGuildFlags(guildId: string): GuildSettingsFlags; + getGuildUnreadSetting(guildId: string): number; + getMessageNotifications(guildId: string): UserNotificationSetting; + getMuteConfig(guildId: string): MuteConfig | null; + getMutedChannels(guildId: string): string[]; + getNewForumThreadsCreated(channel: Channel): boolean; + getNotifyHighlights(guildId: string): NotifyHighlights; + getOptedInChannels(guildId: string): string[]; + // TODO: finish typing these + getOptedInChannelsWithPendingUpdates(guildId: string): Record; + getPendingChannelUpdates(guildId: string): Record; + getState(): UserGuildSettingsState; + isAddedToMessages(channelId: string): boolean; + isCategoryMuted(guildId: string, channelId: string): boolean; + isChannelMuted(guildId: string, channelId: string): boolean; + isChannelOptedIn(guildId: string, channelId: string, usePending?: boolean): boolean; + isChannelOrParentOptedIn(guildId: string, channelId: string, usePending?: boolean): boolean; + isChannelRecordOrParentOptedIn(channel: Channel, usePending?: boolean): boolean; + isFavorite(guildId: string, channelId: string): boolean; + isGuildCollapsed(guildId: string): boolean; + isGuildOrCategoryOrChannelMuted(guildId: string, channelId: string): boolean; + isMessagesFavorite(guildId: string): boolean; + isMobilePushEnabled(guildId: string): boolean; + isMuteScheduledEventsEnabled(guildId: string): boolean; + isMuted(guildId: string): boolean; + isOptInEnabled(guildId: string): boolean; + isSuppressEveryoneEnabled(guildId: string): boolean; + isSuppressRolesEnabled(guildId: string): boolean; + isTemporarilyMuted(guildId: string): boolean; + resolveGuildUnreadSetting(guild: Guild): number; + resolveUnreadSetting(channel: Channel): number; + resolvedMessageNotifications(channel: Channel): UserNotificationSetting; +} diff --git a/packages/discord-types/src/stores/UserProfileStore.d.ts b/packages/discord-types/src/stores/UserProfileStore.d.ts index d9efc47ba..f6c5e5ad9 100644 --- a/packages/discord-types/src/stores/UserProfileStore.d.ts +++ b/packages/discord-types/src/stores/UserProfileStore.d.ts @@ -1,5 +1,5 @@ import { FluxStore, Guild, User, Application, ApplicationInstallParams } from ".."; -import { ApplicationIntegrationType } from "../../enums"; +import { ApplicationFlags, ApplicationIntegrationType } from "../../enums"; export interface MutualFriend { /** @@ -54,7 +54,7 @@ export interface ProfileApplication { id: string; customInstallUrl: string | undefined; installParams: ApplicationInstallParams | undefined; - flags: number; + flags: ApplicationFlags; popularApplicationCommandIds?: string[]; integrationTypesConfig: Record premiumSince: Date | null; } -export class UserProfileStore extends FluxStore { - /** - * @param userId the user ID of the profile being fetched. - * @param guildId the guild ID to of the profile being fetched. - * defaults to the internal symbol `NO GUILD ID` if nullish - * - * @returns true if the profile is being fetched, false otherwise. - */ - isFetchingProfile(userId: string, guildId?: string): boolean; - /** - * Check if mutual friends for {@link userId} are currently being fetched. - * - * @param userId the user ID of the mutual friends being fetched. - * - * @returns true if mutual friends are being fetched, false otherwise. - */ - isFetchingFriends(userId: string): boolean; +export interface ApplicationWidgetConfig { + applicationId: string; + widgetType: number; +} +export interface WishlistSettings { + privacy: number; +} + +export class UserProfileStore extends FluxStore { + get applicationWidgetApplicationConfigs(): Record; get isSubmitting(): boolean; - getUserProfile(userId: string): UserProfile | undefined; - + getApplicationWidgetApplicationConfig(applicationId: string): ApplicationWidgetConfig | undefined; + getFirstWishlistId(userId: string): string | null; getGuildMemberProfile(userId: string, guildId: string | undefined): UserProfileBase | null; /** * Get the mutual friends of a user. @@ -148,4 +141,26 @@ export class UserProfileStore extends FluxStore { * @returns an array of mutual guilds, or undefined if the user has no mutual guilds */ getMutualGuilds(userId: string): MutualGuild[] | undefined; + getUserProfile(userId: string): UserProfile | undefined; + // TODO: finish typing + getWidgets(userId: string): any[] | undefined; + getWishlistIds(userId: string): string[]; + getWishlistSettings(userId: string): WishlistSettings | null; + /** + * Check if mutual friends for {@link userId} are currently being fetched. + * + * @param userId the user ID of the mutual friends being fetched. + * + * @returns true if mutual friends are being fetched, false otherwise. + */ + isFetchingFriends(userId: string): boolean; + /** + * @param userId the user ID of the profile being fetched. + * @param guildId the guild ID to of the profile being fetched. + * defaults to the internal symbol `NO GUILD ID` if nullish + * + * @returns true if the profile is being fetched, false otherwise. + */ + isFetchingProfile(userId: string, guildId?: string): boolean; + takeSnapshot(): Record; } diff --git a/packages/discord-types/src/stores/UserSettingsProtoStore.d.ts b/packages/discord-types/src/stores/UserSettingsProtoStore.d.ts new file mode 100644 index 000000000..66b02352d --- /dev/null +++ b/packages/discord-types/src/stores/UserSettingsProtoStore.d.ts @@ -0,0 +1,226 @@ +import { FluxStore } from ".."; +import { StickerAnimationSetting, Theme, UIDensity } from "../../enums"; + +export type UserStatus = "online" | "idle" | "dnd" | "invisible"; + +export interface GuildFolder { + guildIds: string[]; + folderId?: number; + folderName?: string; + folderColor?: number; +} + +export interface GuildProto { + // TODO: finish typing + channels: Record; + hubProgress: number; + guildOnboardingProgress: number; + dismissedGuildContent: Record; + disableRaidAlertPush: boolean; + disableRaidAlertNag: boolean; + leaderboardsDisabled: boolean; + // TODO: finish typing + guildDismissibleContentStates: Record; +} + +export interface UserSettingsVersions { + clientVersion: number; + serverVersion: number; + dataVersion: number; +} + +export interface InboxSettings { + currentTab: number; + viewedTutorial: boolean; +} + +export interface GuildsSettings { + guilds: Record; +} + +export interface UserContentSettings { + dismissedContents: string; + lastReceivedChangelogId: string; + // TODO: finish typing + recurringDismissibleContentStates: Record; + // TODO: type + lastDismissedOutboundPromotionStartDate: any; + premiumTier0ModalDismissedAt: any; +} + +export interface VoiceAndVideoSettings { + // TODO: type + videoBackgroundFilterDesktop: any; + alwaysPreviewVideo: boolean; + afkTimeout: number; + streamNotificationsEnabled: boolean; + nativePhoneIntegrationEnabled: boolean; + disableStreamPreviews: boolean; + soundmojiVolume: number; +} + +export interface TextAndImagesSettings { + emojiPickerCollapsedSections: string[]; + stickerPickerCollapsedSections: string[]; + soundboardPickerCollapsedSections: string[]; + dmSpamFilterV2: number; + viewImageDescriptions: boolean; + inlineAttachmentMedia: boolean; + inlineEmbedMedia: boolean; + gifAutoPlay: boolean; + renderEmbeds: boolean; + renderReactions: boolean; + animateEmoji: boolean; + animateStickers: StickerAnimationSetting; + enableTtsCommand: boolean; + messageDisplayCompact: boolean; + explicitContentFilter: number; + viewNsfwGuilds: boolean; + convertEmoticons: boolean; + viewNsfwCommands: boolean; + includeStickersInAutocomplete: boolean; + // TODO: type these + explicitContentSettings: any; + goreContentSettings: any; + showMentionSuggestions: boolean; +} + +export interface NotificationsSettings { + notificationCenterAckedBeforeId: string; + focusModeExpiresAtMs: string; + reactionNotifications: number; + gameActivityNotifications: boolean; + customStatusPushNotifications: boolean; + showInAppNotifications: boolean; + notifyFriendsOnGoLive: boolean; + enableVoiceActivityNotifications: boolean; + enableUserResurrectionNotifications: boolean; +} + +export interface PrivacySettings { + restrictedGuildIds: string[]; + defaultGuildsRestricted: boolean; + allowAccessibilityDetection: boolean; + activityRestrictedGuildIds: string[]; + defaultGuildsActivityRestricted: boolean; + activityJoiningRestrictedGuildIds: string[]; + messageRequestRestrictedGuildIds: string[]; + guildsLeaderboardOptOutDefault: boolean; + slayerSdkReceiveDmsInGame: boolean; + defaultGuildsActivityRestrictedV2: boolean; + detectPlatformAccounts: boolean; + passwordless: boolean; + contactSyncEnabled: boolean; + friendSourceFlags: number; + friendDiscoveryFlags: number; + dropsOptedOut: boolean; + hideLegacyUsername: boolean; + defaultGuildsRestrictedV2: boolean; + quests3PDataOptedOut: boolean; +} + +export interface GameLibrarySettings { + disableGamesTab: boolean; +} + +export interface StatusSettings { + statusExpiresAtMs: string; + status: { value: UserStatus; } | null; + showCurrentGame: boolean; + statusCreatedAtMs: string; +} + +export interface LocalizationSettings { + locale: { localeCode: string; } | null; + timezoneOffset: { offset: number; } | null; +} + +export interface AppearanceSettings { + theme: Theme; + developerMode: boolean; + mobileRedesignDisabled: boolean; + timestampHourCycle: number; + launchPadMode: number; + uiDensity: UIDensity; + swipeRightToLeftMode: number; + // TODO: type + clientThemeSettings: any; +} + +export interface GuildFoldersSettings { + folders: GuildFolder[]; + guildPositions: string[]; +} + +export interface AudioContextSettings { + // TODO: finish these + user: Record; + stream: Record; +} + +export interface ClipsSettings { + allowVoiceRecording: boolean; +} + +export interface InAppFeedbackSettings { + // TODO: finish typing + inAppFeedbackStates: Record; +} + +export interface UserSettings { + versions: UserSettingsVersions; + inbox: InboxSettings; + guilds: GuildsSettings; + userContent: UserContentSettings; + voiceAndVideo: VoiceAndVideoSettings; + textAndImages: TextAndImagesSettings; + notifications: NotificationsSettings; + privacy: PrivacySettings; + // TODO: finish typing + debug: Record; + gameLibrary: GameLibrarySettings; + status: StatusSettings; + localization: LocalizationSettings; + appearance: AppearanceSettings; + guildFolders: GuildFoldersSettings; + audioContextSettings: AudioContextSettings; + clips: ClipsSettings; + inAppFeedbackSettings: InAppFeedbackSettings; +} + +export interface FrecencySettings { + // TODO: type all of these + versions: any; + favoriteGifs: any; + favoriteStickers: any; + stickerFrecency: any; + favoriteEmojis: any; + emojiFrecency: any; + applicationCommandFrecency: any; + favoriteSoundboardSounds: any; + applicationFrecency: any; + playedSoundFrecency: any; + guildAndChannelFrecency: any; + emojiReactionFrecency: any; +} + +export interface ProtoState { + // TODO: type + proto: any; +} + +export class UserSettingsProtoStore extends FluxStore { + settings: UserSettings; + frecencyWithoutFetchingLatest: FrecencySettings; + wasMostRecentUpdateFromServer: boolean; + getState(): Record; + computeState(): Record; + getFullState(): Record; + hasLoaded(settingsType: number): boolean; + getGuildFolders(): GuildFolder[]; + getGuildRecentsDismissedAt(guildId: string): number; + getDismissedGuildContent(guildId: string): Record | null; + // TODO: finish typing + getGuildDismissedContentState(guildId: string): any; + getGuildsProto(): Record; +} diff --git a/packages/discord-types/src/stores/UserStore.d.ts b/packages/discord-types/src/stores/UserStore.d.ts index 323a1df0a..0bb12cac1 100644 --- a/packages/discord-types/src/stores/UserStore.d.ts +++ b/packages/discord-types/src/stores/UserStore.d.ts @@ -1,10 +1,34 @@ import { FluxStore, User } from ".."; +/** returned by takeSnapshot for persistence */ +export interface UserStoreSnapshot { + /** snapshot format version, currently 1 */ + version: number; + data: { + /** contains only the current user */ + users: User[]; + }; +} + export class UserStore extends FluxStore { - filter(filter: (user: User) => boolean, sort?: boolean): Record; - findByTag(username: string, discriminator: string): User; - forEach(action: (user: User) => void): void; + /** + * filters users and optionally sorts results. + * @param sort if true (default false), sorts alphabetically by username + */ + filter(filter: (user: User) => boolean, sort?: boolean): User[]; + /** + * finds user by username and discriminator. + * for new username system (unique usernames), pass null/undefined as discriminator. + */ + findByTag(username: string, discriminator?: string | null): User | undefined; + /** @param action return false to break iteration early */ + forEach(action: (user: User) => boolean | void): void; getCurrentUser(): User; getUser(userId: string): User; + /** keyed by user ID */ getUsers(): Record; + /** increments when users are added/updated/removed */ + getUserStoreVersion(): number; + /** only includes current user, used for persistence */ + takeSnapshot(): UserStoreSnapshot; } diff --git a/packages/discord-types/src/stores/VoiceStateStore.d.ts b/packages/discord-types/src/stores/VoiceStateStore.d.ts index 2feaf3925..95e917f94 100644 --- a/packages/discord-types/src/stores/VoiceStateStore.d.ts +++ b/packages/discord-types/src/stores/VoiceStateStore.d.ts @@ -28,19 +28,28 @@ export interface VoiceState extends DiscordRecord { export class VoiceStateStore extends FluxStore { getAllVoiceStates(): VoiceStates; + getVoiceStateVersion(): number; getVoiceStates(guildId?: string | null): UserVoiceStateRecords; getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords; getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords; getVoiceState(guildId: string | null, userId: string): VoiceState | undefined; - getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined; + getDiscoverableVoiceState(guildId: string | null, userId: string): VoiceState | null; getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined; getVoiceStateForUser(userId: string): VoiceState | undefined; + getDiscoverableVoiceStateForUser(userId: string): VoiceState | undefined; + getVoiceStateForSession(userId: string, sessionId?: string | null): VoiceState | null | undefined; + getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined; getCurrentClientVoiceChannelId(guildId: string | null): string | undefined; - isCurrentClientInVoiceChannel(): boolean; + getUsersWithVideo(channelId: string): Set; + getVoicePlatformForChannel(channelId: string, guildId: string): string | undefined; + + isCurrentClientInVoiceChannel(): boolean; isInChannel(channelId: string, userId?: string): boolean; hasVideo(channelId: string): boolean; + + get userHasBeenMovedVersion(): number; } diff --git a/packages/discord-types/src/stores/WindowStore.d.ts b/packages/discord-types/src/stores/WindowStore.d.ts index 53a40e967..619db2573 100644 --- a/packages/discord-types/src/stores/WindowStore.d.ts +++ b/packages/discord-types/src/stores/WindowStore.d.ts @@ -1,7 +1,22 @@ import { FluxStore } from ".."; -export class WindowStore extends FluxStore { - isElementFullScreen(): boolean; - isFocused(): boolean; - windowSize(): Record<"width" | "height", number>; +export interface WindowSize { + width: number; + height: number; +} + +export class WindowStore extends FluxStore { + /** returns focused window ID, or null if no window is focused */ + getFocusedWindowId(): string | null; + getLastFocusedWindowId(): string; + /** true if any window is focused (getFocusedWindowId() !== null) */ + isAppFocused(): boolean; + /** @param windowId defaults to current window */ + isElementFullScreen(windowId?: string): boolean; + /** @param windowId defaults to current window */ + isFocused(windowId?: string): boolean; + /** @param windowId defaults to current window */ + isVisible(windowId?: string): boolean; + /** @param windowId defaults to current window, returns {width: 0, height: 0} for invalid ID */ + windowSize(windowId?: string): WindowSize; } diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index ecbb256ef..f0b475dee 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -1,25 +1,47 @@ // please keep in alphabetical order +export * from "./AccessibilityStore"; +export * from "./ActiveJoinedThreadsStore"; +export * from "./ApplicationStore"; export * from "./AuthenticationStore"; +export * from "./CallStore"; +export * from "./ChannelRTCStore"; export * from "./ChannelStore"; export * from "./DraftStore"; export * from "./EmojiStore"; export * from "./FluxStore"; +export * from "./FriendsStore"; +export * from "./GuildChannelStore"; +export * from "./GuildMemberCountStore"; export * from "./GuildMemberStore"; export * from "./GuildRoleStore"; +export * from "./GuildScheduledEventStore"; export * from "./GuildStore"; -export * from "./MessageStore"; -export * from "./PresenceStore"; -export * from "./RelationshipStore"; +export * from "./InstantInviteStore"; +export * from "./InviteStore"; +export * from "./LocaleStore"; export * from "./MediaEngineStore"; -export * from "./StreamerModeStore"; -export * from "./SpellCheckStore"; +export * from "./MessageStore"; +export * from "./NotificationSettingsStore"; +export * from "./OverridePremiumTypeStore"; +export * from "./PermissionStore"; +export * from "./PresenceStore"; +export * from "./ReadStateStore"; +export * from "./RelationshipStore"; +export * from "./RTCConnectionStore"; +export * from "./RunningGameStore"; export * from "./SelectedChannelStore"; export * from "./SelectedGuildStore"; +export * from "./SoundboardStore"; +export * from "./SpellCheckStore"; +export * from "./SpotifyStore"; export * from "./StickersStore"; export * from "./StreamerModeStore"; export * from "./ThemeStore"; export * from "./TypingStore"; +export * from "./UploadAttachmentStore"; +export * from "./UserGuildSettingsStore"; export * from "./UserProfileStore"; +export * from "./UserSettingsProtoStore"; export * from "./UserStore"; export * from "./VoiceStateStore"; export * from "./WindowStore"; diff --git a/packages/discord-types/src/utils.d.ts b/packages/discord-types/src/utils.d.ts index 77ef1212a..bd3c9376c 100644 --- a/packages/discord-types/src/utils.d.ts +++ b/packages/discord-types/src/utils.d.ts @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { LiteralUnion } from "type-fest"; import type { FluxEvents } from "./fluxEvents"; +import { ApplicationCommandOptionType, ChannelType, PremiumType } from "../enums"; export { FluxEvents }; @@ -86,6 +87,8 @@ export type Permissions = "CREATE_INSTANT_INVITE" | "VIEW_GUILD_ANALYTICS" | "VIEW_CREATOR_MONETIZATION_ANALYTICS" | "MODERATE_MEMBERS" + | "USE_EMBEDDED_ACTIVITIES" + | "USE_EXTERNAL_APPS" | "SEND_MESSAGES" | "SEND_TTS_MESSAGES" | "MANAGE_MESSAGES" @@ -102,6 +105,9 @@ export type Permissions = "CREATE_INSTANT_INVITE" | "USE_EXTERNAL_STICKERS" | "SEND_MESSAGES_IN_THREADS" | "SEND_VOICE_MESSAGES" + | "SEND_POLLS" + | "PIN_MESSAGES" + | "BYPASS_SLOWMODE" | "CONNECT" | "SPEAK" | "MUTE_MEMBERS" @@ -110,9 +116,9 @@ export type Permissions = "CREATE_INSTANT_INVITE" | "USE_VAD" | "PRIORITY_SPEAKER" | "STREAM" - | "USE_EMBEDDED_ACTIVITIES" | "USE_SOUNDBOARD" | "USE_EXTERNAL_SOUNDS" + | "SET_VOICE_CHANNEL_STATUS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "CREATE_EVENTS"; @@ -276,6 +282,7 @@ export interface UsernameUtils { humanizeStatus: any; } +// TODO: fix type export class DisplayProfile { userId: string; banner?: string; @@ -291,7 +298,7 @@ export class DisplayProfile { canEditThemes: boolean; premiumGuildSince: Date | null; premiumSince: Date | null; - premiumType?: number; + premiumType?: PremiumType; primaryColor?: number; getBadges(): Array<{ @@ -323,7 +330,7 @@ export interface DateUtils { } export interface CommandOptions { - type: number; + type: ApplicationCommandOptionType; name: string; description: string; required?: boolean; @@ -332,7 +339,7 @@ export interface CommandOptions { values: string | number; }[]; options?: CommandOptions[]; - channel_types?: number[]; + channel_types?: ChannelType[]; min_value?: number; max_value?: number; autocomplete?: boolean; diff --git a/packages/vencord-types/package.json b/packages/vencord-types/package.json index ea9fcd6a2..8cfad6c3e 100644 --- a/packages/vencord-types/package.json +++ b/packages/vencord-types/package.json @@ -1,7 +1,7 @@ { "name": "@vencord/types", "private": false, - "version": "1.13.8", + "version": "1.13.9", "description": "", "types": "index.d.ts", "scripts": { @@ -9,7 +9,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], - "author": "Vencord", + "author": "Equicord & Vencord Contributors", "license": "GPL-3.0", "devDependencies": { "@types/fs-extra": "^11.0.4", @@ -29,4 +29,4 @@ "@types/react": "18.3.1", "@types/react-dom": "18.3.1" } -} +} \ No newline at end of file diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index fc2d6a863..454f0b74b 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -185,14 +185,7 @@ async function buildExtension(target, files) { } const appendCssRuntime = readFile("dist/Equicord.user.css", "utf-8").then(content => { - const cssRuntime = ` -;document.addEventListener("DOMContentLoaded", () => document.body.insertAdjacentElement("afterend", - Object.assign(document.createElement("style"), { - textContent: \`${content.replaceAll("`", "\\`")}\`, - id: "vencord-css-core" - }) -), { once: true }); -`; + const cssRuntime = `unsafeWindow._vcUserScriptRendererCss=\`${content.replaceAll("`", "\\`")}\``; return appendFile("dist/Equicord.user.js", cssRuntime); }); diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index da858e2ec..a792858f9 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -31,7 +31,6 @@ import { fileURLToPath } from "url"; import { promisify } from "util"; import { getPluginTarget } from "../utils.mjs"; -import { builtinModules } from "module"; const PackageJSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf-8")); @@ -261,7 +260,7 @@ export const fileUrlPlugin = { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => { const { searchParams } = new URL(uri); const base64 = searchParams.has("base64"); - const minify = IS_STANDALONE === true && searchParams.has("minify"); + const minify = searchParams.has("minify"); const noTrim = searchParams.get("trim") === "false"; const encoding = base64 ? "base64" : "utf-8"; @@ -294,7 +293,7 @@ export const fileUrlPlugin = { throw new Error(`Don't know how to minify file type: ${path}`); } - if (base64) + if (base64 && !content.startsWith("data:")) content = Buffer.from(content).toString("base64"); } diff --git a/src/Vencord.ts b/src/Vencord.ts index 8fb08db06..1fc28e225 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -28,7 +28,7 @@ export * as Webpack from "./webpack"; export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; -import { addVencordUiStyles } from "@components/css"; +import { coreStyleRootNode, initStyles } from "@api/Styles"; import { openSettingsTabModal, UpdaterTab } from "@components/settings"; import { debounce } from "@shared/debounce"; import { IS_WINDOWS } from "@utils/constants"; @@ -39,7 +39,7 @@ import { get as dsGet } from "./api/DataStore"; import { NotificationData, showNotification } from "./api/Notifications"; import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager"; import { PlainSettings, Settings, SettingsStore } from "./api/Settings"; -import { getCloudSettings, putCloudSettings } from "./api/SettingsSync/cloudSync"; +import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync"; import { localStorage } from "./utils/localStorage"; import { relaunch } from "./utils/native"; import { checkForUpdates, update, UpdateLogger } from "./utils/updater"; @@ -53,6 +53,10 @@ if (IS_REPORTER) { async function syncSettings() { // Check if cloud auth exists for current user before attempting sync + if (localStorage.Vencord_cloudSyncDirection === undefined) { + // by default, sync bi-directionally + localStorage.Vencord_cloudSyncDirection = "both"; + } const hasCloudAuth = await dsGet("Vencord_cloudSecret"); if (!hasCloudAuth) { if (Settings.cloud.authenticated) { @@ -87,13 +91,12 @@ async function syncSettings() { if ( Settings.cloud.settingsSync && // if it's enabled - Settings.cloud.authenticated // if cloud integrations are enabled + Settings.cloud.authenticated && // if cloud integrations are enabled + localStorage.Vencord_cloudSyncDirection !== "manual" // if we're not in manual mode ) { - if (localStorage.Vencord_settingsDirty) { + if (localStorage.Vencord_settingsDirty && shouldCloudSync("push")) { await putCloudSettings(); - delete localStorage.Vencord_settingsDirty; - } else if (await getCloudSettings(false)) { - // if we synchronized something (false means no sync) + } else if (shouldCloudSync("pull") && await getCloudSettings(false)) { // if we synchronized something (false means no sync) // we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of // potential notifications that might occur. getCloudSettings() will always send a notification regardless if // there was an error to notify the user, but besides that we only want to show one notification instead of all @@ -108,9 +111,8 @@ async function syncSettings() { } const saveSettingsOnFrequentAction = debounce(async () => { - if (Settings.cloud.settingsSync && Settings.cloud.authenticated) { + if (Settings.cloud.settingsSync && Settings.cloud.authenticated && shouldCloudSync("push")) { await putCloudSettings(); - delete localStorage.Vencord_settingsDirty; } }, 60_000); @@ -192,16 +194,15 @@ async function init() { } initPluginManager(); +initStyles(); startAllPlugins(StartAt.Init); init(); document.addEventListener("DOMContentLoaded", () => { - addVencordUiStyles(); - startAllPlugins(StartAt.DOMContentLoaded); // FIXME if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) { - createAndAppendStyle("vencord-native-titlebar-style").textContent = "[class*=titleBar]{display: none!important}"; + createAndAppendStyle("vencord-native-titlebar-style", coreStyleRootNode).textContent = "[class*=titleBar]{display: none!important}"; } }, { once: true }); diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 380c42954..5ad69a9be 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -5,13 +5,13 @@ */ import type { Settings } from "@api/Settings"; -import { CspRequestResult } from "@main/csp/manager"; -import { PluginIpcMappings } from "@main/ipcPlugins"; +import type { CspRequestResult } from "@main/csp/manager"; +import type { PluginIpcMappings } from "@main/ipcPlugins"; import { IpcEvents } from "@shared/IpcEvents"; -import { IpcRes } from "@utils/types"; -import { ipcRenderer } from "electron"; +import type { IpcRes } from "@utils/types"; +import { ipcRenderer } from "electron/renderer"; -function invoke(event: IpcEvents, ...args: any[]) { +export function invoke(event: IpcEvents, ...args: any[]) { return ipcRenderer.invoke(event, ...args) as Promise; } @@ -31,8 +31,12 @@ for (const [plugin, methods] of Object.entries(pluginIpcMap)) { export default { themes: { - uploadTheme: (fileName: string, fileData: string) => invoke(IpcEvents.UPLOAD_THEME, fileName, fileData), - deleteTheme: (fileName: string) => invoke(IpcEvents.DELETE_THEME, fileName), + uploadTheme: async (fileName: string, fileData: string): Promise => { + throw new Error("uploadTheme is WEB only"); + }, + deleteTheme: async (fileName: string): Promise => { + throw new Error("deleteTheme is WEB only"); + }, getThemesDir: () => invoke(IpcEvents.GET_THEMES_DIR), getThemesList: () => invoke>(IpcEvents.GET_THEMES_LIST), getThemeData: (fileName: string) => invoke(IpcEvents.GET_THEME_DATA, fileName), @@ -70,11 +74,18 @@ export default { openFile: () => invoke(IpcEvents.OPEN_QUICKCSS), openEditor: () => invoke(IpcEvents.OPEN_MONACO_EDITOR), + getEditorTheme: () => sendSync(IpcEvents.GET_MONACO_THEME), }, native: { getVersions: () => process.versions as Partial, - openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url) + openExternal: (url: string) => invoke(IpcEvents.OPEN_EXTERNAL, url), + getRendererCss: () => invoke(IpcEvents.GET_RENDERER_CSS), + onRendererCssUpdate: (cb: (newCss: string) => void) => { + if (!IS_DEV) return; + + ipcRenderer.on(IpcEvents.RENDERER_CSS_UPDATE, (_e, newCss: string) => cb(newCss)); + } }, csp: { diff --git a/src/api/Badges.ts b/src/api/Badges.ts index 9ab0f5cb2..a1d7a0c62 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -50,7 +50,8 @@ export interface ProfileBadge { key?: string; /** - * Allows dynamically returning multiple badges + * Allows dynamically returning multiple badges. + * May call hooks but then you must not use shouldShow */ getBadges?(userInfo: BadgeUserArgs): ProfileBadge[]; } diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts index b52df9424..b609fe5bf 100644 --- a/src/api/Commands/index.ts +++ b/src/api/Commands/index.ts @@ -88,7 +88,6 @@ export const _handleCommand = function (cmd: VencordCommand, args: CommandArgume } } as never; - /** * Prepare a Command Option for Discord by filling missing fields * @param opt diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index bf4efd2d5..dc5d3b8df 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -106,7 +106,6 @@ export function removeMessagePreEditListener(listener: MessageEditListener) { return editListeners.delete(listener); } - // Message clicks export type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void; diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index 40958a724..0fcea8a78 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -18,11 +18,11 @@ import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Flex } from "@components/Flex"; import { Paragraph } from "@components/Paragraph"; import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings"; +import { classNameFactory } from "@utils/css"; import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useAwaiter } from "@utils/react"; import { Alerts, Button, ListScrollerThin, React, Timestamp, useEffect, useReducer, useState } from "@webpack/common"; diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 4be6da317..208c37698 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -251,21 +251,6 @@ export function useSettings(paths?: UseSettings[]) { return SettingsStore.store; } -export function migratePluginToSetting(newName: string, oldName: string, settingName: string) { - const { plugins } = SettingsStore.plain; - const newPlugin = plugins[newName]; - const oldPlugin = plugins[oldName]; - - if (!newPlugin || !oldPlugin) return; - - if (oldPlugin?.enabled) { - newPlugin[settingName] = true; - oldPlugin.enabled = false; - if (!newPlugin?.enabled) newPlugin.enabled = true; - SettingsStore.markAsChanged(); - } -} - export function migratePluginSettings(name: string, ...oldNames: string[]) { const { plugins } = SettingsStore.plain; if (name in plugins) return; @@ -287,21 +272,46 @@ export function migratePluginSetting(pluginName: string, newSetting: string, old if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return; + logger.info(`Migrating plugin setting from ${oldSetting} to ${newSetting} on ${pluginName}`); settings[newSetting] = settings[oldSetting]; delete settings[oldSetting]; SettingsStore.markAsChanged(); } -export function migrateSettingFromPlugin(newPlugin: string, newSetting: string, oldPlugin: string, oldSetting: string) { - const newSettings = SettingsStore.plain.plugins[newPlugin]; - const oldSettings = SettingsStore.plain.plugins[oldPlugin]; - if (!oldSettings || !Object.hasOwn(oldSettings, oldSetting)) return; - if (!newSettings || (Object.hasOwn(newSettings, newSetting))) return; +export function migratePluginToSettings(deleteOldSettings: boolean, newName: string, oldName: string, ...settingNames: string[]) { + const { plugins } = SettingsStore.plain; + const newPlugin = plugins[newName]; + const oldPlugin = plugins[oldName]; - if (Object.hasOwn(newSettings, newSetting)) return; + if (newPlugin && oldPlugin?.enabled) { + for (const settingName of settingNames) { + logger.info(`Migrating plugin to setting from old name ${oldName} to ${newName} as ${settingName}`); + newPlugin[settingName] = true; + } + + newPlugin.enabled = true; + if (deleteOldSettings) delete plugins[oldName]; + SettingsStore.markAsChanged(); + } +} + +export function migrateSettingsFromPlugin(newPlugin: string, oldPlugin: string, ...settings: string[]) { + const { plugins } = SettingsStore.plain; + + const oldSettings = plugins[oldPlugin]; + const newSettings = plugins[newPlugin]; + if (!oldSettings || !newSettings) return; + + for (const setting of settings) { + if (!Object.hasOwn(oldSettings, setting)) continue; + if (Object.hasOwn(newSettings, setting)) continue; + + logger.info(`Migrating plugin setting "${setting}" from ${oldPlugin} to ${newPlugin}`); + + newSettings[setting] = oldSettings[setting]; + delete oldSettings[setting]; + } - newSettings[newSetting] = oldSettings[oldSetting]; - delete oldSettings[oldSetting]; SettingsStore.markAsChanged(); } diff --git a/src/api/SettingsSync/cloudSync.ts b/src/api/SettingsSync/cloudSync.ts index 6efe46792..e1e5edf97 100644 --- a/src/api/SettingsSync/cloudSync.ts +++ b/src/api/SettingsSync/cloudSync.ts @@ -6,6 +6,7 @@ import { showNotification } from "@api/Notifications"; import { PlainSettings, Settings } from "@api/Settings"; +import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; import { relaunch } from "@utils/native"; import { openUserSettingsPanel } from "@webpack/common"; @@ -16,6 +17,12 @@ import { exportSettings, importSettings } from "./offline"; const logger = new Logger("SettingsSync:Cloud", "#39b7e0"); +export function shouldCloudSync(direction: "push" | "pull") { + const localDirection = localStorage.Vencord_cloudSyncDirection; + + return localDirection === direction || localDirection === "both"; +} + export async function putCloudSettings(manual?: boolean) { const settings = await exportSettings({ syncDataStore: false, minify: true }); @@ -52,6 +59,8 @@ export async function putCloudSettings(manual?: boolean) { noPersist: true, }); } + + delete localStorage.Vencord_settingsDirty; } catch (e: any) { logger.error("Failed to sync up", e); showNotification({ @@ -151,6 +160,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) { noPersist: true }); + delete localStorage.Vencord_settingsDirty; + return true; } catch (e: any) { logger.error("Failed to sync down", e); diff --git a/src/api/SettingsSync/offline.ts b/src/api/SettingsSync/offline.ts index 2e0cbea9d..3b114407c 100644 --- a/src/api/SettingsSync/offline.ts +++ b/src/api/SettingsSync/offline.ts @@ -56,8 +56,8 @@ export async function importSettings(data: string, type: BackupType = "all", clo switch (type) { case "all": { - if (!cloud && (!("settings" in parsed) || !("quickCss" in parsed) || !("dataStore" in parsed))) - throw new Error("Invalid Settings. Missing one or more required keys."); + if (!cloud && (!("settings" in parsed))) + throw new Error("Invalid Settings. Plugin settings is required for this import try a different one."); if (parsed.settings) { Object.assign(PlainSettings, parsed.settings); @@ -68,10 +68,10 @@ export async function importSettings(data: string, type: BackupType = "all", clo break; } case "plugins": { - if (!parsed.settings?.settings) throw new Error("Plugin settings missing"); + if (!parsed.settings) throw new Error("Plugin settings missing"); - Object.assign(PlainSettings, parsed.settings.settings); - await VencordNative.settings.set(parsed.settings.settings); + Object.assign(PlainSettings, parsed.settings); + await VencordNative.settings.set(parsed.settings); break; } case "css": { @@ -99,7 +99,7 @@ export async function exportSettings({ syncDataStore = true, type = "all", minif return JSON.stringify({ settings, quickCss, ...(syncDataStore && { dataStore }) }, null, minify ? undefined : 4); } case "plugins": { - return JSON.stringify({ settings: { settings } }, null, minify ? undefined : 4); + return JSON.stringify({ settings }, null, minify ? undefined : 4); } case "css": { return JSON.stringify({ quickCss }, null, minify ? undefined : 4); @@ -110,21 +110,6 @@ export async function exportSettings({ syncDataStore = true, type = "all", minif } } -export async function exportPlugins({ minify }: { minify?: boolean; } = {}) { - const { plugins } = VencordNative.settings.get(); - return JSON.stringify({ settings: { plugins } }, null, minify ? undefined : 4); -} - -export async function exportCSS({ minify }: { minify?: boolean; } = {}) { - const quickCss = await VencordNative.quickCss.get(); - return JSON.stringify({ quickCss }, null, minify ? undefined : 4); -} - -export async function exportDataStores({ minify }: { minify?: boolean; } = {}) { - const dataStore = await DataStore.entries(); - return JSON.stringify({ dataStore }, null, minify ? undefined : 4); -} - export async function downloadSettingsBackup(type: BackupType = "all", { minify }: { minify?: boolean; } = {}) { const backup = await exportSettings({ minify, type }); const filename = `equicord-${type}-backup-${moment().format("YYYY-MM-DD")}.json`; @@ -141,7 +126,7 @@ export async function uploadSettingsBackup(type: BackupType = "all", showToast = if (IS_DISCORD_DESKTOP) { const [file] = await DiscordNative.fileManager.openFiles({ filters: [ - { name: "Vencord Settings Backup", extensions: ["json"] }, + { name: "Equicord Settings Backup", extensions: ["json"] }, { name: "all", extensions: ["*"] } ] }); diff --git a/src/api/Styles.ts b/src/api/Styles.ts index fb40f72d8..5b91c1958 100644 --- a/src/api/Styles.ts +++ b/src/api/Styles.ts @@ -16,6 +16,14 @@ * along with this program. If not, see . */ +import { generateTextCss } from "@components/BaseText"; +import { generateMarginCss } from "@components/margins"; +import { classNameFactory as _classNameFactory, classNameToSelector, createAndAppendStyle } from "@utils/css"; + +// Backwards compat for Vesktop +/** @deprecated Import this from `@utils/css` instead */ +export const classNameFactory = _classNameFactory; + export interface Style { name: string; source: string; @@ -25,6 +33,57 @@ export interface Style { export const styleMap = window.VencordStyles ??= new Map(); +export const vencordRootNode = document.createElement("vencord-root"); +/** + * Houses all Vencord core styles. This includes all imported css files + */ +export const coreStyleRootNode = document.createElement("vencord-styles"); +/** + * Houses all plugin specific managed styles + */ +export const managedStyleRootNode = document.createElement("vencord-managed-styles"); +/** + * Houses the user's themes and quick css + */ +export const userStyleRootNode = document.createElement("vencord-user-styles"); + +vencordRootNode.style.display = "none"; +vencordRootNode.append(coreStyleRootNode, managedStyleRootNode, userStyleRootNode); + +export function initStyles() { + const osValuesNode = createAndAppendStyle("vencord-os-theme-values", coreStyleRootNode); + createAndAppendStyle("vencord-text", coreStyleRootNode).textContent = generateTextCss(); + const rendererCssNode = createAndAppendStyle("vencord-css-core", coreStyleRootNode); + const vesktopCssNode = IS_VESKTOP ? createAndAppendStyle("vesktop-css-core", coreStyleRootNode) : null; + createAndAppendStyle("vencord-margins", coreStyleRootNode).textContent = generateMarginCss(); + + VencordNative.native.getRendererCss().then(css => rendererCssNode.textContent = css); + if (IS_DEV) { + VencordNative.native.onRendererCssUpdate(newCss => { + rendererCssNode.textContent = newCss; + }); + } + + if (IS_VESKTOP && VesktopNative.app.getRendererCss || IS_EQUIBOP && VesktopNative.app.getRendererCss) { + VesktopNative.app.getRendererCss().then(css => vesktopCssNode!.textContent = css); + VesktopNative.app.onRendererCssUpdate(newCss => { + vesktopCssNode!.textContent = newCss; + }); + } + + VencordNative.themes.getSystemValues().then(values => { + const variables = Object.entries(values) + .filter(([, v]) => !!v) + .map(([k, v]) => `--${k}: ${v};`) + .join(""); + osValuesNode.textContent = `:root{${variables}}`; + }); +} + +document.addEventListener("DOMContentLoaded", () => { + document.documentElement.append(vencordRootNode); +}, { once: true }); + export function requireStyle(name: string) { const style = styleMap.get(name); if (!style) throw new Error(`Style "${name}" does not exist`); @@ -53,7 +112,7 @@ export function enableStyle(name: string) { } compileStyle(style); - document.head.appendChild(style.dom); + managedStyleRootNode.appendChild(style.dom); return true; } @@ -134,32 +193,3 @@ export const compileStyle = (style: Style) => { return className ? classNameToSelector(className) : match; }); }; - -/** - * @param name The classname - * @param prefix A prefix to add each class, defaults to `""` - * @return A css selector for the classname - * @example - * classNameToSelector("foo bar") // => ".foo.bar" - */ -export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join(""); - -type ClassNameFactoryArg = string | string[] | Record | false | null | undefined | 0 | ""; -/** - * @param prefix The prefix to add to each class, defaults to `""` - * @returns A classname generator function - * @example - * const cl = classNameFactory("plugin-"); - * - * cl("base", ["item", "editable"], { selected: null, disabled: true }) - * // => "plugin-base plugin-item plugin-editable plugin-disabled" - */ -export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => { - const classNames = new Set(); - for (const arg of args) { - if (arg && typeof arg === "string") classNames.add(arg); - else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); - else if (arg && typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name)); - } - return Array.from(classNames, name => prefix + name).join(" "); -}; diff --git a/src/api/Themes.ts b/src/api/Themes.ts index 525018531..81a55cfb1 100644 --- a/src/api/Themes.ts +++ b/src/api/Themes.ts @@ -20,23 +20,15 @@ import { Settings, SettingsStore } from "@api/Settings"; import { createAndAppendStyle } from "@utils/css"; import { ThemeStore } from "@vencord/discord-types"; +import { userStyleRootNode } from "./Styles"; + let style: HTMLStyleElement; let themesStyle: HTMLStyleElement; -async function initSystemValues() { - const values = await VencordNative.themes.getSystemValues(); - const variables = Object.entries(values) - .filter(([, v]) => v !== "#") - .map(([k, v]) => `--${k}: ${v};`) - .join(""); - - createAndAppendStyle("vencord-os-theme-values").textContent = `:root{${variables}}`; -} - async function toggle(isEnabled: boolean) { if (!style) { if (isEnabled) { - style = createAndAppendStyle("vencord-custom-css"); + style = createAndAppendStyle("vencord-custom-css", userStyleRootNode); VencordNative.quickCss.addChangeListener(css => { style.textContent = css; // At the time of writing this, changing textContent resets the disabled state @@ -49,7 +41,7 @@ async function toggle(isEnabled: boolean) { } async function initThemes() { - themesStyle ??= createAndAppendStyle("vencord-themes"); + themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode); const { enabledThemeLinks, enabledThemes } = Settings; @@ -89,7 +81,6 @@ async function initThemes() { document.addEventListener("DOMContentLoaded", () => { if (IS_USERSCRIPT) return; - initSystemValues(); initThemes(); toggle(Settings.useQuickCss); diff --git a/src/api/UserArea.tsx b/src/api/UserArea.tsx index 4701d8de8..7f8a3cbb1 100644 --- a/src/api/UserArea.tsx +++ b/src/api/UserArea.tsx @@ -9,7 +9,7 @@ import { Logger } from "@utils/Logger"; import { findComponentByCodeLazy } from "@webpack"; import type { ComponentType, MouseEventHandler, ReactNode } from "react"; -const PanelButton = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON") as ComponentType; +const PanelButton = findComponentByCodeLazy("tooltipPositionKey", ".greenTooltip") as ComponentType; export interface UserAreaButtonProps { icon: ReactNode; diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx deleted file mode 100644 index 8e83f5738..000000000 --- a/src/components/Alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { filters, mapMangledModuleLazy } from "@webpack"; -import type { ComponentType, CSSProperties, ReactNode } from "react"; - -const { HelpMessage, HelpMessageTypes } = mapMangledModuleLazy('POSITIVE="positive', { - HelpMessageTypes: filters.byProps("POSITIVE", "WARNING", "INFO", "ERROR"), - HelpMessage: filters.byCode(".iconDiv") -}); - -export interface AlertProps { - children: ReactNode; - className?: string; - style?: CSSProperties; - icon?: ComponentType<{ className?: string; color?: string; }>; -} - -function Warning({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Info({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Error({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -function Positive({ children, className, style, icon }: AlertProps) { - return ( - - {children} - - ); -} - -export const Alert = { - Warning, - Info, - Error, - Positive -}; diff --git a/src/components/BaseText.tsx b/src/components/BaseText.tsx index dfd1565af..78b961ea9 100644 --- a/src/components/BaseText.tsx +++ b/src/components/BaseText.tsx @@ -6,7 +6,7 @@ import "./BaseText.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import type { Text as DiscordText } from "@vencord/discord-types"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; @@ -98,7 +98,6 @@ export function BaseText(props: BaseTextProps): Re ); } - // #region Old compatibility export const TextCompat: DiscordText = function TextCompat({ color, variant, ...restProps }) { diff --git a/src/components/Button.css b/src/components/Button.css index ab57403d4..3c515ef69 100644 --- a/src/components/Button.css +++ b/src/components/Button.css @@ -124,11 +124,11 @@ } .vc-btn-positive { - background-color: var(--redesign-button-positive-background); + background-color: var(--redesign-button-positive-background, var(--green-430)); color: var(--white); &:hover { - background-color: var(--redesign-button-positive-pressed-background); + background-color: var(--redesign-button-positive-pressed-background, var(--green-460)); } } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index b4390c232..2cfbd83d2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,7 +6,7 @@ import "./Button.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import type { Button as DiscordButton } from "@vencord/discord-types"; import type { ComponentPropsWithRef } from "react"; diff --git a/src/components/Card.tsx b/src/components/Card.tsx index be32d978d..dfa6caaa0 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -6,7 +6,7 @@ import "./Card.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { ComponentPropsWithRef } from "react"; diff --git a/src/components/Heart.tsx b/src/components/Heart.tsx index 8f9f9a31b..600a4c723 100644 --- a/src/components/Heart.tsx +++ b/src/components/Heart.tsx @@ -37,4 +37,3 @@ export function Heart(props: SVGProps) { ); } - diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index b3925d1f8..7faf34756 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -285,7 +285,6 @@ export function PluginIcon(props: IconProps) { ); } - export function PlusIcon(props: IconProps) { return ( ); } + +export function CloudDownloadIcon(props: IconProps) { + return ( + + + + ); +} + +export function CloudUploadIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/Notice.tsx b/src/components/Notice.tsx new file mode 100644 index 000000000..e03a068dd --- /dev/null +++ b/src/components/Notice.tsx @@ -0,0 +1,72 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { filters, mapMangledModuleLazy } from "@webpack"; +import type { ComponentType, CSSProperties, ReactNode } from "react"; + +export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview"; + +export const NoticeTypes: Record<"WARNING" | "INFO" | "ERROR" | "POSITIVE" | "PREVIEW", NoticeType> = { + WARNING: "warn", + INFO: "info", + ERROR: "danger", + POSITIVE: "positive", + PREVIEW: "preview" +}; + +const { HelpMessage } = mapMangledModuleLazy('POSITIVE="positive"', { + HelpMessage: filters.byCode(".iconDiv") +}); + +export interface NoticeProps { + children: ReactNode; + className?: string; + style?: CSSProperties; + icon?: ComponentType<{ className?: string; color?: string; }>; + action?: ReactNode; + textColor?: string; + textVariant?: string; + hidden?: boolean; +} + +function Warning({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Info({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Error({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +function Positive({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) { + return ( + + ); +} + +export const Notice = { + Warning, + Info, + Error, + Positive +}; diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index df630ca75..2c1d8353e 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -18,7 +18,7 @@ import "./Switch.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { useState } from "@webpack/common"; import type { FocusEvent } from "react"; diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index bde731b08..61522cac7 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -20,8 +20,6 @@ import "./themesStyles.css"; import { isPluginEnabled } from "@api/PluginManager"; import { Settings, useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { Divider } from "@components/Divider"; import { ErrorCard } from "@components/ErrorCard"; @@ -30,10 +28,12 @@ import { FormSwitch } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; import { CogWheel, DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PluginIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; import { AddonCard, openPluginModal, QuickAction, QuickActionCard, SettingsTab, wrapTab } from "@components/settings"; import { OnlineThemeCard } from "@components/settings/OnlineThemeCard"; import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations"; +import { classNameFactory } from "@utils/css"; import { copyWithToast, openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; @@ -727,9 +727,9 @@ function ThemesTab() { }} /> - + Looking for themes? Check out BetterDiscord Themes or search on GitHub. When downloading from BetterDiscord, click "Download" and place the .theme.css file into your themes folder. - +
& { children: React.ReactNode; }) { + return ( + + {tooltipProps => +
+ {children} +
+ } +
+ ); +} diff --git a/src/components/TooltipFallback.tsx b/src/components/TooltipFallback.tsx new file mode 100644 index 000000000..145b43c7d --- /dev/null +++ b/src/components/TooltipFallback.tsx @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Tooltip } from "@vencord/discord-types"; + +const NOOP = () => { }; + +/** Don't use this */ +export const TooltipFallback: Tooltip = ({ children }) => { + if (typeof children !== "function") { + return null; + } + + const node = children({ + onBlur: NOOP, + onFocus: NOOP, + onMouseEnter: NOOP, + onMouseLeave: NOOP, + onClick: NOOP, + onContextMenu: NOOP + }); + + return <>{node}; +}; + +TooltipFallback.Colors = {} as any; diff --git a/src/components/css.ts b/src/components/css.ts deleted file mode 100644 index 51151b1fe..000000000 --- a/src/components/css.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { createAndAppendStyle } from "@utils/css"; - -import { generateTextCss } from "./BaseText"; -import { generateMarginCss } from "./margins"; - -export function addVencordUiStyles() { - createAndAppendStyle("vencord-text", document.head).textContent = generateTextCss(); - createAndAppendStyle("vencord-margins").textContent = generateMarginCss(); -} diff --git a/src/components/index.ts b/src/components/index.ts index a384f977e..9e583f58e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export * from "./Alert"; export * from "./BaseText"; export * from "./Button"; export * from "./Card"; @@ -21,6 +20,7 @@ export * from "./Heart"; export * from "./Icons"; export * from "./Link"; export * from "./margins"; +export * from "./Notice"; export * from "./Paragraph"; export * from "./settings"; export * from "./Span"; diff --git a/src/components/margins.ts b/src/components/margins.ts index 24138d2a8..1ac7aceb4 100644 --- a/src/components/margins.ts +++ b/src/components/margins.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; const marginCls = classNameFactory("vc-margin-"); diff --git a/src/components/settings/AddonCard.tsx b/src/components/settings/AddonCard.tsx index f5798afd6..c99452252 100644 --- a/src/components/settings/AddonCard.tsx +++ b/src/components/settings/AddonCard.tsx @@ -18,10 +18,10 @@ import "./AddonCard.css"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { AddonBadge } from "@components/settings/PluginBadge"; import { Switch } from "@components/Switch"; +import { classNameFactory } from "@utils/css"; import { Tooltip, useRef } from "@webpack/common"; import type { MouseEventHandler, ReactNode } from "react"; diff --git a/src/components/settings/OnlineThemeCard.tsx b/src/components/settings/OnlineThemeCard.tsx index abbbc8bc6..62dbc297e 100644 --- a/src/components/settings/OnlineThemeCard.tsx +++ b/src/components/settings/OnlineThemeCard.tsx @@ -18,10 +18,10 @@ import "./AddonCard.css"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Switch } from "@components/settings"; import { AddonBadge } from "@components/settings/PluginBadge"; +import { classNameFactory } from "@utils/css"; import { useRef } from "@webpack/common"; import type { MouseEventHandler, ReactNode } from "react"; diff --git a/src/components/settings/QuickAction.tsx b/src/components/settings/QuickAction.tsx index a0cd0bcd3..e9cc1f86b 100644 --- a/src/components/settings/QuickAction.tsx +++ b/src/components/settings/QuickAction.tsx @@ -6,8 +6,8 @@ import "./QuickAction.css"; -import { classNameFactory } from "@api/Styles"; import { Card } from "@components/Card"; +import { classNameFactory } from "@utils/css"; import type { ComponentType, PropsWithChildren, ReactNode } from "react"; const cl = classNameFactory("vc-settings-quickActions-"); diff --git a/src/components/settings/SpecialCard.tsx b/src/components/settings/SpecialCard.tsx index 3388f9604..99b1c04be 100644 --- a/src/components/settings/SpecialCard.tsx +++ b/src/components/settings/SpecialCard.tsx @@ -18,11 +18,11 @@ import "./SpecialCard.css"; -import { classNameFactory } from "@api/Styles"; import { Card } from "@components/Card"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; +import { classNameFactory } from "@utils/css"; import { Clickable } from "@webpack/common"; import type { PropsWithChildren } from "react"; diff --git a/src/components/settings/tabs/changelog/NewPluginsSection.tsx b/src/components/settings/tabs/changelog/NewPluginsSection.tsx index fc3a3eba4..a86c562c8 100644 --- a/src/components/settings/tabs/changelog/NewPluginsSection.tsx +++ b/src/components/settings/tabs/changelog/NewPluginsSection.tsx @@ -5,13 +5,13 @@ */ import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { Card } from "@components/Card"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { PluginCard } from "@components/settings/tabs/plugins/PluginCard"; import { ChangeList } from "@utils/ChangeList"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { useForceUpdater } from "@utils/react"; import { React, Tooltip } from "@webpack/common"; diff --git a/src/components/settings/tabs/plugins/ContributorModal.css b/src/components/settings/tabs/plugins/ContributorModal.css index ef3b2ddec..f35d49f57 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.css +++ b/src/components/settings/tabs/plugins/ContributorModal.css @@ -40,7 +40,13 @@ border-radius: 50%; } +.vc-author-modal-root+[class*="footer"] { + padding: 16px; +} + .vc-author-modal-links { + display: flex; + gap: 4px; margin-left: auto; } @@ -48,4 +54,4 @@ display: grid; gap: 0.5em; margin-top: 0.75em; -} \ No newline at end of file +} diff --git a/src/components/settings/tabs/plugins/ContributorModal.tsx b/src/components/settings/tabs/plugins/ContributorModal.tsx index 8bdfbbd13..72c8d84c8 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.tsx +++ b/src/components/settings/tabs/plugins/ContributorModal.tsx @@ -7,15 +7,15 @@ import "./ContributorModal.css"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { HeadingPrimary } from "@components/Heading"; import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { EquicordDevsById, VencordDevsById } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { fetchUserProfile } from "@utils/discord"; -import { classes, pluralise } from "@utils/misc"; -import { ModalContent, ModalRoot, openModal } from "@utils/modal"; +import { pluralise } from "@utils/misc"; +import { ModalContent, ModalFooter, ModalRoot, openModal } from "@utils/modal"; import { User } from "@vencord/discord-types"; import { showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; @@ -30,9 +30,7 @@ export function openContributorModal(user: User) { openModal(modalProps => - - - + ); @@ -67,53 +65,61 @@ function ContributorModal({ user }: { user: User; }) { const ContributedHyperLink = contributed; + const hasLinks = website || githubName; + return ( <> -
- - {user.username} - -
- {website && ( - - )} - {githubName && ( - - )} + +
+ + {user.username}
-
- {plugins.length ? ( - - {user.username} has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}! - - ) : ( - - {user.username} has not made any plugins. They likely {ContributedHyperLink} in other ways! - - )} + {plugins.length ? ( + + {user.username} has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}! + + ) : ( + + {user.username} has not made any plugins. They likely {ContributedHyperLink} in other ways! + + )} - {!!plugins.length && ( -
- {plugins.map(p => - showToast("Restart to apply changes!")} - /> - )} -
+ {!!plugins.length && ( +
+ {plugins.map(p => + showToast("Restart to apply changes!")} + /> + )} +
+ )} + + + {hasLinks && ( + +
+ {website && ( + + )} + {githubName && ( + + )} +
+
)} ); diff --git a/src/components/settings/tabs/plugins/PluginCard.tsx b/src/components/settings/tabs/plugins/PluginCard.tsx index e9275c376..7c5ab1076 100644 --- a/src/components/settings/tabs/plugins/PluginCard.tsx +++ b/src/components/settings/tabs/plugins/PluginCard.tsx @@ -6,9 +6,9 @@ import { showNotice } from "@api/Notices"; import { isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager"; -import { classNameFactory } from "@api/Styles"; import { CogWheel, InfoIcon } from "@components/Icons"; import { AddonCard } from "@components/settings/AddonCard"; +import { classNameFactory } from "@utils/css"; import { Logger } from "@utils/Logger"; import { isObjectEmpty } from "@utils/misc"; import { Plugin } from "@utils/types"; diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 8f9920b6a..47c2c0ec6 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -20,7 +20,6 @@ import "./PluginModal.css"; import { generateId } from "@api/Commands"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -28,6 +27,7 @@ import { Flex } from "@components/Flex"; import { Paragraph } from "@components/Paragraph"; import { debounce } from "@shared/debounce"; import { gitRemote } from "@shared/vencordUserAgent"; +import { classNameFactory } from "@utils/css"; import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; diff --git a/src/components/settings/tabs/plugins/UIElements.tsx b/src/components/settings/tabs/plugins/UIElements.tsx index df85ef738..2010d6c09 100644 --- a/src/components/settings/tabs/plugins/UIElements.tsx +++ b/src/components/settings/tabs/plugins/UIElements.tsx @@ -9,19 +9,18 @@ import "./UIElements.css"; import { ChatBarButtonMap } from "@api/ChatButtons"; import { MessagePopoverButtonMap } from "@api/MessagePopover"; import { SettingsPluginUiElements, useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Card } from "@components/Card"; import { PlaceholderIcon } from "@components/Icons"; import { Paragraph } from "@components/Paragraph"; import { Switch } from "@components/Switch"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { IconComponent } from "@utils/types"; import { Clickable } from "@webpack/common"; - const cl = classNameFactory("vc-plugin-ui-elements-"); export function UIElementsButton() { diff --git a/src/components/settings/tabs/plugins/components/BooleanSetting.tsx b/src/components/settings/tabs/plugins/components/BooleanSetting.tsx index b2464344b..1af93aa59 100644 --- a/src/components/settings/tabs/plugins/components/BooleanSetting.tsx +++ b/src/components/settings/tabs/plugins/components/BooleanSetting.tsx @@ -45,4 +45,3 @@ export function BooleanSetting({ option, pluginSettings, definedSettings, id, on ); } - diff --git a/src/components/settings/tabs/plugins/components/Common.tsx b/src/components/settings/tabs/plugins/components/Common.tsx index a8eaa1c3b..56bb24f10 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { wordsFromCamel, wordsToTitle } from "@utils/text"; import { DefinedSettings, PluginOptionBase } from "@utils/types"; diff --git a/src/components/settings/tabs/plugins/components/SliderSetting.tsx b/src/components/settings/tabs/plugins/components/SliderSetting.tsx index 85a52da29..847cb7b53 100644 --- a/src/components/settings/tabs/plugins/components/SliderSetting.tsx +++ b/src/components/settings/tabs/plugins/components/SliderSetting.tsx @@ -52,4 +52,3 @@ export function SliderSetting({ option, pluginSettings, definedSettings, id, onC ); } - diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 6f8bdbc80..66a35a346 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -21,7 +21,6 @@ import "./styles.css"; import * as DataStore from "@api/DataStore"; import { isPluginEnabled, stopPlugin } from "@api/PluginManager"; import { useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { Card } from "@components/Card"; import { Divider } from "@components/Divider"; @@ -31,6 +30,7 @@ import { Paragraph } from "@components/Paragraph"; import { SettingsTab } from "@components/settings"; import { debounce } from "@shared/debounce"; import { ChangeList } from "@utils/ChangeList"; +import { classNameFactory } from "@utils/css"; import { isTruthy } from "@utils/guards"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; @@ -150,9 +150,10 @@ export default function PluginSettings() { if (!changes.hasChanges) return; const allChanges = [...changes.getChanges()]; + const pluginNames = [...new Set(allChanges.map(s => s.split(":")[0]))]; const maxDisplay = 15; - const displayed = allChanges.slice(0, maxDisplay); - const remainingCount = allChanges.length - displayed.length; + const displayed = pluginNames.slice(0, maxDisplay); + const remainingCount = pluginNames.length - displayed.length; Alerts.show({ title: "Restart required", @@ -264,7 +265,7 @@ export default function PluginSettings() { return lodash.isEqual(newPlugins, sortedPluginNames) ? null : new Set(newPlugins); })); - const handleRestartNeeded = useCallback((name: string) => changes.handleChange(name), [changes]); + const handleRestartNeeded = useCallback((name: string, key: string) => changes.handleChange(`${name}:${key}`), [changes]); const { plugins, requiredPlugins } = useMemo(() => { const plugins = [] as JSX.Element[]; @@ -352,13 +353,12 @@ export default function PluginSettings() { } } - // Code directly taken from supportHelper.tsx const { totalStockPlugins, totalUserPlugins, enabledStockPlugins, enabledUserPlugins, enabledPlugins } = useMemo(() => { const isApiPlugin = (plugin: string) => plugin.endsWith("API") || Plugins[plugin].required; const totalPlugins = Object.keys(Plugins).filter(p => !isApiPlugin(p)); - const enabledPlugins = Object.keys(Plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p)); + const enabledPlugins = Object.keys(Plugins).filter(p => isPluginEnabled(p) && !isApiPlugin(p)); const totalStockPlugins = totalPlugins.filter(p => !PluginMeta[p].userPlugin && !Plugins[p].hidden).length; const totalUserPlugins = totalPlugins.filter(p => PluginMeta[p].userPlugin).length; @@ -451,7 +451,6 @@ export default function PluginSettings() { : } - diff --git a/src/components/settings/tabs/styles.css b/src/components/settings/tabs/styles.css index 83003366c..b5ab95342 100644 --- a/src/components/settings/tabs/styles.css +++ b/src/components/settings/tabs/styles.css @@ -72,6 +72,7 @@ display: grid; grid-template-columns: repeat(3, 1fr); gap: 1em; + margin-top: 16px; } .vc-cloud-erase-data-danger-btn { @@ -79,6 +80,16 @@ background-color: var(--redesign-button-danger-background); } +.vc-cloud-icon-with-button { + display: flex; + gap: 0.5em; + padding-inline: 0.5em 1em; +} + +.vc-cloud-button-icon { + height: 1.25em; +} + .vc-settings-modal { padding: 1.5em !important; } diff --git a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx index f38741e2f..31f082f36 100644 --- a/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx +++ b/src/components/settings/tabs/sync/BackupAndRestoreTab.tsx @@ -17,11 +17,11 @@ */ import { downloadSettingsBackup, uploadSettingsBackup } from "@api/SettingsSync/offline"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { Divider } from "@components/Divider"; import { Flex } from "@components/Flex"; import { Heading } from "@components/Heading"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; import { Margins } from "@utils/margins"; @@ -34,9 +34,9 @@ function BackupAndRestoreTab() { Import and export your Equicord settings as a JSON file. This allows you to easily transfer your settings to another device, or recover them after reinstalling Equicord or Discord. - + Importing a settings file will overwrite your current settings. Make sure to export a backup first if you want to keep your current configuration. - + What's included in a backup diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx index ad6aec597..117ea2ba6 100644 --- a/src/components/settings/tabs/sync/CloudTab.tsx +++ b/src/components/settings/tabs/sync/CloudTab.tsx @@ -19,23 +19,34 @@ import { useSettings } from "@api/Settings"; import { authorizeCloud, deauthorizeCloud } from "@api/SettingsSync/cloudSetup"; import { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from "@api/SettingsSync/cloudSync"; -import { Alert } from "@components/Alert"; import { Button } from "@components/Button"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Divider } from "@components/Divider"; import { Flex } from "@components/Flex"; import { FormSwitch } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; +import { CloudDownloadIcon, CloudUploadIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import { Notice } from "@components/Notice"; import { Paragraph } from "@components/Paragraph"; -import { QuickAction, QuickActionCard } from "@components/settings/QuickAction"; import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab"; +import { localStorage } from "@utils/localStorage"; import { Margins } from "@utils/margins"; +import { useForceUpdater } from "@utils/react"; import { findComponentByCodeLazy } from "@webpack"; -import { Alerts, useState } from "@webpack/common"; +import { Alerts, SearchableSelect, Select, useState } from "@webpack/common"; -const UploadIcon = findComponentByCodeLazy("M12.7 3.3a1 1 0 0 0-1.4 0l-5 5a1 1 0 0 0 1.4 1.4L11 6.42V20"); -const DownloadIcon = findComponentByCodeLazy("M12.7 20.7a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.4l3.3 3.29V4"); +const ICON_STYLE: React.CSSProperties = { width: 20, height: 20, borderRadius: 4, verticalAlign: "middle" }; + +function EquicordIcon() { + return Equicord; +} + +function VencordIcon() { + return Vencord; +} + +const RefreshIcon = findComponentByCodeLazy("M4 12a8 8 0 0 1 14.93-4H15"); const TrashIcon = findComponentByCodeLazy("2.81h8.36a3"); const SkullIcon = findComponentByCodeLazy("m13.47 1 .07.04c.45.06"); @@ -48,9 +59,22 @@ function validateUrl(url: string) { } } +const cloudBackendOptions = [ + { label: "Equicord Cloud", value: "https://cloud.equicord.org/" }, + { label: "Vencord Cloud", value: "https://api.vencord.dev/" } +]; + +const syncDirectionOptions = [ + { label: "Two-way sync (changes go both directions)", value: "both" }, + { label: "This device is the source (upload only)", value: "push" }, + { label: "The cloud is the source (download only)", value: "pull" }, + { label: "Do not sync automatically (manual sync via buttons below only)", value: "manual" } +]; + function CloudTab() { const settings = useSettings(["cloud.authenticated", "cloud.url", "cloud.settingsSync"]); const [inputKey, setInputKey] = useState(0); + const forceUpdate = useForceUpdater(); const { cloud } = settings; const isAuthenticated = cloud.authenticated; @@ -73,11 +97,11 @@ function CloudTab() { Equicord's cloud integration allows you to sync your settings across multiple devices and Discord installations. Your data is securely stored and can be easily restored at any time. - + We use our own Equicloud backend with enhanced features. View our privacy policy to see what we store and how we use your data. Equicloud is BSD 3.0 licensed, so you can self-host if preferred. - + Cloud Backend - + Choose which cloud backend to use for storing your settings. You can switch between Equicord's and Vencord's cloud services, or use a self-hosted instance. - - Current: {cloud.url} - - { - cloud.url = v; - cloud.authenticated = false; - await deauthorizeCloud(); - }} - validate={validateUrl} + o.value === cloud.url)} + onChange={v => changeUrl(v)} + className={Margins.bottom16} + closeOnSelect={true} + renderOptionPrefix={o => o?.value?.includes("equicord") ? : } /> - + +
+ { + cloud.url = v; + cloud.authenticated = false; + await deauthorizeCloud(); + }} + validate={validateUrl} + /> +
- -
@@ -157,27 +176,51 @@ function CloudTab() { hideBorder /> - {isAuthenticated && ( - - putCloudSettings(true)} - disabled={!syncEnabled} - /> - getCloudSettings(true, true)} - disabled={!syncEnabled} - /> - - )} + + + Sync Rules for This Device + + This setting controls how settings move between this device and the cloud. You can let changes flow both ways, or choose one place to be the main source of truth. + + + v === state} - serialize={v => String(v)} - /> - {error && {error}} -
); -} - - -function ModalComponent(props: { iconName: string, Icon: t.Icon; color: number; } & ModalProps) { - const [color, SetColor] = useState((props.color ?? 187)); - const [iconSize, SetIconSize] = useState("lg"); - const [saveType, SetSaveType] = useState("png"); - const [customSize, SetCustomSize] = useState(32); - const onKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - e.preventDefault(); - if (e.key === "ArrowLeft") { - SetColor(color + -1); - } else if (e.key === "ArrowRight") { - SetColor(color + 1); - } - } - }, [color]); - useEffect(() => { - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [onKeyDown]); - const { iconName, Icon } = props; - return ( - - - - - -
-
- -
-
- SetIconSize(newValue)} onError={() => { }} /> - SetCustomSize(value)} /> - SetSaveType(newValue)} onError={() => { }} /> -
-
-
- - - -
); -} - -export function openSaveModal(iconName: string, Icon: t.Icon, colorIndex: number) { - openModal(props => ); -} - diff --git a/src/equicordplugins/iconViewer/styles.css b/src/equicordplugins/iconViewer/styles.css new file mode 100644 index 000000000..d520329ee --- /dev/null +++ b/src/equicordplugins/iconViewer/styles.css @@ -0,0 +1,140 @@ +.vc-icons-tab-grid-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); + gap: 8px; +} + +.vc-icon-box { + display: flex; + flex-direction: column; + align-items: center; +} + +.vc-icon-container { + padding: 12px; + border-radius: 8px; + border: 3px solid transparent; + transition: background-color 0.15s ease; +} + +.vc-icon-container:hover { + background-color: var(--background-modifier-hover); +} + +.vc-icon-icon { + color: var(--interactive-icon-default); + transition: transform 0.15s ease, filter 0.15s ease; +} + +.vc-icon-container:hover .vc-icon-icon { + transform: scale(1.1); + filter: brightness(1.5); +} + +.vc-icon-title { + font-size: 0.75rem; + margin-top: 4px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 64px; +} + +.vc-icon-tab-search-bar-grid { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 16px; +} + +.vc-icon-tab-search-bar-grid > div:first-child { + flex: 1; +} + +.vc-icon-sentinel { + height: 32px; +} + +.vc-ic-modal-header { + align-items: flex-start; + position: relative; + padding: 16px 16px 16px 24px; + z-index: 1; +} + +.vc-ic-modal-header-content { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.vc-ic-modal-header-trailing { + position: absolute; + top: 8px; + right: 8px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} + +.vc-ic-modal-content { + padding: 16px; +} + +.vc-ic-modal-main { + gap: 24px; + align-items: center; +} + +.vc-ic-icon-preview { + width: 128px; + height: 128px; + display: flex; + align-items: center; + justify-content: center; + background-image: repeating-linear-gradient(45deg, + #ffffff1a 0, + #ffffff1a 10px, + #0000001a 10px, + #0000001a 20px); + border-radius: 8px; + cursor: context-menu; +} + +.vc-ic-icon-large { + width: 96px; + height: 96px; +} + +.vc-ic-icon-info { + gap: 12px; +} + +.vc-ic-icon-sizes { + gap: 16px; + align-items: flex-end; +} + +.vc-ic-color-label { + cursor: help; +} + +.vc-ic-modal-footer { + display: flex; + flex-direction: row-reverse; + justify-content: flex-start; + gap: 8px; +} + +.vc-ic-unordered-list li { + margin-left: 16px; + list-style: disc; +} + +.vc-ic-modal-title { + user-select: none; + padding-right: 32px; +} \ No newline at end of file diff --git a/src/equicordplugins/iconViewer/subComponents.tsx b/src/equicordplugins/iconViewer/subComponents.tsx deleted file mode 100644 index 166057b33..000000000 --- a/src/equicordplugins/iconViewer/subComponents.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { BaseText } from "@components/BaseText"; -import { HeadingSecondary } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { copyToClipboard } from "@utils/clipboard"; -import { getIntlMessage } from "@utils/discord"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; -import { Clickable, ContextMenuApi, FluxDispatcher, Menu, TooltipContainer, useState } from "@webpack/common"; -import type { ComponentPropsWithRef, PropsWithChildren } from "react"; - -import { _cssColors, cssColors } from "./utils"; - - -function searchMatch(search: string, name: string): boolean { - if (search === "") return true; - const words = name.toLowerCase().split("_"); - const searchKeywords = search.toLowerCase().split(" ").filter(keyword => keyword !== ""); - return searchKeywords.every(keyword => words.includes(keyword)) || words.every(keyword => searchKeywords.includes(keyword)) || name.toLowerCase().includes(search.toLowerCase()); -} - -export type ClickableProps = PropsWithChildren> & { - tag?: T; -}; - -export function IconTooltip({ children, copy, className, ...props }: ClickableProps & { children: string; copy: string; }) { - return - { - copyToClipboard(copy); - }} {...props}>{children} - ; -} - -export const ModalHeaderTitle = ({ iconName, color, name }: { iconName: string; color: number; name: string; }) => { - return - - {iconName} - - {" - "} - { - ContextMenuApi.openContextMenu(e, () => { - const [query, setQuery] = useState(""); - return ( FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - color="danger" - aria-label="Icon Viewer Colors" - > - - ( - - )} - /> - - {!!_cssColors.length && } - - {_cssColors.map(p => ( - searchMatch(query, p) && { - // @ts-ignore - FluxDispatcher.dispatch({ type: "ICONVIEWER_COLOR_CHANGE", color: p }); - }} - /> - ))} - - ); - }); - }}> - {cssColors[color]?.name} - - ; -}; - -export function SettingsAbout() { - return <> - Features - - -
    -
  • Preview icons
  • -
  • Copy icon names and CSS variables
  • -
  • Ability to download icons in different formats (SVG, PNG, GIF, etc.)
  • -
  • Copy pre-made icon finds for your plugins (Only some icons have this, submit finds either in a server or DMs)
  • -
  • Find icons by function context (helpful when creating finds)
  • -
  • Search for colors by right-clicking the color name in the modal title
  • -
-
-
- Special thanks - - -
    -
  • krystalskullofficial._.
  • -
  • davr1
  • -
  • suffocate
  • -
-
-
- ; -} - diff --git a/src/equicordplugins/iconViewer/types.ts b/src/equicordplugins/iconViewer/types.ts new file mode 100644 index 000000000..61114c3d3 --- /dev/null +++ b/src/equicordplugins/iconViewer/types.ts @@ -0,0 +1,17 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Icon } from "@vencord/discord-types"; + +export type IconsDef = Record; + +export interface CssColorData { + name: string; + css: string; + key: string; +} + +export type IconSize = "xxs" | "xs" | "sm" | "md" | "lg"; diff --git a/src/equicordplugins/iconViewer/utils.ts b/src/equicordplugins/iconViewer/utils.ts new file mode 100644 index 000000000..05fc25e22 --- /dev/null +++ b/src/equicordplugins/iconViewer/utils.ts @@ -0,0 +1,50 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findAll, findByPropsLazy, waitFor } from "@webpack"; + +import { CssColorData, IconSize } from "./types"; + +let colorKeys: string[] = []; + +const Colors = findByPropsLazy("colors", "layout"); + +export const iconSizesInPx: Record = findByPropsLazy("md", "lg", "xxs"); +export const iconSizes: IconSize[] = ["xxs", "xs", "sm", "md", "lg"]; + +export function getCssColorKeys(): string[] { + return colorKeys; +} + +export const cssColors = new Proxy({} as Record, { + get: (target, key) => { + const idx = Number(key); + if (isNaN(idx)) return undefined; + + if (target[idx]) return target[idx]; + + const colorKey = colorKeys[idx]; + if (!colorKey || !Colors.colors[colorKey]?.css) return undefined; + + const name = colorKey.split("_").map(x => x[0].toUpperCase() + x.toLowerCase().slice(1)).join(" "); + target[idx] = { name, css: Colors.colors[colorKey].css, key: colorKey }; + return target[idx]; + } +}); + +export function findAllIcons() { + return findAll(m => { + if (typeof m !== "function") return false; + const str = m.toString?.() ?? ""; + if (str.includes("direction:")) return false; + if (str.includes('viewBox:"0 0 272 143"')) return false; + return str.includes("viewBox:") && str.includes("color:") && (str.includes("foreground:") || str.includes("colorClass:")); + }); +} + +waitFor(["colors", "layout"], m => { + colorKeys = Object.keys(m.colors); +}); diff --git a/src/equicordplugins/iconViewer/utils.tsx b/src/equicordplugins/iconViewer/utils.tsx deleted file mode 100644 index be44b67e0..000000000 --- a/src/equicordplugins/iconViewer/utils.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { saveFile } from "@utils/web"; -import * as t from "@vencord/discord-types"; -import { findAll, findByPropsLazy, waitFor } from "@webpack"; -import { createRoot, ReactDOM } from "@webpack/common"; -export let _cssColors: string[] = []; -export type IconsDef = { [k: string]: t.Icon; }; - -export const iconSizesInPx = findByPropsLazy("md", "lg", "xxs"); -export const Colors = findByPropsLazy("colors", "layout"); - -export const cssColors = new Proxy( - { - }, - { - get: (target, key) => { - const colorKey = _cssColors[key]; - return key in target - ? target[key] - : Colors.colors[colorKey]?.css != null ? (target[key] = { name: colorKey.split("_").map((x: string) => x[0].toUpperCase() + x.toLowerCase().slice(1)).join(" "), css: Colors.colors[colorKey].css, key: colorKey }) : undefined; - }, - set: (target, key, value) => { - target[key] = value; - return true; - } - } -) as unknown as Array<{ name: string; css: string; key: string; }>; - -export const iconSizes = ["xxs", "xs", "sm", "md", "lg"]; - -const CrosspendingTypes: Record = { - "image/png": "png", - "image/jpeg": "jpeg", - "image/gif": "gif", - "image/bmp": "bmp", - "image/tiff": "tiff", - "image/webp": "webp", - "image/svg+xml": "svg", - "image/avif": "avif" -}; - -export function saveIcon(iconName: string, icon: EventTarget & SVGSVGElement | Element | string, color: number, size: number, type = "image/png") { - const filename = `${iconName}-${cssColors[color]?.name ?? "unknown"}-${size}px.${CrosspendingTypes[type] ?? "png"}`; - if (typeof icon === "string") { - const file = new File([icon], filename, { type: "text/plain" }); - saveFile(file); - return; - } - - const innerElements = icon.children; - for (const el of innerElements) { - const fill = el.getAttribute("fill"); - if (fill && fill.startsWith("var(")) { - el.setAttribute("fill", getComputedStyle(icon).getPropertyValue(fill.replace("var(", "").replace(")", ""))); - } - } - - // save svg as the given type - const canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - const img = new Image(); - - img.onload = () => { - ctx.drawImage(img, 0, 0, size, size); - const link = document.createElement("a"); - link.download = filename; - link.href = canvas.toDataURL(type); - link.click(); - }; - - img.src = `data:image/svg+xml;base64,${btoa(icon.outerHTML)}`; -} - - -export function convertComponentToHtml(component?: React.ReactElement): string { - const container = document.createElement("div"); - const root = createRoot(container); - - ReactDOM.flushSync(() => root.render(component)); - const content = container.innerHTML; - root.unmount(); - - return content; -} - -export function findAllIcons() { - return findAll(m => { - if (typeof m !== "function") return false; - const str = m.toString?.() || ""; - if (str.includes("direction:")) return false; - return str.includes("viewBox:") && str.includes("color:") && (str.includes("foreground:") || str.includes("colorClass:")); - }); -} - -waitFor(["colors", "layout"], m => { - _cssColors = Object.keys(m.colors); - cssColors.length = _cssColors.length; -}); diff --git a/src/equicordplugins/ignoreCalls/index.tsx b/src/equicordplugins/ignoreCalls/index.tsx index e0f23a244..239f3c626 100644 --- a/src/equicordplugins/ignoreCalls/index.tsx +++ b/src/equicordplugins/ignoreCalls/index.tsx @@ -8,10 +8,10 @@ import "./styles.css"; import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { ErrorBoundary } from "@components/index"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { Channel } from "@vencord/discord-types"; import { findComponentByCodeLazy } from "@webpack"; @@ -65,7 +65,6 @@ const ContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { ); }; - const settings = definePluginSettings({ permanentlyIgnoredUsers: { type: OptionType.STRING, diff --git a/src/equicordplugins/inRole/RoleMembersModal.tsx b/src/equicordplugins/inRole/RoleMembersModal.tsx index 8b0615f13..6483b105b 100644 --- a/src/equicordplugins/inRole/RoleMembersModal.tsx +++ b/src/equicordplugins/inRole/RoleMembersModal.tsx @@ -4,9 +4,9 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Paragraph } from "@components/Paragraph"; +import { classNameFactory } from "@utils/css"; import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { GuildMember } from "@vencord/discord-types"; import { Parser } from "@webpack/common"; diff --git a/src/equicordplugins/instantScreenshare/index.tsx b/src/equicordplugins/instantScreenshare/index.tsx index 56b418730..322b61f7b 100644 --- a/src/equicordplugins/instantScreenshare/index.tsx +++ b/src/equicordplugins/instantScreenshare/index.tsx @@ -20,18 +20,20 @@ const startStream = findByCodeLazy('type:"STREAM_START"'); const StreamPreviewSettings = getUserSettingLazy("voiceAndVideo", "disableStreamPreviews")!; const ApplicationStreamingSettingsStore = findStoreLazy("ApplicationStreamingSettingsStore"); -async function autoStartStream() { +async function autoStartStream(instant = true) { const selected = SelectedChannelStore.getVoiceChannelId(); if (!selected) return; const channel = ChannelStore.getChannel(selected); + if (!channel) return; + const isGuildChannel = !channel.isDM() && !channel.isGroupDM(); if (channel.type === 13 || isGuildChannel && !PermissionStore.can(PermissionsBits.STREAM, channel)) return; - if (settings.store.autoDeafen && !MediaEngineStore.isSelfDeaf()) { + if (settings.store.autoDeafen && !MediaEngineStore.isSelfDeaf() && instant) { VoiceActions.toggleSelfDeaf(); - } else if (settings.store.autoMute && !MediaEngineStore.isSelfMute()) { + } else if (settings.store.autoMute && !MediaEngineStore.isSelfMute() && instant) { VoiceActions.toggleSelfMute(); } @@ -56,7 +58,8 @@ export default definePlugin({ description: "Instantly screenshare when joining a voice channel with support for desktop sources, windows, and video input devices (cameras, capture cards)", authors: [Devs.HAHALOSAH, Devs.thororen, EquicordDevs.mart], dependencies: ["EquicordToolbox"], - getCurrentMedia, + tags: ["ScreenshareKeybind"], + autoStartStream, settings, settingsAboutComponent: () => ( @@ -80,9 +83,28 @@ export default definePlugin({ ), + patches: [ + { + find: "DISCONNECT_FROM_VOICE_CHANNEL]", + predicate: () => settings.store.keybindScreenshare, + replacement: { + match: /\[\i\.\i\.DISCONNECT_FROM_VOICE_CHANNEL/, + replace: '["INSTANT_SCREEN_SHARE"]:{onTrigger(){$self.autoStartStream(false)},keyEvents:{keyUp:!1,keyDown:!0,blurred:!1,focused:!0}},$&' + }, + }, + { + find: "keybindActionTypes()", + predicate: () => settings.store.keybindScreenshare, + replacement: { + match: /=\[(\{value:\i\.\i\.UNASSIGNED)/, + replace: '=[{value:"INSTANT_SCREEN_SHARE",label:"Instant Screenshare"},$1' + } + } + ], + flux: { async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - if (!settings.store.toolboxManagement) return; + if (!settings.store.toolboxManagement || !settings.store.instantScreenshare) return; const myId = UserStore.getCurrentUser().id; for (const state of voiceStates) { const { userId, channelId } = state; diff --git a/src/equicordplugins/instantScreenshare/utils.tsx b/src/equicordplugins/instantScreenshare/utils.tsx index 05ad4f640..36a0756af 100644 --- a/src/equicordplugins/instantScreenshare/utils.tsx +++ b/src/equicordplugins/instantScreenshare/utils.tsx @@ -6,6 +6,7 @@ import { definePluginSettings } from "@api/Settings"; import { Heading } from "@components/Heading"; +import { Margins } from "@components/margins"; import { Paragraph } from "@components/Paragraph"; import { Logger } from "@utils/Logger"; import { OptionType } from "@utils/types"; @@ -41,6 +42,17 @@ export const settings = definePluginSettings({ description: "Automatically deafen when joining a voice channel (also mutes you)", default: false, }, + instantScreenshare: { + type: OptionType.BOOLEAN, + description: "Enables automatic screenshare feature", + default: true, + }, + keybindScreenshare: { + type: OptionType.BOOLEAN, + description: "Screenshare by keybind in discord keybind settings", + restartNeeded: true, + default: false, + }, toolboxManagement: { type: OptionType.BOOLEAN, description: "Enable/Disable Instant Screenshare", @@ -140,7 +152,7 @@ function SettingSection() { return (
Media source to stream - Resets to main screen if not found + Resets to main screen if not found
); diff --git a/src/equicordplugins/invisibleChat.desktop/index.tsx b/src/equicordplugins/invisibleChat.desktop/index.tsx index 5d3d34ee4..5589f8dec 100644 --- a/src/equicordplugins/invisibleChat.desktop/index.tsx +++ b/src/equicordplugins/invisibleChat.desktop/index.tsx @@ -44,7 +44,6 @@ function PopOverIcon() { ); } - function Indicator() { return ( diff --git a/src/equicordplugins/jellyfinRichPresence/index.tsx b/src/equicordplugins/jellyfinRichPresence/index.tsx index 0508337c3..2b4ac9ec6 100644 --- a/src/equicordplugins/jellyfinRichPresence/index.tsx +++ b/src/equicordplugins/jellyfinRichPresence/index.tsx @@ -13,7 +13,6 @@ import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { ApplicationAssetUtils, FluxDispatcher, showToast } from "@webpack/common"; - interface ActivityAssets { large_image?: string; large_text?: string; diff --git a/src/equicordplugins/jumpTo/index.tsx b/src/equicordplugins/jumpTo/index.tsx index 1d2279048..e74dfc913 100644 --- a/src/equicordplugins/jumpTo/index.tsx +++ b/src/equicordplugins/jumpTo/index.tsx @@ -7,51 +7,126 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { Menu, NavigationRouter, RestAPI } from "@webpack/common"; +import { Channel, Message, User } from "@vencord/discord-types"; +import { Constants, Menu, NavigationRouter, RestAPI, SelectedChannelStore, SelectedGuildStore, Toasts } from "@webpack/common"; -function jumpToFirstMessage(props) { - const guildid = props.guild_id !== null ? props.guild_id : "@me"; - const channelid = props.id; - const url = `/channels/${guildid}/${channelid}/0`; +function jumpToFirstMessage(channelId: string, guildId?: string | null) { + const url = `/channels/${guildId ?? "@me"}/${channelId}/0`; NavigationRouter.transitionTo(url); } -async function jumpToBottom(props) { - const guildid = props.guild_id ?? "@me"; - const channelid = props.id; +async function jumpToLastMessage(channelId: string, guildId?: string | null) { const res = await RestAPI.get({ - url: `/channels/${channelid}/messages?limit=1` + url: Constants.Endpoints.MESSAGES(channelId), + query: { limit: 1 } }); - const lastMessageId = res.body?.[0]?.id; - if (!lastMessageId) return; + const messageId = res.body?.[0]?.id; + if (!messageId) return; - const url = `/channels/${guildid}/${channelid}/${lastMessageId}`; + const url = `/channels/${guildId ?? "@me"}/${channelId}/${messageId}`; NavigationRouter.transitionTo(url); } -const MenuPatch: NavContextMenuPatchCallback = (children, { channel }) => { +async function jumpToUserMessage(channelId: string, guildId: string, userId: string, first: boolean) { + try { + const res = await RestAPI.get({ + url: Constants.Endpoints.SEARCH_GUILD(guildId), + query: { + author_id: userId, + channel_id: channelId, + sort_by: "timestamp", + sort_order: first ? "asc" : "desc" + } + }); + + const messageId = res.body?.messages?.[0]?.[0]?.id; + if (!messageId) { + Toasts.show({ + type: Toasts.Type.FAILURE, + message: "No messages found from this user in this channel.", + id: Toasts.genId() + }); + return; + } + + const url = `/channels/${guildId}/${channelId}/${messageId}`; + NavigationRouter.transitionTo(url); + } catch (e) { + Toasts.show({ + type: Toasts.Type.FAILURE, + message: "Failed to search for messages.", + id: Toasts.genId() + }); + } +} + +const ChannelMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => { + if (!channel) return; + children.push( jumpToFirstMessage(channel)} + action={() => jumpToFirstMessage(channel.id, channel.guild_id)} />, jumpToBottom(channel)} + id="vc-jump-to-last" + label="Jump To Last Message" + action={() => jumpToLastMessage(channel.id, channel.guild_id)} + /> + ); +}; + +const UserMenuPatch: NavContextMenuPatchCallback = (children, { user, channel }: { user: User; channel?: Channel; }) => { + if (!user) return; + + if (!channel || channel.guild_id) return; + + children.push( + jumpToFirstMessage(channel.id, null)} + />, + jumpToLastMessage(channel.id, null)} + /> + ); +}; + +const MessageMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { + if (!message) return; + + const channelId = SelectedChannelStore.getChannelId(); + const guildId = SelectedGuildStore.getGuildId(); + if (!channelId || !guildId) return; + + children.push( + jumpToUserMessage(channelId, guildId, message.author.id, true)} + />, + jumpToUserMessage(channelId, guildId, message.author.id, false)} /> ); }; export default definePlugin({ name: "JumpTo", - description: "Adds context menu options to jump to the start or bottom of a channel/DM", + description: "Adds context menu options to jump to the start or bottom of a channel/DM.", authors: [Devs.Samwich, Devs.thororen], contextMenus: { - "channel-context": MenuPatch, - "user-context": MenuPatch, - "thread-context": MenuPatch + "channel-context": ChannelMenuPatch, + "gdm-context": ChannelMenuPatch, + "thread-context": ChannelMenuPatch, + "user-context": UserMenuPatch, + "message": MessageMenuPatch } }); diff --git a/src/equicordplugins/keyboardNavigation/components/CommandPalette.tsx b/src/equicordplugins/keyboardNavigation/components/CommandPalette.tsx index efaf5e9f9..d860aeace 100644 --- a/src/equicordplugins/keyboardNavigation/components/CommandPalette.tsx +++ b/src/equicordplugins/keyboardNavigation/components/CommandPalette.tsx @@ -6,8 +6,8 @@ import "./style.css"; -import { classNameFactory } from "@api/Styles"; import { actions } from "@equicordplugins/keyboardNavigation/commands"; +import { classNameFactory } from "@utils/css"; import { Logger } from "@utils/Logger"; import { closeAllModals, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { React, TextInput, useEffect, useState } from "@webpack/common"; diff --git a/src/equicordplugins/keyboardNavigation/components/MultipleChoice.tsx b/src/equicordplugins/keyboardNavigation/components/MultipleChoice.tsx index 88f70abbb..2929f1a94 100644 --- a/src/equicordplugins/keyboardNavigation/components/MultipleChoice.tsx +++ b/src/equicordplugins/keyboardNavigation/components/MultipleChoice.tsx @@ -6,8 +6,8 @@ import "./style.css"; -import { classNameFactory } from "@api/Styles"; import { ButtonAction } from "@equicordplugins/keyboardNavigation/commands"; +import { classNameFactory } from "@utils/css"; import { closeAllModals, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { React, TextInput, useEffect, useState } from "@webpack/common"; @@ -33,7 +33,6 @@ export function MultipleChoice({ modalProps, onSelect, choices }: MultipleChoice action => action.label.toLowerCase().includes(queryEh.toLowerCase()) ); - const visibleActions = filteredActions.slice(startIndex, startIndex + 20); const totalActions = filteredActions.length; diff --git a/src/equicordplugins/keyboardNavigation/components/TextInput.tsx b/src/equicordplugins/keyboardNavigation/components/TextInput.tsx index 718191d7d..a070137ac 100644 --- a/src/equicordplugins/keyboardNavigation/components/TextInput.tsx +++ b/src/equicordplugins/keyboardNavigation/components/TextInput.tsx @@ -48,7 +48,6 @@ export function SimpleTextInput({ modalProps, onSelect, placeholder, info }: Sim ); } - export function openSimpleTextInput(placeholder?: string, info?: string): Promise { return new Promise(resolve => { openModal(modalProps => ( diff --git a/src/equicordplugins/keyboardNavigation/index.tsx b/src/equicordplugins/keyboardNavigation/index.tsx index 75bb65d7e..5eaaf5bad 100644 --- a/src/equicordplugins/keyboardNavigation/index.tsx +++ b/src/equicordplugins/keyboardNavigation/index.tsx @@ -5,8 +5,8 @@ */ import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { closeAllModals } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; import { openUserSettingsPanel, useState } from "@webpack/common"; @@ -84,7 +84,6 @@ export const settings = definePluginSettings({ } }); - export default definePlugin({ name: "KeyboardNavigation", description: "Allows you to navigate the UI with a keyboard.", @@ -108,7 +107,6 @@ export default definePlugin({ document.removeEventListener("keydown", this.event); }, - event(e: KeyboardEvent) { enum Modifiers { diff --git a/src/equicordplugins/keywordNotify/index.tsx b/src/equicordplugins/keywordNotify/index.tsx index 5f98923f9..52556e4dd 100644 --- a/src/equicordplugins/keywordNotify/index.tsx +++ b/src/equicordplugins/keywordNotify/index.tsx @@ -8,12 +8,12 @@ import "./style.css"; import { DataStore } from "@api/index"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { FormSwitch } from "@components/FormSwitch"; import { Heading, HeadingTertiary } from "@components/Heading"; import { DeleteIcon } from "@components/Icons"; import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { useForceUpdater } from "@utils/react"; diff --git a/src/equicordplugins/limitMiddleClickPaste/index.ts b/src/equicordplugins/limitMiddleClickPaste/index.ts index 0cce627c4..e019c7339 100644 --- a/src/equicordplugins/limitMiddleClickPaste/index.ts +++ b/src/equicordplugins/limitMiddleClickPaste/index.ts @@ -20,7 +20,6 @@ import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { makeRange, OptionType } from "@utils/types"; - const settings = definePluginSettings({ limitTo: { type: OptionType.SELECT, diff --git a/src/equicordplugins/loginWithQR/index.tsx b/src/equicordplugins/loginWithQR/index.tsx index 6150f4cb6..7be30e5b4 100644 --- a/src/equicordplugins/loginWithQR/index.tsx +++ b/src/equicordplugins/loginWithQR/index.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { plugins } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { Paragraph } from "@components/Paragraph"; import { EquicordDevs } from "@utils/constants"; @@ -26,7 +27,7 @@ export default definePlugin({ type: OptionType.COMPONENT, description: "Scan a QR code", component() { - if (!Vencord.Plugins.plugins.LoginWithQR.started) + if (!plugins.LoginWithQR.started) return ( Enable the plugin and restart your client to scan a login QR code diff --git a/src/equicordplugins/loginWithQR/ui/index.ts b/src/equicordplugins/loginWithQR/ui/index.ts index 1277db7a5..0683fa372 100644 --- a/src/equicordplugins/loginWithQR/ui/index.ts +++ b/src/equicordplugins/loginWithQR/ui/index.ts @@ -6,7 +6,7 @@ import "./styles.css"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { findComponentByCodeLazy } from "@webpack"; import { ComponentType, HTMLAttributes } from "react"; diff --git a/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx b/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx index 11f68fade..a3bc0473c 100644 --- a/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx +++ b/src/equicordplugins/loginWithQR/ui/modals/QrModal.tsx @@ -5,6 +5,7 @@ */ import { BaseText } from "@components/BaseText"; +import loginWithQR from "@equicordplugins/loginWithQR"; import { images } from "@equicordplugins/loginWithQR/images"; import { getIntlMessage } from "@utils/discord"; import { @@ -248,10 +249,8 @@ function QrModal(props: ModalProps) { }); useEffect(() => { - const plugin = Vencord.Plugins.plugins.LoginWithQR as any; - - plugin.qrModalOpen = true; - return () => void (plugin.qrModalOpen = false); + loginWithQR.qrModalOpen = true; + return () => void (loginWithQR.qrModalOpen = false); }, []); useEffect(() => { diff --git a/src/equicordplugins/mediaPlaybackSpeed/index.tsx b/src/equicordplugins/mediaPlaybackSpeed/index.tsx index 12d13f45d..396913d0e 100644 --- a/src/equicordplugins/mediaPlaybackSpeed/index.tsx +++ b/src/equicordplugins/mediaPlaybackSpeed/index.tsx @@ -7,9 +7,9 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { makeRange, OptionType } from "@utils/types"; import { ContextMenuApi, FluxDispatcher, Menu, React, Tooltip, useEffect } from "@webpack/common"; import { RefObject } from "react"; diff --git a/src/equicordplugins/messageFetchTimer/index.tsx b/src/equicordplugins/messageFetchTimer/index.tsx index 93640440b..139a35209 100644 --- a/src/equicordplugins/messageFetchTimer/index.tsx +++ b/src/equicordplugins/messageFetchTimer/index.tsx @@ -144,7 +144,6 @@ function handleMessageLoad(data: any) { currentFetch = null; } - export default definePlugin({ name: "MessageFetchTimer", description: "Shows how long it took to fetch messages for the current channel", diff --git a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts index e83a30e61..5408349e9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts +++ b/src/equicordplugins/messageLoggerEnhanced/LoggedMessageManager.ts @@ -17,18 +17,35 @@ */ import { Flogger, settings } from "."; -import { addMessageIDB, db, DBMessageStatus, deleteMessagesBulkIDB, 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"; -export const addMessage = async (message: LoggedMessage | LoggedMessageJSON, status: DBMessageStatus) => { +let lastCleanupTime = 0; +const CLEANUP_COOLDOWN = 60 * 1000; + +export const addMessage = async (message: LoggedMessage | LoggedMessageJSON, status: DBMessageStatus, currentChannelId?: string) => { if (settings.store.saveImages && status === DBMessageStatus.DELETED) await cacheMessageImages(message); const finalMessage = cleanupMessage(message); await addMessageIDB(finalMessage, status); + if (settings.store.timeBasedCleanupMinutes > 0) { + const now = Date.now(); + if (now - lastCleanupTime > CLEANUP_COOLDOWN) { + lastCleanupTime = now; + const cutoffTime = new Date(now - settings.store.timeBasedCleanupMinutes * 60 * 1000).toISOString(); + const oldGuildMessages = await getOlderThanTimestampForGuildsIDB(cutoffTime, currentChannelId, settings.store.preserveCurrentChannel); + + if (oldGuildMessages.length > 0) { + Flogger.info(`Deleting ${oldGuildMessages.length} old server messages older than ${settings.store.timeBasedCleanupMinutes} minutes`); + await deleteMessagesBulkIDB(oldGuildMessages.map(m => m.message_id)); + } + } + } + if (settings.store.messageLimit > 0) { const currentMessageCount = await db.count("messages"); if (currentMessageCount > settings.store.messageLimit) { diff --git a/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx b/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx index 28f1e31e6..c0d2ff937 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; import { Heading } from "@components/Heading"; import { DEFAULT_IMAGE_CACHE_DIR } from "@equicordplugins/messageLoggerEnhanced/utils/constants"; +import { classNameFactory } from "@utils/css"; import { copyWithToast } from "@utils/discord"; import { Button, Toasts } from "@webpack/common"; diff --git a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx index 04fbf62e0..5102ee9ce 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Flex } from "@components/Flex"; import { InfoIcon } from "@components/Icons"; @@ -13,6 +12,7 @@ import { settings } from "@equicordplugins/messageLoggerEnhanced/index"; import { LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; import { messageJsonToMessageClass } from "@equicordplugins/messageLoggerEnhanced/utils"; import { importLogs } from "@equicordplugins/messageLoggerEnhanced/utils/settingsUtils"; +import { classNameFactory } from "@utils/css"; import { copyWithToast, openUserProfile } from "@utils/discord"; import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { LazyComponent } from "@utils/react"; @@ -232,7 +232,6 @@ function LogsContent({ visibleMessages, canLoadMore, sortNewest, tab, reset, han const LogsContentMemo = LazyComponent(() => React.memo(LogsContent)); - function NoResults({ tab }: { tab: LogTabs; }) { const generateSuggestedTabs = (tab: LogTabs) => { switch (tab) { diff --git a/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts b/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts index 56caf2b9b..23315ca83 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts +++ b/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts @@ -52,7 +52,6 @@ export function useMessages(query: string, currentTab: LogTabs, sortNewest: bool countMessagesByStatusIDB(status), ]); - if (isMounted) { setMessages(messages); setStatusTotal(statusTotal); @@ -92,11 +91,9 @@ export function useMessages(query: string, currentTab: LogTabs, sortNewest: bool }, [debouncedQuery, sortNewest, numDisplayedMessages, currentTab, pending]); - return { messages, statusTotal, total, pending, reset: () => setPending(true) }; } - function getStatus(currentTab: LogTabs) { switch (currentTab) { case LogTabs.DELETED: diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 6288f1d51..21dcaa3a8 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { ChannelStore } from "@webpack/common"; import { DBSchema, IDBPDatabase, openDB } from "idb"; import { LoggedMessageJSON } from "./types"; @@ -109,6 +110,36 @@ export async function getOldestMessagesIDB(limit: number) { return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit)); } +export async function getOlderThanTimestampIDB(timestamp: string) { + const tx = db.transaction("messages", "readonly"); + const { store } = tx; + const index = store.index("by_timestamp"); + + const cursor = await index.openCursor(IDBKeyRange.upperBound(timestamp)); + + if (!cursor) { + return []; + } + + 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) { + const allOldMessages = await getOlderThanTimestampIDB(timestamp); + return allOldMessages.filter(record => { + const { message } = record; + const channel = ChannelStore.getChannel(message.channel_id); + const isGuildMessage = channel?.guild_id != null; + const isCurrentChannel = preserveCurrentChannel && currentChannelId && message.channel_id === currentChannelId; + return isGuildMessage && !isCurrentChannel; + }); +} + export async function getDateStortedMessagesByStatusIDB(newest: boolean, limit: number, status: DBMessageStatus) { const tx = db.transaction("messages", "readonly"); const { store } = tx; @@ -179,7 +210,6 @@ export async function addMessagesBulkIDB(messages: LoggedMessageJSON[], status?: messages.forEach(message => cachedMessages.set(message.id, message)); } - export async function deleteMessageIDB(message_id: string) { await db.delete("messages", message_id); diff --git a/src/equicordplugins/messageLoggerEnhanced/index.tsx b/src/equicordplugins/messageLoggerEnhanced/index.tsx index fb6f707b6..db9a4238b 100644 --- a/src/equicordplugins/messageLoggerEnhanced/index.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/index.tsx @@ -8,11 +8,11 @@ export const Native = getNative(); import "./styles.css"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { FluxDispatcher, MessageStore, UserStore } from "@webpack/common"; +import { FluxDispatcher, MessageStore, SelectedChannelStore, UserStore } from "@webpack/common"; import { LogsIcon, OpenLogsButton } from "./components/LogsButton"; import { openLogModal } from "./components/LogsModal"; @@ -84,13 +84,13 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo }); } - if (message == null || message.channel_id == null || !message.deleted) return; // Flogger.log("ADDING MESSAGE (DELETED)", message); if (payload.isBulk) return message; - await addMessage(message, ghostPinged ? idb.DBMessageStatus.GHOST_PINGED : idb.DBMessageStatus.DELETED); + const currentChannelId = SelectedChannelStore.getChannelId(); + await addMessage(message, ghostPinged ? idb.DBMessageStatus.GHOST_PINGED : idb.DBMessageStatus.DELETED, currentChannelId); } finally { handledMessageIds.delete(payload.id); @@ -106,6 +106,17 @@ async function messageDeleteBulkHandler({ channelId, guildId, ids }: MessageDele } await idb.addMessagesBulkIDB(messages); + + if (messages.length > 0 && settings.store.timeBasedCleanupMinutes > 0) { + const currentChannelId = SelectedChannelStore.getChannelId(); + const cutoffTime = new Date(Date.now() - settings.store.timeBasedCleanupMinutes * 60 * 1000).toISOString(); + const oldGuildMessages = await idb.getOlderThanTimestampForGuildsIDB(cutoffTime, currentChannelId, settings.store.preserveCurrentChannel); + + if (oldGuildMessages.length > 0) { + Flogger.info(`Deleting ${oldGuildMessages.length} old server messages older than ${settings.store.timeBasedCleanupMinutes} minutes (bulk cleanup)`); + await idb.deleteMessagesBulkIDB(oldGuildMessages.map(m => m.message_id)); + } + } } async function messageUpdateHandler(payload: MessageUpdatePayload) { @@ -155,7 +166,8 @@ async function messageUpdateHandler(payload: MessageUpdatePayload) { if (message == null || message.channel_id == null || message.editHistory == null || message.editHistory.length === 0) return; // Flogger.log("ADDING MESSAGE (EDITED)", message, payload); - await addMessage(message, idb.DBMessageStatus.EDITED); + const currentChannelId = SelectedChannelStore.getChannelId(); + await addMessage(message, idb.DBMessageStatus.EDITED, currentChannelId); } function messageCreateHandler(payload: MessageCreatePayload) { @@ -229,7 +241,7 @@ async function processMessageFetch(response: FetchMessagesResponse) { export default definePlugin({ name: "MessageLoggerEnhanced", - authors: [Devs.Aria], + authors: [Devs.Aria, EquicordDevs.keyages], description: "G'day", dependencies: ["MessageLogger"], @@ -262,7 +274,6 @@ export default definePlugin({ replace: "$1childrenAccessories:arguments[0].childrenAccessories || null,childrenHeader:" } }, - // https://regex101.com/r/S3IVGm/1 // fix vidoes failing because there are no thumbnails { diff --git a/src/equicordplugins/messageLoggerEnhanced/native/index.ts b/src/equicordplugins/messageLoggerEnhanced/native/index.ts index 861f5fada..cfde35f03 100644 --- a/src/equicordplugins/messageLoggerEnhanced/native/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/native/index.ts @@ -30,8 +30,6 @@ let imageCacheDir: string; const getImageCacheDir = async () => imageCacheDir ?? await getDefaultNativeImageDir(); const getLogsDir = async () => logsDir ?? await getDefaultNativeDataDir(); - - export async function initDirs() { const { logsDir: ld, imageCacheDir: icd } = await getSettings(); @@ -88,14 +86,12 @@ export async function deleteFileNative(_event: IpcMainInvokeEvent, attachmentId: await unlink(imagePath); } - export async function writeLogs(_event: IpcMainInvokeEvent, contents: string) { const logsDir = await getLogsDir(); writeFile(path.join(logsDir, LOGS_DATA_FILENAME), contents); } - export async function getDefaultNativeImageDir(): Promise { return path.join(await getDefaultNativeDataDir(), "savedImages"); } diff --git a/src/equicordplugins/messageLoggerEnhanced/settings.tsx b/src/equicordplugins/messageLoggerEnhanced/settings.tsx index 695a999da..fd54ebe2c 100644 --- a/src/equicordplugins/messageLoggerEnhanced/settings.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/settings.tsx @@ -165,6 +165,18 @@ export const settings = definePluginSettings({ description: "Maximum number of messages to store in the cache. Older messages are deleted when the limit is reached. This helps reduce memory usage and improve performance. 0 means there is no limit", }, + timeBasedCleanupMinutes: { + default: 0, + type: OptionType.NUMBER, + description: "Automatically remove messages from servers that are older than this many minutes. Set to 0 to disable time-based cleanup.", + }, + + preserveCurrentChannel: { + default: true, + type: OptionType.BOOLEAN, + description: "When enabled, messages in your currently selected channel are not affected by time-based cleanup.", + }, + whitelistedIds: { default: "", type: OptionType.STRING, diff --git a/src/equicordplugins/messageLoggerEnhanced/types.ts b/src/equicordplugins/messageLoggerEnhanced/types.ts index 7df6bf714..443689efc 100644 --- a/src/equicordplugins/messageLoggerEnhanced/types.ts +++ b/src/equicordplugins/messageLoggerEnhanced/types.ts @@ -80,7 +80,6 @@ export interface MessageDeleteBulkPayload { channelId: string; } - export interface MessageUpdatePayload { type: string; guildId: string; diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts index f9813b02f..0c55a1723 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts @@ -27,7 +27,6 @@ import { findLastIndex, getGuildIdByChannel } from "./misc"; export * from "./cleanUp"; export * from "./misc"; - // stolen from mlv2 // https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/MessageLoggerV2/MessageLoggerV2.plugin.js#L2367 interface Id { id: string, time: number; message?: LoggedMessageJSON; } @@ -126,7 +125,6 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi const isAuthorBlacklisted = blacklistedIds.includes(authorId); const isChannelBlacklisted = blacklistedIds.includes(channelId); - const shouldIgnoreMutedGuilds = settings.store.ignoreMutedGuilds; const shouldIgnoreMutedCategories = settings.store.ignoreMutedCategories; const shouldIgnoreMutedChannels = settings.store.ignoreMutedChannels; diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts index 9a48bedec..0883049ee 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts @@ -72,8 +72,6 @@ export const sortMessagesByDate = (timestampA: string, timestampB: string) => { } }; - - // stolen from mlv2 export function findLastIndex(array: T[], predicate: (e: T, t: number, n: T[]) => boolean) { let l = array.length; @@ -95,7 +93,6 @@ export const mapTimestamp = (m: any) => { return m; }; - export const messageJsonToMessageClass = memoize((log: { message: LoggedMessageJSON; }) => { // console.time("message populate"); if (!log?.message) return null; @@ -128,7 +125,6 @@ export const messageJsonToMessageClass = memoize((log: { message: LoggedMessageJ return message; }); - export function parseJSON(json?: string | null) { try { return JSON.parse(json!); diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts b/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts index fc2408a0a..62a5c4afc 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts @@ -22,7 +22,6 @@ import { ChannelStore, GuildStore } from "@webpack/common"; import { getGuildIdByChannel } from "./index"; import { memoize } from "./memoize"; - const validIdSearchTypes = ["server", "guild", "channel", "in", "user", "from", "message", "has", "before", "after", "around", "near", "during"] as const; type ValidIdSearchTypesUnion = typeof validIdSearchTypes[number]; diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts index 61a02069b..229ef361e 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts @@ -79,13 +79,11 @@ export async function deleteImage(attachmentId: string): Promise { if (idbPath) return await del(idbPath, ImageStore); - if (IS_WEB) return; await Native.deleteFileNative(attachmentId); } - async function downloadAttachmentWeb(attachemnt: LoggedAttachment, attempts = 0) { if (!attachemnt?.url || !attachemnt?.id || !attachemnt?.fileExtension) { Flogger.error("Invalid attachment", attachemnt); diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts index 7c9a57594..7a2825846 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts @@ -105,4 +105,3 @@ export async function exportLogs() { DiscordNative.fileManager.saveWithDialog(data, filename); } } - diff --git a/src/equicordplugins/micLoopbackTester/index.tsx b/src/equicordplugins/micLoopbackTester/index.tsx new file mode 100644 index 000000000..f0f249fd6 --- /dev/null +++ b/src/equicordplugins/micLoopbackTester/index.tsx @@ -0,0 +1,130 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { UserAreaButton, UserAreaButtonFactory, UserAreaRenderProps } from "@api/UserArea"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { MediaEngineStore, React, UserStore, VoiceActions, VoiceStateStore } from "@webpack/common"; + +let loopbackActive = false; +let selfDeafenedByPlugin = false; + +function isInVoiceChannel() { + const id = UserStore.getCurrentUser()?.id; + if (!id) return false; + const state = VoiceStateStore.getVoiceStateForUser(id); + return Boolean(state?.channelId); +} + +async function enableLoopback() { + try { + await VoiceActions.setLoopback("mic_test", true); + loopbackActive = true; + + if (isInVoiceChannel() && !MediaEngineStore.isSelfDeaf()) { + await VoiceActions.toggleSelfDeaf(); + selfDeafenedByPlugin = true; + } else { + selfDeafenedByPlugin = false; + } + + return true; + } catch (e) { + return false; + } +} + +async function disableLoopback() { + try { + await VoiceActions.setLoopback("mic_test", false); + loopbackActive = false; + + if (selfDeafenedByPlugin && MediaEngineStore.isSelfDeaf()) { + await VoiceActions.toggleSelfDeaf(); + } + selfDeafenedByPlugin = false; + } catch (e) { } +} + +function MicLoopbackIcon({ active = false, className = "" }: { active?: boolean; className?: string; }) { + const redLinePath = "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z"; + const maskBlackPath = "M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z"; + + return ( + + + + {!active && <> + + + + + + } + + ); +} + +function MicLoopbackButton({ iconForeground, hideTooltips, nameplate }: UserAreaRenderProps) { + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + + const handleToggle = React.useCallback(async () => { + if (loopbackActive) { + await disableLoopback(); + } else { + await enableLoopback(); + } + forceUpdate(); + }, []); + + return ( + } + role="switch" + aria-checked={!loopbackActive} + redGlow={!loopbackActive} + plated={nameplate != null} + onClick={handleToggle} + /> + ); +} + +const MicLoopbackUserAreaButton: UserAreaButtonFactory = props => ; + +export default definePlugin({ + name: "MicLoopbackTester", + description: "Adds mic loopback test icon to the user panel", + authors: [EquicordDevs.benjii], + dependencies: ["UserSettingsAPI", "UserAreaAPI"], + userAreaButton: { + icon: MicLoopbackIcon, + render: MicLoopbackUserAreaButton + }, + + stop() { + void disableLoopback(); + loopbackActive = false; + selfDeafenedByPlugin = false; + }, +}); diff --git a/src/equicordplugins/moreCommands/utils.ts b/src/equicordplugins/moreCommands/utils.ts index e5d0b3068..38a53a517 100644 --- a/src/equicordplugins/moreCommands/utils.ts +++ b/src/equicordplugins/moreCommands/utils.ts @@ -27,7 +27,6 @@ export const settings = definePluginSettings({ } }); - export function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } diff --git a/src/equicordplugins/moreStickers/components/icons.tsx b/src/equicordplugins/moreStickers/components/icons.tsx index c0df54827..cd1f5384c 100644 --- a/src/equicordplugins/moreStickers/components/icons.tsx +++ b/src/equicordplugins/moreStickers/components/icons.tsx @@ -20,7 +20,6 @@ export function IconContainer(props: { children: JSX.Element | JSX.Element[]; }) ); } - export function SearchIcon({ width, height, color }: { width: number, height: number, color: string; }) { return ( diff --git a/src/equicordplugins/moreStickers/components/misc.tsx b/src/equicordplugins/moreStickers/components/misc.tsx index 94e7c1b40..783ded6d4 100644 --- a/src/equicordplugins/moreStickers/components/misc.tsx +++ b/src/equicordplugins/moreStickers/components/misc.tsx @@ -445,7 +445,6 @@ export const Packs = () => { ); }; - export function Header(props: { children: JSX.Element | JSX.Element[]; }) { return (
diff --git a/src/equicordplugins/moreStickers/components/picker.tsx b/src/equicordplugins/moreStickers/components/picker.tsx index e4fbb7edb..6c652a3c8 100644 --- a/src/equicordplugins/moreStickers/components/picker.tsx +++ b/src/equicordplugins/moreStickers/components/picker.tsx @@ -168,7 +168,6 @@ function PickerContentRow({ rowIndex, grid1, grid2, grid3, channelId }: PickerCo ); } - function HeaderCollapseIcon({ isExpanded }: { isExpanded: boolean; }) { return ( { const [query, setQuery] = React.useState(); diff --git a/src/equicordplugins/moreStickers/upload.ts b/src/equicordplugins/moreStickers/upload.ts index 83c516fdd..64d3e48b2 100644 --- a/src/equicordplugins/moreStickers/upload.ts +++ b/src/equicordplugins/moreStickers/upload.ts @@ -16,7 +16,6 @@ const PendingReplyStore = findByPropsLazy("getPendingReply"); const MessageUtils = findByPropsLazy("sendMessage"); const DraftStore = findByPropsLazy("getDraft", "getState"); - export const ffmpeg = new FFmpeg(); async function resizeImage(url: string) { diff --git a/src/equicordplugins/moreStickers/utils.tsx b/src/equicordplugins/moreStickers/utils.tsx index 0af5de359..0933e1458 100644 --- a/src/equicordplugins/moreStickers/utils.tsx +++ b/src/equicordplugins/moreStickers/utils.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import type { FFmpeg } from "@ffmpeg/ffmpeg"; +import { classNameFactory } from "@utils/css"; import { waitFor } from "@webpack"; import { React } from "@webpack/common"; diff --git a/src/equicordplugins/moreUserTags/index.tsx b/src/equicordplugins/moreUserTags/index.tsx index ddf0db266..4889178e0 100644 --- a/src/equicordplugins/moreUserTags/index.tsx +++ b/src/equicordplugins/moreUserTags/index.tsx @@ -6,8 +6,8 @@ import "./styles.css"; -import { classNameFactory } from "@api/Styles"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { getCurrentChannel, getIntlMessage } from "@utils/discord"; import definePlugin from "@utils/types"; import { Channel, Message, User } from "@vencord/discord-types"; diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 14e798e8b..f90b7ac7c 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -18,7 +18,6 @@ import "./styles.css"; -import { migratePluginToSetting } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -31,8 +30,6 @@ import { TidalLyrics } from "./tidal/lyrics/components/lyrics"; import { TidalPlayer } from "./tidal/TidalPlayer"; import { YtmPlayer } from "./youtubeMusic/PlayerComponent"; -migratePluginToSetting("MusicControls", "SpotifyControls", "showSpotifyControls"); -migratePluginToSetting("MusicControls", "SpotifyLyrics", "showSpotifyLyrics"); export default definePlugin({ name: "MusicControls", description: "Music Controls and Lyrics for multiple services ", @@ -93,7 +90,6 @@ export default definePlugin({ }, ], - PanelWrapper({ VencordOriginal, ...props }) { const { showTidalControls, showTidalLyrics, showSpotifyLyrics, showSpotifyControls, LyricsPosition, showYoutubeMusicControls } = settings.store; return ( diff --git a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx index 6ea78af69..cfde0c29e 100644 --- a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx +++ b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx @@ -19,12 +19,12 @@ import "./spotifyStyles.css"; import { Settings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; import { Paragraph } from "@components/Paragraph"; import { Span } from "@components/Span"; import { debounce } from "@shared/debounce"; +import { classNameFactory } from "@utils/css"; import { copyWithToast, openImageModal } from "@utils/discord"; import { classes } from "@utils/misc"; import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; @@ -215,7 +215,6 @@ function SpotifySeekBar() { ); } - function AlbumContextMenu({ track }: { track: Track; }) { const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume); diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx index fa09f9243..aafdd0193 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx @@ -5,11 +5,12 @@ */ import { BaseText } from "@components/BaseText"; +import { TooltipContainer } from "@components/TooltipContainer"; import { settings } from "@equicordplugins/musicControls/settings"; import { SpotifyLrcStore } from "@equicordplugins/musicControls/spotify/lyrics/providers/store"; import { SpotifyStore } from "@equicordplugins/musicControls/spotify/SpotifyStore"; import { openModal } from "@utils/modal"; -import { ContextMenuApi, React, TooltipContainer, useEffect, useState, useStateFromStores } from "@webpack/common"; +import { ContextMenuApi, React, useEffect, useState, useStateFromStores } from "@webpack/common"; import { LyricsContextMenu } from "./ctxMenu"; import { LyricsModal } from "./modal"; diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx index 895ea8bb4..742c7148a 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx @@ -4,11 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { settings } from "@equicordplugins/musicControls/settings"; import { SpotifyLrcStore } from "@equicordplugins/musicControls/spotify/lyrics/providers/store"; import { SyncedLyric } from "@equicordplugins/musicControls/spotify/lyrics/providers/types"; import { SpotifyStore } from "@equicordplugins/musicControls/spotify/SpotifyStore"; +import { classNameFactory } from "@utils/css"; import { findByPropsLazy } from "@webpack"; import { React, useEffect, useState, useStateFromStores } from "@webpack/common"; @@ -24,7 +24,6 @@ export function NoteSvg() { ); } - const getIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => { const posInSec = (position + delay) / 1000; @@ -82,7 +81,6 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { } }, [currentLyrics]); - useEffect(() => { if (currentLyrics && position != null) { const [currentIndex, nextLyricIndex] = getIndexes(currentLyrics, position, LyricDelay); diff --git a/src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts index 5ef6391b0..e3fe48d58 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts @@ -19,7 +19,6 @@ interface Line { endTimeMs: string; } - export async function getLyricsSpotify(trackId: string): Promise { const resp = await fetch("https://spotify-lyrics-api-pi.vercel.app/?trackid=" + trackId); if (!resp.ok) return null; diff --git a/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx b/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx index c23976314..256a742e0 100644 --- a/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx +++ b/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx @@ -328,7 +328,6 @@ export function TidalPlayer() { const isPlaying = useStateFromStores([TidalStore], () => TidalStore.isPlaying); const [shouldHide, setShouldHide] = useState(false); - React.useEffect(() => { setShouldHide(false); if (!isPlaying) { @@ -351,4 +350,3 @@ export function TidalPlayer() {
); } - diff --git a/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx index e5a5687b6..903565367 100644 --- a/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx @@ -5,11 +5,12 @@ */ import { BaseText } from "@components/BaseText"; +import { TooltipContainer } from "@components/TooltipContainer"; import { settings } from "@equicordplugins/musicControls/settings"; import { TidalLrcStore } from "@equicordplugins/musicControls/tidal/lyrics/providers/store"; import { TidalStore } from "@equicordplugins/musicControls/tidal/TidalStore"; import { openModal } from "@utils/modal"; -import { ContextMenuApi, TooltipContainer, useEffect, useState, useStateFromStores } from "@webpack/common"; +import { ContextMenuApi, useEffect, useState, useStateFromStores } from "@webpack/common"; import { LyricsContextMenu } from "./ctxMenu"; import { LyricsModal } from "./modal"; diff --git a/src/equicordplugins/musicControls/tidal/lyrics/components/util.tsx b/src/equicordplugins/musicControls/tidal/lyrics/components/util.tsx index f2cbd6fbb..8aa0002b2 100644 --- a/src/equicordplugins/musicControls/tidal/lyrics/components/util.tsx +++ b/src/equicordplugins/musicControls/tidal/lyrics/components/util.tsx @@ -4,11 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { settings } from "@equicordplugins/musicControls/settings"; import { TidalLrcStore } from "@equicordplugins/musicControls/tidal/lyrics/providers/store"; import { EnhancedLyric } from "@equicordplugins/musicControls/tidal/lyrics/types"; import { TidalStore } from "@equicordplugins/musicControls/tidal/TidalStore"; +import { classNameFactory } from "@utils/css"; import { findByPropsLazy } from "@webpack"; import { React, useEffect, useState, useStateFromStores } from "@webpack/common"; diff --git a/src/equicordplugins/musicControls/youtubeMusic/PlayerComponent.tsx b/src/equicordplugins/musicControls/youtubeMusic/PlayerComponent.tsx index fe5f29009..e0a71e61f 100644 --- a/src/equicordplugins/musicControls/youtubeMusic/PlayerComponent.tsx +++ b/src/equicordplugins/musicControls/youtubeMusic/PlayerComponent.tsx @@ -347,7 +347,6 @@ export function YtmPlayer() { const isPlaying = useStateFromStores([YoutubeMusicStore], () => YoutubeMusicStore.isPlaying); const [shouldHide, setShouldHide] = useState(false); - React.useEffect(() => { setShouldHide(false); if (!isPlaying) { @@ -370,4 +369,3 @@ export function YtmPlayer() {
); } - diff --git a/src/equicordplugins/musicControls/youtubeMusic/YtmStore.ts b/src/equicordplugins/musicControls/youtubeMusic/YtmStore.ts index 75a8e2c48..22256b63c 100644 --- a/src/equicordplugins/musicControls/youtubeMusic/YtmStore.ts +++ b/src/equicordplugins/musicControls/youtubeMusic/YtmStore.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { isPluginEnabled } from "@api/PluginManager"; import { Logger } from "@utils/Logger"; import { proxyLazyWebpack } from "@webpack"; import { Flux, FluxDispatcher } from "@webpack/common"; @@ -31,7 +32,6 @@ enum MediaType { export type RepeatMode = "NONE" | "ONE" | "ALL"; - export interface Song { title: string; artist: string; @@ -166,7 +166,6 @@ class YoutubeMusicSocket { this.onChange({ position: 0, isPlaying: false, song: undefined }); }); - this.socket.addEventListener("message", e => { let message: Message; try { @@ -215,7 +214,7 @@ export const YoutubeMusicStore = proxyLazyWebpack(() => { public openExternal(path: string) { const videoId = path.match(/watch\?v=([\w-]+)/); - const url = Vencord.Plugins.isPluginEnabled("OpenInApp") && videoId + const url = isPluginEnabled("OpenInApp") && videoId ? encodeURI("youtubemusic://openVideo " + videoId[1]) : "https://music.youtube.com" + path; @@ -225,7 +224,6 @@ export const YoutubeMusicStore = proxyLazyWebpack(() => { VencordNative.native.openExternal(url); } - set position(p: number) { this.mPosition = p * 1000; this.start = Date.now(); diff --git a/src/equicordplugins/neverPausePreviews/index.tsx b/src/equicordplugins/neverPausePreviews/index.tsx index 6758e933e..3c237c788 100644 --- a/src/equicordplugins/neverPausePreviews/index.tsx +++ b/src/equicordplugins/neverPausePreviews/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -25,9 +25,9 @@ export default definePlugin({ description: "Prevents in-call/PiP previews (screenshare, streams, etc) from pausing even if the client loses focus", authors: [EquicordDevs.vappstar], settingsAboutComponent: () => ( - + This plugin will cause discord to use more resources than normal - + ), patches: [ { diff --git a/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx b/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx index 9f1c60510..fd8277a02 100644 --- a/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx +++ b/src/equicordplugins/newPluginsManager/NewPluginsModal.tsx @@ -7,7 +7,6 @@ import "./styles.css"; import { Settings, useSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -15,6 +14,7 @@ import { Flex } from "@components/Flex"; import { PluginDependencyList } from "@components/settings/tabs/plugins"; import { PluginCard } from "@components/settings/tabs/plugins/PluginCard"; import { ChangeList } from "@utils/ChangeList"; +import { classNameFactory } from "@utils/css"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { useForceUpdater } from "@utils/react"; import { findComponentByCodeLazy } from "@webpack"; diff --git a/src/equicordplugins/notificationTitle.discordDesktop/index.tsx b/src/equicordplugins/notificationTitle.discordDesktop/index.tsx index 3786357b6..7d6e19bce 100644 --- a/src/equicordplugins/notificationTitle.discordDesktop/index.tsx +++ b/src/equicordplugins/notificationTitle.discordDesktop/index.tsx @@ -17,7 +17,6 @@ const ChannelTypes = findByPropsLazy("DM", "GUILD_TEXT", "PUBLIC_THREAD", "UNKNO const ChannelTypesSets = findByPropsLazy("THREADS", "GUILD_TEXTUAL", "ALL_DMS"); const MessageTypes = findByPropsLazy("REPLY", "STAGE_RAISE_HAND", "CHANNEL_NAME_CHANGE"); - export default definePlugin({ name: "NotificationTitle", description: "Makes desktop notifications more informative", diff --git a/src/equicordplugins/platformSpoofer/index.tsx b/src/equicordplugins/platformSpoofer/index.tsx index 161e6c67a..7817d3dbf 100644 --- a/src/equicordplugins/platformSpoofer/index.tsx +++ b/src/equicordplugins/platformSpoofer/index.tsx @@ -5,7 +5,7 @@ */ import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { UserStore } from "@webpack/common"; @@ -50,9 +50,9 @@ export default definePlugin({ description: "Spoof what platform or device you're on", authors: [EquicordDevs.Drag], settingsAboutComponent: () => ( - + We can't guarantee this plugin won't get you warned or banned. - + ), settings: settings, patches: [ diff --git a/src/equicordplugins/polishWording/index.ts b/src/equicordplugins/polishWording/index.ts index 6334e92ed..57f925901 100644 --- a/src/equicordplugins/polishWording/index.ts +++ b/src/equicordplugins/polishWording/index.ts @@ -275,8 +275,7 @@ function capitalize(textInput: string): string { } } - // We'll fix capitalization of I's - result = result.replace(/\bi[\b']/g, "I"); + result = result.replace(/\bi\b(?!\s+is\b)(?=['\s]|$)/g, "I"); return result; } diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx deleted file mode 100644 index a74bbc5a7..000000000 --- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import "./style.css"; - -import { HeaderBarButton } from "@api/HeaderBar"; -import { showNotification } from "@api/Notifications"; -import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; -import { Devs } from "@utils/constants"; -import { getTheme, Theme } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, GuildChannelStore, NavigationRouter, RestAPI, UserStore } from "@webpack/common"; - -const QuestIcon = findComponentByCodeLazy("10.47a.76.76"); -const ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore"); -const RunningGameStore = findStoreLazy("RunningGameStore"); -const QuestsStore = findByPropsLazy("getQuest"); - -function ToolBarHeader() { - return ( - - ); -} - -async function openCompleteQuestUI() { - const quest = [...QuestsStore.quests.values()].find(x => x.id !== "1412491570820812933" && x.userStatus?.enrolledAt && !x.userStatus?.completedAt && new Date(x.config.expiresAt).getTime() > Date.now()); - - if (!quest) { - showNotification({ - title: "Quest Completer", - body: "No Quests To Complete. Click to navigate to the quests tab", - onClick() { - NavigationRouter.transitionTo("/quest-home"); - }, - }); - } else { - const pid = Math.floor(Math.random() * 30000) + 1000; - const theme = getTheme() === Theme.Light - ? "light" - : "dark"; - - const applicationId = quest.config.application.id; - const applicationName = quest.config.application.name; - const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE", "ACHIEVEMENT_IN_ACTIVITY"].find(x => quest.config.taskConfigV2.tasks[x] != null); - const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; - // @ts-ignore - const secondsNeeded = quest.config.taskConfigV2.tasks[taskName].target; - // @ts-ignore - let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; - if (taskName === "WATCH_VIDEO" || taskName === "WATCH_VIDEO_ON_MOBILE") { - const maxFuture = 10, speed = 7, interval = 1; - const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime(); - const fn = async () => { - while (true) { - const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture; - const diff = maxAllowed - secondsDone; - const timestamp = secondsDone + speed; - if (diff >= speed) { - await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } }); - secondsDone = Math.min(secondsNeeded, timestamp); - } - if (timestamp >= secondsNeeded) { - break; - } - await new Promise(resolve => setTimeout(resolve, interval * 1000)); - } - if (!settings.store.disableNotifications) { - showNotification({ - title: `${applicationName} - Quest Completer`, - body: "Quest Completed.", - icon: icon, - }); - } - }; - fn(); - } else if (taskName === "PLAY_ON_DESKTOP") { - RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => { - const appData = res.body[0]; - const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", ""); - const fakeGame = { - cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, - exeName, - exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, - hidden: false, - isLauncher: false, - id: applicationId, - name: appData.name, - pid: pid, - pidPath: [pid], - processName: appData.name, - start: Date.now(), - }; - const realGames = RunningGameStore.getRunningGames(); - const fakeGames = [fakeGame]; - const realGetRunningGames = RunningGameStore.getRunningGames; - const realGetGameForPID = RunningGameStore.getGameForPID; - RunningGameStore.getRunningGames = () => fakeGames; - RunningGameStore.getGameForPID = pid => fakeGames.find(x => x.pid === pid); - FluxDispatcher.dispatch({ - type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames - }); - - const fn = data => { - const progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value); - - if (progress >= secondsNeeded) { - if (!settings.store.disableNotifications) { - showNotification({ - title: `${applicationName} - Quest Completer`, - body: "Quest Completed.", - icon: icon, - }); - } - - RunningGameStore.getRunningGames = realGetRunningGames; - RunningGameStore.getGameForPID = realGetGameForPID; - FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] }); - FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); - } - }; - FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); - }); - } else if (taskName === "STREAM_ON_DESKTOP") { - const stream = ApplicationStreamingStore.getAnyStreamForUser(UserStore.getCurrentUser()?.id); - if (!stream && !settings.store.disableNotifications) { - showNotification({ - title: "You're not streaming - Quest Completer", - body: `${applicationName} requires you to be streaming.\nPlease stream any window in vc. Make sure 1 other user is watching.`, - icon: icon, - }); - } - const realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata; - ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({ - id: applicationId, - pid, - sourceName: null - }); - - const fn = data => { - const progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value); - if (progress >= secondsNeeded) { - if (!settings.store.disableNotifications) { - showNotification({ - title: `${applicationName} - Quest Completer`, - body: "Quest Completed.", - icon: icon, - }); - } - - ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc; - FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); - } - }; - FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); - } else if (taskName === "PLAY_ACTIVITY") { - const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds() as any[]).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id; - const streamKey = `call:${channelId}:1`; - - const fn = async () => { - - while (true) { - const res = await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: false } }); - const progress = res.body.progress.PLAY_ACTIVITY.value; - - await new Promise(resolve => setTimeout(resolve, 20 * 1000)); - - if (progress >= secondsNeeded) { - await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: true } }); - break; - } - } - if (!settings.store.disableNotifications) { - showNotification({ - title: `${applicationName} - Quest Completer`, - body: "Quest Completed.", - icon: icon, - }); - } - }; - fn(); - } - return; - } -} - -const settings = definePluginSettings({ - disableNotifications: { - type: OptionType.BOOLEAN, - description: "Disable notifications when no quests are available or when a quest is completed - still shows no quests notif", - default: false, - }, -}); - -export default definePlugin({ - name: "QuestCompleter", - description: "Adds a button to the header bar to complete quests without having the game installed.", - authors: [Devs.amia], - settings, - settingsAboutComponent: () => ( - - You must manually accept the quest first before clicking the button. - - ), - headerBarButton: { - icon: QuestIcon, - render: ToolBarHeader - } -}); diff --git a/src/equicordplugins/questCompleter.discordDesktop/style.css b/src/equicordplugins/questCompleter.discordDesktop/style.css deleted file mode 100644 index 22ddb9473..000000000 --- a/src/equicordplugins/questCompleter.discordDesktop/style.css +++ /dev/null @@ -1,11 +0,0 @@ -.vc-quest-completer-icon { - padding: 0; - width: var(--space-32); - min-width: 0; - height: var(--space-32); - color: var(--interactive-icon-default) !important; - - &:hover { - filter: brightness(2); - } -} \ No newline at end of file diff --git a/src/equicordplugins/questFocused/index.tsx b/src/equicordplugins/questFocused/index.tsx deleted file mode 100644 index 695a4f023..000000000 --- a/src/equicordplugins/questFocused/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Alert } from "@components/Alert"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "QuestFocused", - description: "Prevent the quests player from pausing and possibly skip it all together.", - settingsAboutComponent: () => ( - - You might need to spam left mouse button on the video to skip it. - - ), - authors: [EquicordDevs.secp192k1], - patches: [ - // Block pausing - { - find: "[QV] | updatePlayerState | playerState", - replacement: { - match: /(?<=case \i\.\i\.PAUSED:.{0,25})\i\.current\.pause\(\),/, - replace: "" - } - }, - { - find: "[QV] | updatePlayerState | playerState:", - replacement: { - match: /(?<=case \i\.\i\.PLAYING:)\i\.current\.paused/, - replace: "false" - } - }, - ], -}); diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index fb17c4710..38d9cbbe7 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -7,20 +7,24 @@ import "./styles.css"; import { showNotification } from "@api/Notifications"; +import { plugins } from "@api/PluginManager"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; +import { migratePluginToSettings } from "@api/Settings"; import { ErrorBoundary, openPluginModal } from "@components/index"; import { EquicordDevs } from "@utils/constants"; -import definePlugin, { StartAt } from "@utils/types"; +import { copyToClipboard } from "@utils/index"; +import definePlugin, { PluginNative, StartAt } from "@utils/types"; import { onceReady } from "@webpack"; -import { ContextMenuApi, Menu, NavigationRouter, useEffect, useState } from "@webpack/common"; +import { ContextMenuApi, Menu, NavigationRouter, RestAPI, useEffect, useState } from "@webpack/common"; import { JSX } from "react"; import { addIgnoredQuest, addRerenderCallback, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, questIsIgnored, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings"; import { ExcludedQuestMap, GuildlessServerListItem, Quest, QuestIcon, QuestMap, QuestStatus, RGB } from "./utils/components"; -import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, waitUntilEnrolled } from "./utils/misc"; +import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, videoQuestLeeway, waitUntilEnrolled } from "./utils/misc"; +const QuestifyNative = VencordNative.pluginHelpers.Questify as PluginNative; const patchedMobileQuests = new Set(); -export const activeQuestIntervals = new Map(); +export const activeQuestIntervals = new Map(); function questMenuUnignoreAllClicked(): void { validateAndOverwriteIgnoredQuests([]); @@ -84,7 +88,7 @@ export function QuestButton(): JSX.Element { if (todo === "open-quests") { NavigationRouter.transitionTo(questPath); } else if (todo === "plugin-settings") { - openPluginModal(Vencord.Plugins.plugins.Questify); + openPluginModal(plugins.Questify); } else if (todo === "context-menu") { ContextMenuApi.openContextMenu(event, () => ( } + { copyToClipboard(props.quest.id); }} + /> )); } @@ -369,7 +378,7 @@ function makeDesktopCompatible(quests: Quest[]): void { const config = quest.config?.taskConfigV2; const tasks = config?.tasks; - if (tasks?.WATCH_VIDEO_ON_MOBILE && !tasks?.WATCH_VIDEO) { + if (tasks?.WATCH_VIDEO_ON_MOBILE && (!tasks?.WATCH_VIDEO || patchedMobileQuests.has(quest.id))) { patchedMobileQuests.add(quest.id); tasks.WATCH_VIDEO = { @@ -403,6 +412,7 @@ function sortQuests(quests: Quest[], skip?: boolean): Quest[] { expiredSubsort, completeVideoQuestsInBackground, completeGameQuestsInBackground, + completeAchievementQuestsInBackground, triggerQuestsRerender } = settings.use([ "ignoredQuestIDs", @@ -414,6 +424,7 @@ function sortQuests(quests: Quest[], skip?: boolean): Quest[] { "expiredSubsort", "completeVideoQuestsInBackground", "completeGameQuestsInBackground", + "completeAchievementQuestsInBackground", "triggerQuestsRerender" ]); @@ -565,11 +576,18 @@ function shouldPreloadQuestAssets(): boolean { return restyleQuestsPreload; } -async function startVideoProgressTracking(quest: Quest, questDuration: number): Promise { +async function startVideoProgressTracking(quest: Quest, questTarget: number): Promise { const questName = normalizeQuestName(quest.config.messages.questName); const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null; - const initialProgress = Math.floor(((new Date()).getTime() - (questEnrolledAt ?? new Date()).getTime()) / 1000) || 1; // Max up to 10 seconds into the future can be reported. - activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, duration: questDuration, type: "watch" }); + const initialProgress = Math.floor(((new Date()).getTime() - (questEnrolledAt ?? new Date()).getTime()) / 1000) || 1; + activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, type: "watch" }); + // Max up to ~25 seconds into the future can be reported. + const questTargetWithLeeway = questTarget - videoQuestLeeway; + let currentProgress = initialProgress; + let currentProgressScaled = initialProgress; + const timeRemaining = Math.max(0, questTargetWithLeeway - currentProgressScaled); + + QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background in ${timeRemaining} seconds.`); if (!questEnrolledAt) { const enrollmentTimeout = 60000; @@ -583,16 +601,18 @@ async function startVideoProgressTracking(quest: Quest, questDuration: number): } } - let progressIntervalId: NodeJS.Timeout; - let currentProgress = initialProgress; - const timeRemaining = Math.max(0, questDuration - currentProgress); + if (!activeQuestIntervals.has(quest.id)) { + return; + } + const reportEverySec = 10; + let progressIntervalId: NodeJS.Timeout; async function handleSendComplete() { clearInterval(progressIntervalId); clearTimeout(renderIntervalId); + const success = await reportVideoQuestProgress(quest, questTarget, QuestifyLogger); activeQuestIntervals.delete(quest.id); - const success = await reportVideoQuestProgress(quest, questDuration, QuestifyLogger); if (success) { QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`); @@ -615,15 +635,33 @@ async function startVideoProgressTracking(quest: Quest, questDuration: number): await handleSendComplete(); }, timeRemaining * 1000); } else { + const simulatedProgressToCover = questTarget - currentProgressScaled; + const speedFactor = simulatedProgressToCover / timeRemaining; + const progressIncrementPerInterval = reportEverySec * speedFactor; + const numberOfIntervals = Math.floor(timeRemaining / reportEverySec); + let intervalsRun = 0; + reportVideoQuestProgress(quest, initialProgress, QuestifyLogger); progressIntervalId = setInterval(async () => { + intervalsRun++; currentProgress += reportEverySec; + currentProgressScaled += progressIncrementPerInterval; + const progressToReport = Math.min(Math.floor(currentProgressScaled), questTarget); - if (currentProgress >= questDuration - 10) { - await handleSendComplete(); - } else { - await reportVideoQuestProgress(quest, currentProgress, QuestifyLogger); + if (intervalsRun < numberOfIntervals || (currentProgress < questTarget - videoQuestLeeway - (reportEverySec / 2))) { + await reportVideoQuestProgress(quest, progressToReport, QuestifyLogger); + } + + if (intervalsRun >= numberOfIntervals) { + clearInterval(progressIntervalId); + + const timeSpentInIntervals = numberOfIntervals * reportEverySec; + const finalWaitTime = Math.max(0, timeRemaining - timeSpentInIntervals); + progressIntervalId = setTimeout(handleSendComplete, finalWaitTime * 1000); + + const intervalData = activeQuestIntervals.get(quest.id); + if (intervalData) intervalData.progressTimeout = progressIntervalId; } }, reportEverySec * 1000); } @@ -640,21 +678,24 @@ async function startVideoProgressTracking(quest: Quest, questDuration: number): rerenderQuests(); }, 1000); - activeQuestIntervals.set(quest.id, { progressTimeout: progressIntervalId, rerenderTimeout: renderIntervalId, progress: initialProgress, duration: questDuration, type: "watch" }); + const intervalData = activeQuestIntervals.get(quest.id); - if (timeRemaining > 0) { - QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background in ${timeRemaining} seconds.`); + if (intervalData) { + intervalData.progressTimeout = progressIntervalId; + intervalData.rerenderTimeout = renderIntervalId; } } -async function startPlayGameProgressTracking(quest: Quest, questDuration: number): Promise { +async function startPlayGameProgressTracking(quest: Quest, questTarget: number): Promise { const questName = normalizeQuestName(quest.config.messages.questName); const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null; const playType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY; const initialProgress = quest.userStatus?.progress?.[playType?.type || ""]?.value || 0; - const remaining = Math.max(0, questDuration - initialProgress); + const remaining = Math.max(0, questTarget - initialProgress); const heartbeatInterval = 20; // Heartbeats must be at most 2 minutes apart. - activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, duration: questDuration, type: "play" }); + activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, type: "play" }); + + QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background in ${remaining} seconds.`); if (!questEnrolledAt) { const enrollmentTimeout = 60000; @@ -668,6 +709,10 @@ async function startPlayGameProgressTracking(quest: Quest, questDuration: number } } + if (!activeQuestIntervals.has(quest.id)) { + return; + } + const initial = await reportPlayGameQuestProgress(quest, false, QuestifyLogger, { attempts: 3, delay: 2500 }); const progressIntervalId = setInterval(async () => { @@ -680,16 +725,16 @@ async function startPlayGameProgressTracking(quest: Quest, questDuration: number return; } - const isComplete = result.progress >= questDuration; - const timeRemaining = questDuration - result.progress; + const isComplete = result.progress >= questTarget; + const timeRemaining = questTarget - result.progress; const intervalData = activeQuestIntervals.get(quest.id); intervalData && (intervalData.progress = result.progress); if (isComplete) { clearInterval(progressIntervalId); clearTimeout(renderIntervalId); - activeQuestIntervals.delete(quest.id); const success = await reportPlayGameQuestProgress(quest, true, QuestifyLogger, { attempts: 3, delay: 2500 }); + activeQuestIntervals.delete(quest.id); if (success) { QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`); @@ -710,8 +755,8 @@ async function startPlayGameProgressTracking(quest: Quest, questDuration: number clearTimeout(renderIntervalId); setTimeout(async () => { - activeQuestIntervals.delete(quest.id); const success = await reportPlayGameQuestProgress(quest, true, QuestifyLogger, { attempts: 3, delay: 2500 }); + activeQuestIntervals.delete(quest.id); if (success) { QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`); @@ -743,36 +788,123 @@ async function startPlayGameProgressTracking(quest: Quest, questDuration: number rerenderQuests(); }, 1000); - activeQuestIntervals.set(quest.id, { progressTimeout: progressIntervalId, rerenderTimeout: renderIntervalId, progress: initial.progress || initialProgress, duration: questDuration, type: "play" }); + const intervalData = activeQuestIntervals.get(quest.id); - if (remaining > 0) { - QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background in ${remaining} seconds.`); + if (intervalData) { + intervalData.progress = initial.progress || initialProgress; + intervalData.progressTimeout = progressIntervalId; + intervalData.rerenderTimeout = renderIntervalId; + } +} + +async function startAchievementActivityProgressTracking(quest: Quest, questTarget: number): Promise { + const questName = normalizeQuestName(quest.config.messages.questName); + const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null; + const achievementType = quest.config.taskConfigV2?.tasks.ACHIEVEMENT_IN_ACTIVITY; + activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: 0, type: "achievement" }); + + QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background.`); + + if (!questEnrolledAt) { + const enrollmentTimeout = 60000; + const enrolled = await waitUntilEnrolled(quest, enrollmentTimeout, 15, QuestifyLogger); + quest = refreshQuest(quest); + + if (!enrolled) { + QuestifyLogger.warn(`[${getFormattedNow()}] Quest ${questName} not enrolled within ${enrollmentTimeout / 1000} seconds.`); + activeQuestIntervals.delete(quest.id); + return; + } + } + + if (!activeQuestIntervals.has(quest.id)) { + return; + } + + const appID = achievementType?.applications?.[0].id; + + if (!appID) { + QuestifyLogger.warn(`[${getFormattedNow()}] Could not find application ID for Quest ${questName}.`); + activeQuestIntervals.delete(quest.id); + return; + } + + let authCode: string | null = null; + + try { + const response = await RestAPI.post({ + url: `/oauth2/authorize?client_id=${appID}&response_type=code&scope=identify%20applications.entitlements&state=`, + body: { authorize: true } + }); + + const location = response?.body?.location; + + if (location) { + authCode = new URL(location).searchParams.get("code"); + } + + if (!authCode) { + QuestifyLogger.warn(`[${getFormattedNow()}] Auth code not found for Quest ${questName}.`); + activeQuestIntervals.delete(quest.id); + return; + } + } catch (error) { + QuestifyLogger.error(`[${getFormattedNow()}] Failed to retrieve auth code for Quest ${questName}:`, error); + activeQuestIntervals.delete(quest.id); + return; + } + + const result = await QuestifyNative.complete(appID, authCode!, questTarget); + activeQuestIntervals.delete(quest.id); + + if (!result.success) { + const errorReason = result.error || "An error occurred while completing the Quest."; + QuestifyLogger.error(`[${getFormattedNow()}] Failed to complete Quest ${questName}:`, errorReason); + return; + } else { + QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`); + + if (settings.store.notifyOnQuestComplete) { + showNotification({ + title: "Quest Completed!", + body: `The ${questName} Quest has completed.`, + dismissOnClick: true, + onClick: () => NavigationRouter.transitionTo(`${questPath}#${quest.id}`), + }); + } } } function processQuestForAutoComplete(quest: Quest): boolean { const questName = normalizeQuestName(quest.config.messages.questName); - const { completeGameQuestsInBackground, completeVideoQuestsInBackground } = settings.store; + const { completeVideoQuestsInBackground, completeGameQuestsInBackground, completeAchievementQuestsInBackground, makeMobileQuestsDesktopCompatible } = settings.store; const playType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY; - const watchType = quest.config.taskConfigV2?.tasks.WATCH_VIDEO || quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE; - const questDuration = playType?.target || watchType?.target || 0; + const watchType = (makeMobileQuestsDesktopCompatible && quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE) || quest.config.taskConfigV2?.tasks.WATCH_VIDEO; + const achievementType = quest.config.taskConfigV2?.tasks.ACHIEVEMENT_IN_ACTIVITY; + const questTarget = playType?.target || watchType?.target || achievementType?.target || 0; const existingInterval = activeQuestIntervals.get(quest.id); + const watchTypeIncompatible = watchType && !completeVideoQuestsInBackground; + const playTypeIncompatible = playType && (!completeGameQuestsInBackground || !IS_DISCORD_DESKTOP); + const achievementTypeIncompatible = achievementType && (!completeAchievementQuestsInBackground || !IS_DISCORD_DESKTOP); if (quest.userStatus?.completedAt || existingInterval) { return false; - } else if (!playType && !watchType) { + } else if (!playType && !watchType && !achievementType) { QuestifyLogger.warn(`[${getFormattedNow()}] Could not recognize the Quest type for ${questName}.`); return false; - } else if ((watchType && !completeVideoQuestsInBackground) || (playType && (!completeGameQuestsInBackground || !IS_DISCORD_DESKTOP))) { + } else if (watchTypeIncompatible || playTypeIncompatible || achievementTypeIncompatible) { return false; - } else if (!questDuration) { - QuestifyLogger.warn(`[${getFormattedNow()}] Could not find duration for Quest ${questName}.`); + } else if (!questTarget) { + QuestifyLogger.warn(`[${getFormattedNow()}] Could not find target for Quest ${questName}.`); return false; } else if (watchType) { - startVideoProgressTracking(quest, questDuration); + startVideoProgressTracking(quest, questTarget); return true; } else if (playType) { - startPlayGameProgressTracking(quest, questDuration); + startPlayGameProgressTracking(quest, questTarget); + return true; + } else if (achievementType) { + startAchievementActivityProgressTracking(quest, questTarget); return true; } @@ -785,9 +917,7 @@ function shouldDisableQuestAcceptedButton(quest: Quest): boolean | null { if (activeQuestIntervals.has(quest.id)) { return true; } else if (completeGameQuestsInBackground) { - if (!IS_DISCORD_DESKTOP) { - return true; - } else { + if (IS_DISCORD_DESKTOP) { return false; } } @@ -795,23 +925,54 @@ function shouldDisableQuestAcceptedButton(quest: Quest): boolean | null { return null; } +function getQuestUnacceptedButtonText(quest: Quest): string | null { + const { completeGameQuestsInBackground, completeVideoQuestsInBackground, completeAchievementQuestsInBackground, makeMobileQuestsDesktopCompatible } = settings.store; + const playType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY; + const watchType = (makeMobileQuestsDesktopCompatible && quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE) || quest.config.taskConfigV2?.tasks.WATCH_VIDEO; + const achievementType = quest.config.taskConfigV2?.tasks.ACHIEVEMENT_IN_ACTIVITY; + const target = (playType?.target || (watchType?.target ? watchType.target - videoQuestLeeway : undefined) || achievementType?.target || 0); + const targetFormatted = `${String(Math.floor(target / 60)).padStart(2, "0")}:${String(target % 60).padStart(2, "0")}`; + + if (target > 0) { + if ((playType && completeGameQuestsInBackground && IS_DISCORD_DESKTOP) || (watchType && completeVideoQuestsInBackground)) { + return `Complete (${targetFormatted})`; + } else if (achievementType && completeAchievementQuestsInBackground) { + return "Complete (Immediate)"; + } + } + + return null; +} + function getQuestAcceptedButtonText(quest: Quest): string | null { - const { completeGameQuestsInBackground, completeVideoQuestsInBackground } = settings.store; + const { completeGameQuestsInBackground, completeVideoQuestsInBackground, completeAchievementQuestsInBackground, makeMobileQuestsDesktopCompatible } = settings.store; const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null; const playType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY; - const watchType = quest.config.taskConfigV2?.tasks.WATCH_VIDEO || quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE; - const taskType = playType || watchType; - const duration = taskType?.target || 0; + const watchType = (makeMobileQuestsDesktopCompatible && quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE) || quest.config.taskConfigV2?.tasks.WATCH_VIDEO; + const achievementType = quest.config.taskConfigV2?.tasks.ACHIEVEMENT_IN_ACTIVITY; const intervalData = activeQuestIntervals.get(quest.id); - const progress = Math.min((intervalData?.progress ?? (quest.userStatus?.progress?.[taskType?.type || ""]?.value || 0)), duration); - const timeRemaining = Math.max(0, duration - progress); - const progressFormatted = `${String(Math.floor(timeRemaining / 60)).padStart(2, "0")}:${String(timeRemaining % 60).padStart(2, "0")}`; - if (questEnrolledAt && ((playType && completeGameQuestsInBackground) || (watchType && completeVideoQuestsInBackground))) { - if (!!intervalData) { - return timeRemaining ? `Completing (${progressFormatted})` : "Completing..."; - } else if (watchType || (playType && IS_DISCORD_DESKTOP)) { - return `Resume (~${progressFormatted})`; + if (questEnrolledAt) { + if (((playType && completeGameQuestsInBackground && IS_DISCORD_DESKTOP) || (watchType && completeVideoQuestsInBackground))) { + const taskType = playType || watchType; + const duration = taskType?.target || 0; + const durationWithLeeway = watchType ? duration - videoQuestLeeway : duration; + const progress = Math.min((intervalData?.progress ?? (quest.userStatus?.progress?.[taskType?.type || ""]?.value || 0)), durationWithLeeway); + const timeRemaining = Math.max(0, durationWithLeeway - progress); + const canCompleteImmediately = watchType && questEnrolledAt && ((new Date().getTime() - questEnrolledAt.getTime()) / 1000) >= durationWithLeeway; + const progressFormatted = `${String(Math.floor(timeRemaining / 60)).padStart(2, "0")}:${String(timeRemaining % 60).padStart(2, "0")}`; + + if (!!intervalData) { + return !canCompleteImmediately && timeRemaining ? `Completing (${progressFormatted})` : "Completing..."; + } else if (watchType || playType) { + return canCompleteImmediately ? "Complete (Immediate)" : timeRemaining === durationWithLeeway ? `Complete (${progressFormatted})` : `Resume (${progressFormatted})`; + } + } else if (achievementType && completeAchievementQuestsInBackground) { + if (!!intervalData) { + return "Completing..."; + } else { + return "Complete (Immediate)"; + } } } @@ -819,6 +980,7 @@ function getQuestAcceptedButtonText(quest: Quest): string | null { } function getQuestPanelOverride(): Quest | null { + const { makeMobileQuestsDesktopCompatible } = settings.store; let closestQuest: Quest | null = null; let closestTimeRemaining = Infinity; @@ -829,7 +991,16 @@ function getQuestPanelOverride(): Quest | null { return; } - const timeRemaining = interval.duration - interval.progress; + const playType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY; + const watchType = (makeMobileQuestsDesktopCompatible && quest.config.taskConfigV2?.tasks.WATCH_VIDEO_ON_MOBILE) || quest.config.taskConfigV2?.tasks.WATCH_VIDEO; + const achievementType = quest.config.taskConfigV2?.tasks.ACHIEVEMENT_IN_ACTIVITY; + const duration = playType?.target || (watchType?.target ? watchType.target - videoQuestLeeway : undefined) || achievementType?.target || 0; + + if (!duration) { + return; + } + + const timeRemaining = duration - interval.progress; // 3 second buffer to account for per-second rerendering // which could cause flickering if multiple Quests were @@ -903,10 +1074,15 @@ function getQuestAcceptedButtonProps(quest: Quest, text: string, disabled: boole "PLAY_ON_DESKTOP", "PLAY_ON_XBOX", "PLAY_ON_PLAYSTATION", - "PLAY_ACTIVITY" + "PLAY_ACTIVITY", + "ACHIEVEMENT_IN_ACTIVITY" ]; - if (!Array.from(validTasks).some(taskType => Object.values(quest.config.taskConfigV2?.tasks || {}).some(task => task.type === taskType))) { + const validTask = Array.from(validTasks).some(taskType => Object.values(quest.config.taskConfigV2?.tasks || {}).some( + task => task.type === taskType && (task.type !== "WATCH_VIDEO_ON_MOBILE" || settings.store.makeMobileQuestsDesktopCompatible) + )); + + if (!validTask) { return { disabled: disabled, text: text, @@ -916,13 +1092,16 @@ function getQuestAcceptedButtonProps(quest: Quest, text: string, disabled: boole } return { - disabled: shouldDisableQuestAcceptedButton(quest) ?? true, + disabled: shouldDisableQuestAcceptedButton(quest) ?? disabled, text: getQuestAcceptedButtonText(quest) ?? text, - onClick: () => { processQuestForAutoComplete(quest); }, + onClick: () => { const startingAutocomplete = processQuestForAutoComplete(quest); !startingAutocomplete && onClick ? onClick() : null; }, icon: () => { } }; } +// Drop support for QuestCompleter and migrate to Questify settings. +migratePluginToSettings(true, "Questify", "QuestCompleter", "completeVideoQuestsInBackground", "completeGameQuestsInBackground", "completeAchievementQuestsInBackground"); + export default definePlugin({ name: "Questify", description: "Enhance your Quest experience with a suite of features, or disable them entirely if they're not your thing.", @@ -946,6 +1125,7 @@ export default definePlugin({ shouldHideGiftInventoryRelocationNotice, shouldHideFriendsListActiveNowPromotion, shouldHideMembersListActivelyPlayingIcon, + getQuestUnacceptedButtonText, processQuestForAutoComplete, getQuestAcceptedButtonProps, getQuestAcceptedButtonText, @@ -1005,27 +1185,11 @@ export default definePlugin({ { // Hides the Quest icon from members list items when // a user is playing a game tied to an active Quest. - find: "HANG_STATUS});", + find: "),\"activity-\".concat", group: true, replacement: [ { - match: /(?=if\(\(0,.{0,30}?isBlockedOrIgnored)/, - replace: "const shouldHideMembersListActivelyPlayingIcon=$self.shouldHideMembersListActivelyPlayingIcon();" - }, - { - match: /(?<=\i\(\),\i&&)/, - replace: "!shouldHideMembersListActivelyPlayingIcon&&" - } - ] - }, - { - // Same as above, probably? Not sure when - // each function is used, so patching both. - find: '"StackedActivityStatus"})', - group: true, - replacement: [ - { - match: /(?=if\(\i\)return null;let \i=function)/, + match: /(?<=voiceActivityChannel:\i\?\i:null}\);)/, replace: "const shouldHideMembersListActivelyPlayingIcon=$self.shouldHideMembersListActivelyPlayingIcon();" }, { @@ -1270,18 +1434,19 @@ export default definePlugin({ group: true, replacement: [ { - // Resume Video Quest + // Resume Video Quest. match: /(onClick:\(\)=>)(\(0,\i.openVideoQuestModal\)\({quest:(\i))/, replace: "$1!$self.processQuestForAutoComplete($3)&&$2" }, { - // Start Play Game Quests. + // Start Play Game and Play Activity Quests. // Video Quests are handled in the next patch group. - match: /(?<=onClick:async\(\)=>{)/, - replace: "const startingAutoComplete=arguments[0].isVideoQuest?false:!$self.processQuestForAutoComplete(arguments[0].quest);" + // Also set the unaccepted button text to "Complete". + match: /(\i,tooltipText:null,onClick:async\(\)=>{)/, + replace: "$self.getQuestUnacceptedButtonText(arguments[0].quest)??$1const startingAutoComplete=arguments[0].isVideoQuest?false:$self.processQuestForAutoComplete(arguments[0].quest);" }, { - // The "Resume (XX:XX)" text is changed to "Watching (XX:XX)" if the Quest is active. + // Set the accepted button text to "Complete", "Completing", or "Resume" based on progress. match: /(if\(\i\)return{text:)/, replace: "$1$self.getQuestAcceptedButtonText(arguments[0].quest)??", }, @@ -1296,7 +1461,7 @@ export default definePlugin({ replace: "$1questRerenderTrigger," }, { - // Stop Play Activity Quests from launching the activity. + // Stop Play Activity Quests from launching the activity on first click. match: /(?<=,)(\i\(\))(\)}};)/, replace: "!startingAutoComplete&&$1$2" } diff --git a/src/equicordplugins/questify/native.ts b/src/equicordplugins/questify/native.ts new file mode 100644 index 000000000..93eed6818 --- /dev/null +++ b/src/equicordplugins/questify/native.ts @@ -0,0 +1,50 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IpcMainInvokeEvent } from "electron"; + +export async function complete(_: IpcMainInvokeEvent, appID: string, authCode: string, questTarget: number): Promise<{ success: boolean; error: string | null; }> { + const result = await authorize(appID, authCode); + + if (result.error || !result.token) { + return { success: false, error: JSON.stringify(result.error) }; + } + + const success = await progress(appID, result.token, questTarget); + + if (success.error || !success.success) { + return { success: false, error: JSON.stringify(success.error) }; + } + + return { success: true, error: null }; +} + +async function authorize(appID: string, authCode: string): Promise<{ token: string | false; error: any; }> { + let error = null; + + const token = await fetch(`https://${appID}.discordsays.com/.proxy/acf/authorize`, { + body: JSON.stringify({ code: authCode }), + method: "POST", + mode: "cors", + credentials: "include" + }).then(res => res.json()).then(data => data.token).catch(e => { error = e; return ""; }); + + return { token, error }; +} + +async function progress(appID: string, token: string, questTarget: number): Promise<{ success: boolean; error: any; }> { + let error = null; + + const success = await fetch(`https://${appID}.discordsays.com/.proxy/acf/quest/progress`, { + headers: { "x-auth-token": token }, + body: JSON.stringify({ progress: questTarget }), + method: "POST", + mode: "cors", + credentials: "include" + }).then(res => res.ok).catch(e => { error = e; return false; }); + + return { success, error }; +} diff --git a/src/equicordplugins/questify/settings.tsx b/src/equicordplugins/questify/settings.tsx index 9cc5ea498..06a4666aa 100644 --- a/src/equicordplugins/questify/settings.tsx +++ b/src/equicordplugins/questify/settings.tsx @@ -684,6 +684,7 @@ function DisableQuestsSetting(): JSX.Element { makeMobileQuestsDesktopCompatible, completeVideoQuestsInBackground, completeGameQuestsInBackground, + completeAchievementQuestsInBackground, notifyOnQuestComplete } = settings.use([ "disableQuestsEverything", @@ -699,7 +700,8 @@ function DisableQuestsSetting(): JSX.Element { "makeMobileQuestsDesktopCompatible", "completeVideoQuestsInBackground", "completeGameQuestsInBackground", - "notifyOnQuestComplete" + "completeAchievementQuestsInBackground", + "notifyOnQuestComplete", ]); const options: DynamicDropdownSettingOption[] = [ @@ -713,9 +715,10 @@ function DisableQuestsSetting(): JSX.Element { { label: "Disable Sponsored Banner on Quests Page", value: "sponsored-banner", selected: disableQuestsPageSponsoredBanner, type: "disable" }, { label: "Disable Friends List Active Now Promotion", value: "friends-list", selected: disableFriendsListActiveNowPromotion, type: "disable" }, { label: "Disable Members List Actively Playing Icon", value: "members-list", selected: disableMembersListActivelyPlayingIcon, type: "disable" }, + { label: "Complete Watch Video Quests in Background", value: "video-quests-background", selected: completeVideoQuestsInBackground, type: "modification" }, + { label: "Complete Play Game/Activity Quests in Background", value: "game-quests-background", selected: completeGameQuestsInBackground, type: "modification" }, + { label: "Complete Task in Activity Quests in Background", value: "achievement-quests-background", selected: completeAchievementQuestsInBackground, type: "modification" }, { label: "Make Mobile Quests Desktop Compatible", value: "mobile-desktop-compatible", selected: makeMobileQuestsDesktopCompatible, type: "modification" }, - { label: "Complete Game Quests in Background", value: "game-quests-background", selected: completeGameQuestsInBackground, type: "modification" }, - { label: "Complete Video Quests in Background", value: "video-quests-background", selected: completeVideoQuestsInBackground, type: "modification" }, { label: "Notify on Auto-Complete", value: "notify-on-complete", selected: notifyOnQuestComplete, type: "modification" }, ]; @@ -747,8 +750,9 @@ function DisableQuestsSetting(): JSX.Element { settings.store.disableFriendsListActiveNowPromotion = enabledValues.includes("friends-list"); settings.store.disableMembersListActivelyPlayingIcon = enabledValues.includes("members-list"); settings.store.makeMobileQuestsDesktopCompatible = enabledValues.includes("mobile-desktop-compatible"); - settings.store.completeGameQuestsInBackground = enabledValues.includes("game-quests-background"); settings.store.completeVideoQuestsInBackground = enabledValues.includes("video-quests-background"); + settings.store.completeGameQuestsInBackground = enabledValues.includes("game-quests-background"); + settings.store.completeAchievementQuestsInBackground = enabledValues.includes("achievement-quests-background"); settings.store.notifyOnQuestComplete = enabledValues.includes("notify-on-complete"); redoAutoFetch ? checkAutoFetchInterval(settings.store.fetchingQuestsInterval) : null; @@ -806,15 +810,19 @@ function DisableQuestsSetting(): JSX.Element { The Complete Video Quests in Background option will wait for the duration of the Video Quest and mark it as completed automatically.

- Similarly, the Complete Play Game Quests in Background option + Similarly, the Complete Play Game/Activity Quests in Background option will wait for the duration of the Game Quest and mark it as completed automatically. This option is only supported on the official desktop client.

- You still must start the Quests manually. The first click will start the Quests in the background. - For Video Quests, subsequent clicks will open the video modal as normal. To abort the Quests, you - can open the context menu on the Quest tile and select Stop Auto-Complete. + The Complete Task in Activity Quests in Background option + will immediately mark Task in Activity Quests as completed when started.

- Using either of those options is against Discord's TOS. Use at your own risk. + You still must start the Quests manually. The first click will start the Quests in the background. + For Video Quests, subsequent clicks will open the video modal as normal. For Activity Quests, subsequent clicks + will open the activity as normal. To abort the Quests, you can open the context menu on the Quest tile and + select Stop Auto-Complete. +

+ Using any of the Auto-Complete options is against Discord's TOS. Use at your own risk. { + if (!value) { + activeQuestIntervals.forEach((interval, questId) => { + if (interval.type === "achievement") { + clearTimeout(interval.progressTimeout); + clearTimeout(interval.rerenderTimeout); + activeQuestIntervals.delete(questId); + } + }); + } + + rerenderQuests(); + } + }, notifyOnQuestComplete: { type: OptionType.BOOLEAN, description: "Show a notification when a Quest is auto-completed.", diff --git a/src/equicordplugins/questify/utils/components.tsx b/src/equicordplugins/questify/utils/components.tsx index ff61c28ef..e885807a4 100644 --- a/src/equicordplugins/questify/utils/components.tsx +++ b/src/equicordplugins/questify/utils/components.tsx @@ -116,6 +116,14 @@ export interface Quest { type: "PLAY_ACTIVITY"; target: number; }; + ACHIEVEMENT_IN_ACTIVITY?: { + type: "ACHIEVEMENT_IN_ACTIVITY"; + target: number; + eventName: "progress"; + applications: { + id: string; + }[]; + }; }; }; }, diff --git a/src/equicordplugins/questify/utils/misc.tsx b/src/equicordplugins/questify/utils/misc.tsx index a1ac6701a..17a0811fd 100644 --- a/src/equicordplugins/questify/utils/misc.tsx +++ b/src/equicordplugins/questify/utils/misc.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { questIsIgnored, settings } from "@equicordplugins/questify/settings"; +import { classNameFactory } from "@utils/css"; import { Logger } from "@utils/Logger"; import { findByPropsLazy } from "@webpack"; import { FluxDispatcher, RestAPI, UserStore } from "@webpack/common"; @@ -16,6 +16,7 @@ export const q = classNameFactory("questify-"); export const QuestifyLogger = new Logger("Questify"); export const QuestsStore = findByPropsLazy("getQuest"); export const questPath = "/quest-home"; +export const videoQuestLeeway = 24; export const leftClick = 0; export const middleClick = 1; export const rightClick = 2; diff --git a/src/equicordplugins/questionMarkReplacement/index.tsx b/src/equicordplugins/questionMarkReplacement/index.tsx index 928dd5495..1c9acf14f 100644 --- a/src/equicordplugins/questionMarkReplacement/index.tsx +++ b/src/equicordplugins/questionMarkReplacement/index.tsx @@ -17,7 +17,6 @@ const settings = definePluginSettings({ }, }); - function replaceQuestionMarks(content: string): string { const allQuestionMarks = content.split("").every(char => char === "?"); diff --git a/src/equicordplugins/randomVoice/index.tsx b/src/equicordplugins/randomVoice/index.tsx index 9320fed87..a3d385216 100644 --- a/src/equicordplugins/randomVoice/index.tsx +++ b/src/equicordplugins/randomVoice/index.tsx @@ -22,11 +22,6 @@ const valueOperation = [ { label: "Equal to", value: "==", default: true }, ]; -const CONNECT = 1n << 20n; -const SPEAK = 1n << 21n; -const STREAM = 1n << 9n; -const VIDEO = 1 << 21; - const settings = definePluginSettings({ UserAmountOperation: { description: "Select an operation for the amounts of users", @@ -245,7 +240,6 @@ function ContextMenu() { aria-label="Voice state modifier" > - ) { // Don't run when typing/editing message @@ -43,7 +43,21 @@ export default definePlugin({ }; const magnetLink = matchedContent[0]; - const filename = (new URL(magnetLink)).searchParams.get("dn") ?? "unknown filename"; + let filename = "unknown filename"; + + try { + const searchPart = magnetLink.split("?")[1]; + if (searchPart) { + const params = new URLSearchParams(searchPart); + const dn = params.get("dn"); + if (dn) { + filename = decodeURIComponent(dn.replace(/\+/g, " ")); + } + } + } catch (err) { + console.error("Failed to parse magnet link", err); + } + return { type: "magnet", filename, magnetLink }; }, react({ magnetLink, filename }) { @@ -56,4 +70,3 @@ export default definePlugin({ } }) }); - diff --git a/src/equicordplugins/rpcStats/index.tsx b/src/equicordplugins/rpcStats/index.tsx index c81b557b1..76f07a44a 100644 --- a/src/equicordplugins/rpcStats/index.tsx +++ b/src/equicordplugins/rpcStats/index.tsx @@ -16,7 +16,6 @@ export async function getApplicationAsset(key: string): Promise { return (await ApplicationAssetUtils.fetchAssetIds("0", [key]))[0]; } - enum StatsDisplay { messagesSentToday, messagesSentAllTime, diff --git a/src/equicordplugins/saveFavoriteGIFs/index.tsx b/src/equicordplugins/saveFavoriteGIFs/index.tsx index e0cd01685..e5af23613 100644 --- a/src/equicordplugins/saveFavoriteGIFs/index.tsx +++ b/src/equicordplugins/saveFavoriteGIFs/index.tsx @@ -4,17 +4,17 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { ApplicationCommandInputType } from "@api/Commands"; import { showNotification } from "@api/Notifications"; +import { isPluginEnabled } from "@api/PluginManager"; +import { definePluginSettings } from "@api/Settings"; +import equicordToolbox from "@equicordplugins/equicordToolbox"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { saveFile } from "@utils/web"; -import { UserSettingsActionCreators } from "@webpack/common"; - -async function saveGifs() { - const filename = `favorite-gifs-${new Date().toISOString().split("T")[0]}.txt`; - const gifUrls = Object.keys(UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue().favoriteGifs.gifs); - const content = gifUrls.join("\n"); +import { Menu, UserSettingsActionCreators } from "@webpack/common"; +async function saveContentToFile(content: string, filename: string) { try { if (IS_DISCORD_DESKTOP) { const data = new TextEncoder().encode(content); @@ -27,23 +27,120 @@ async function saveGifs() { showNotification({ title: "Save Favorite GIFs", body: `Saved GIFs successfully as ${filename}`, + color: "var(--text-positive)", }); } catch (error) { + console.error(error); showNotification({ title: "Save Favorite GIFs", body: "Failed to save GIFs", + color: "var(--text-danger)", }); } } -export default definePlugin({ - name: "SaveFavoriteGIFs", - description: "Save favorite GIF urls to a file", - authors: [Devs.thororen], - dependencies: ["EquicordToolbox"], - toolboxActions: { - "Save Favorite GIFs": () => { - saveGifs(); +function getGifUrls(): string[] { + return Object.keys(UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue().favoriteGifs.gifs); +} + +async function saveAllGifs() { + const filename = `favorite-gifs-${new Date().toISOString().split("T")[0]}.txt`; + const gifUrls = getGifUrls(); + + if (gifUrls.length === 0) { + showNotification({ title: "Save Favorite GIFs", body: "No favorite GIFs found..?" }); + return; + } + + const content = gifUrls.join("\n"); + await saveContentToFile(content, filename); +} + +async function saveWorkingGifs() { + const gifUrls = getGifUrls(); + + if (gifUrls.length === 0) { + showNotification({ title: "Save Favorite GIFs", body: "No favorite GIFs found?" }); + return; + } + + showNotification({ + title: "Save Favorite GIFs", + body: `Testing ${gifUrls.length} GIFs.. This may take a moment...`, + }); + + const workingUrls: string[] = []; + + await Promise.all(gifUrls.map(async url => { + try { + const response = await fetch(url, { method: "HEAD" }); + if (response.ok) workingUrls.push(url); + } catch (e) { + try { + const response = await fetch(url); + if (response.ok) workingUrls.push(url); + } catch (err) { } + } + })); + + if (workingUrls.length === 0) { + showNotification({ title: "Save Favorite GIFs", body: "None of your saved GIFs appear to be working." }); + return; + } + + const filename = `working-gifs-${new Date().toISOString().split("T")[0]}.txt`; + const content = workingUrls.join("\n"); + + await saveContentToFile(content, filename); + + showNotification({ + title: "Save Favorite GIFs", + body: `Filtered ${gifUrls.length - workingUrls.length} possibly broken GIFs. Saved ${workingUrls.length} working GIFs.`, + color: "var(--text-positive)", + }); +} + +const settings = definePluginSettings({ + showToolboxButton: { + description: "Show 'Save Favorite GIFs' button in Equicord Toolbox (Requires Reload)", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true, + get hidden() { + return !isPluginEnabled(equicordToolbox.name); } } }); + +export default definePlugin({ + name: "SaveFavoriteGIFs", + description: "Export favorited GIF urls", + authors: [Devs.thororen], + settings, + commands: [ + { + name: "savegifs", + description: "Save all favorite GIF urls to a text file", + inputType: ApplicationCommandInputType.BUILT_IN, + execute: saveAllGifs + }, + { + name: "saveworkinggifs", + description: "Test all favorite GIFs and only save the ones that are still working", + inputType: ApplicationCommandInputType.BUILT_IN, + execute: saveWorkingGifs + } + ], + toolboxActions() { + const { showToolboxButton } = settings.use(["showToolboxButton"]); + if (!showToolboxButton) return null; + + return ( + + ); + } +}); diff --git a/src/equicordplugins/scheduledMessages/components/MessageAccessory.tsx b/src/equicordplugins/scheduledMessages/components/MessageAccessory.tsx index fe69de80c..0bf5a14f2 100644 --- a/src/equicordplugins/scheduledMessages/components/MessageAccessory.tsx +++ b/src/equicordplugins/scheduledMessages/components/MessageAccessory.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { ExtendedMessage } from "../types"; import { phantomMessageMap } from "../utils"; diff --git a/src/equicordplugins/scheduledMessages/components/ScheduleTimeModal.tsx b/src/equicordplugins/scheduledMessages/components/ScheduleTimeModal.tsx index 4b540e0fa..95f4348af 100644 --- a/src/equicordplugins/scheduledMessages/components/ScheduleTimeModal.tsx +++ b/src/equicordplugins/scheduledMessages/components/ScheduleTimeModal.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; import { Heading } from "@components/Heading"; +import { classNameFactory } from "@utils/css"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { findByPropsLazy } from "@webpack"; import { ChannelStore, DraftType, showToast, TextInput, Toasts, UploadManager, useState } from "@webpack/common"; diff --git a/src/equicordplugins/scheduledMessages/components/ViewScheduledModal.tsx b/src/equicordplugins/scheduledMessages/components/ViewScheduledModal.tsx index eb6204967..1c41e9dbe 100644 --- a/src/equicordplugins/scheduledMessages/components/ViewScheduledModal.tsx +++ b/src/equicordplugins/scheduledMessages/components/ViewScheduledModal.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; import { Heading } from "@components/Heading"; +import { classNameFactory } from "@utils/css"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; import { ChannelStore, showToast, Toasts, useState } from "@webpack/common"; diff --git a/src/equicordplugins/scheduledMessages/index.tsx b/src/equicordplugins/scheduledMessages/index.tsx index 640170b80..5f800dda7 100644 --- a/src/equicordplugins/scheduledMessages/index.tsx +++ b/src/equicordplugins/scheduledMessages/index.tsx @@ -9,7 +9,6 @@ import "./styles.css"; import { MessageOptions } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { isScheduleModeEnabled, ScheduledMessagesButton, setScheduleModeEnabled } from "./components/ChatBarButton"; @@ -30,8 +29,6 @@ import { stopScheduler } from "./utils"; -const logger = new Logger("ScheduledMessages"); - export const settings = definePluginSettings({ maxMessagesPerMinute: { type: OptionType.SLIDER, @@ -66,28 +63,20 @@ export const settings = definePluginSettings({ function handleReactionEvent(event: FluxReactionEvent): void { const { messageId, channelId, emoji } = event; - logger.info("handleReactionEvent:", { type: event.type, messageId, optimistic: event.optimistic, emoji: emoji?.name }); - - if (!messageId || !channelId || !emoji) { - logger.warn("handleReactionEvent: Missing required fields"); - return; - } + if (!messageId || !channelId || !emoji) return; const isPhantom = isPhantomMessage(messageId); - logger.info("handleReactionEvent: isPhantomMessage =", isPhantom); if (!isPhantom) return; if (event.optimistic) { // User-initiated reaction change - update our data - logger.info("handleReactionEvent: Processing optimistic", event.type); if (event.type === "MESSAGE_REACTION_ADD") { handleReactionAdd(messageId, channelId, emoji); } else if (event.type === "MESSAGE_REACTION_REMOVE") { handleReactionRemove(messageId, channelId, emoji); } } else { - logger.info("handleReactionEvent: Non-optimistic event, scheduling resync"); setTimeout(() => { resyncPhantomReactions(messageId, channelId); }, 50); diff --git a/src/equicordplugins/scheduledMessages/utils.ts b/src/equicordplugins/scheduledMessages/utils.ts index c3b1dca97..d15b35392 100644 --- a/src/equicordplugins/scheduledMessages/utils.ts +++ b/src/equicordplugins/scheduledMessages/utils.ts @@ -68,7 +68,6 @@ export function getChannelDisplayInfo(channelId: string): { name: string; avatar }; } - function getImageDimensions(dataUrl: string): Promise<{ width: number; height: number; }> { return new Promise(resolve => { const img = new Image(); @@ -292,7 +291,6 @@ function doRecreatePhantomMessage(messageId: string, channelId: string, reaction }, 50); } - async function uploadAttachment(channelId: string, att: ScheduledAttachment): Promise<{ id: string; filename: string; uploaded_filename: string; } | null> { return new Promise(resolve => { const bytes = Uint8Array.from(atob(att.data), c => c.charCodeAt(0)); diff --git a/src/equicordplugins/sekaiStickers/kanade.svg.tsx b/src/equicordplugins/sekaiStickers/kanade.svg.tsx index 906452fa4..58364188f 100644 --- a/src/equicordplugins/sekaiStickers/kanade.svg.tsx +++ b/src/equicordplugins/sekaiStickers/kanade.svg.tsx @@ -17,5 +17,4 @@ const kanadeSvg = () => { ); }; - export { kanadeSvg }; diff --git a/src/equicordplugins/sidebarChat/index.tsx b/src/equicordplugins/sidebarChat/index.tsx index ad797e71f..1831acffd 100644 --- a/src/equicordplugins/sidebarChat/index.tsx +++ b/src/equicordplugins/sidebarChat/index.tsx @@ -221,7 +221,6 @@ export default definePlugin({ useEffect(() => { if (!channel) return; - // @ts-expect-error if (channel.isForumLikeChannel()) { requireForumView().then(() => { setViewComponent( diff --git a/src/equicordplugins/signature/index.tsx b/src/equicordplugins/signature/index.tsx index 7c5630650..1a7d28605 100644 --- a/src/equicordplugins/signature/index.tsx +++ b/src/equicordplugins/signature/index.tsx @@ -126,31 +126,30 @@ export default definePlugin({ "textarea-context": ChatBarContextCheckbox }, - commands: [{ - name: "Signature", - description: "Toggle your signature", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "value", - description: "Toggle your signature (default is toggle)", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, + commands: [ + { + name: "signature", + description: "Toggle your signature", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "value", + description: "Toggle your signature (default is toggle)", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + ], + execute: async (args, ctx) => { + settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); + sendBotMessage(ctx.channel.id, { + content: settings.store.isEnabled ? "Signature enabled!" : "Signature disabled!", + }); }, - ], - execute: async (args, ctx) => { - settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); - sendBotMessage(ctx.channel.id, { - content: settings.store.isEnabled ? "Signature enabled!" : "Signature disabled!", - }); - }, - }], + } + ], }); - // text processing injection processor function textProcessing(input: string) { return `${input}\n${settings.store.textHeader ? settings.store.textHeader + " " : ""}${settings.store.name}`; } - - diff --git a/src/equicordplugins/snowfall/index.tsx b/src/equicordplugins/snowfall/index.tsx index 8d6c79275..7f103e9f4 100644 --- a/src/equicordplugins/snowfall/index.tsx +++ b/src/equicordplugins/snowfall/index.tsx @@ -466,7 +466,6 @@ export default definePlugin({ ), - settings, start() { diff --git a/src/equicordplugins/soundBoardLogger/settings.tsx b/src/equicordplugins/soundBoardLogger/settings.tsx index 7fbcff37a..47b6b037e 100644 --- a/src/equicordplugins/soundBoardLogger/settings.tsx +++ b/src/equicordplugins/soundBoardLogger/settings.tsx @@ -36,7 +36,6 @@ const settings = definePluginSettings({ setErrorMessage(null); } - if (changed < value) { setShouldShowWarning(true); } else { @@ -46,7 +45,6 @@ const settings = definePluginSettings({ setValue(changed); } - return (
The amount of soundboard ids you want to save at a time (0 lets you save infinite) diff --git a/src/equicordplugins/soundBoardLogger/utils.ts b/src/equicordplugins/soundBoardLogger/utils.ts index f25200a8f..f1fee3ae9 100644 --- a/src/equicordplugins/soundBoardLogger/utils.ts +++ b/src/equicordplugins/soundBoardLogger/utils.ts @@ -5,7 +5,7 @@ */ import { playAudio } from "@api/AudioPlayer"; -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { proxyLazy } from "@utils/lazy"; import { LazyComponent } from "@utils/react"; import { saveFile } from "@utils/web"; @@ -31,7 +31,6 @@ export interface SoundLogEntry extends SoundEvent { users: { id: string, plays: number[]; }[]; } - export const cl = classNameFactory("vc-soundlog-"); export function getEmojiUrl(emoji) { diff --git a/src/equicordplugins/statsfmPresence/index.tsx b/src/equicordplugins/statsfmPresence/index.tsx index 2d6ec4b4b..cdd2ea859 100644 --- a/src/equicordplugins/statsfmPresence/index.tsx +++ b/src/equicordplugins/statsfmPresence/index.tsx @@ -22,7 +22,6 @@ interface ActivityAssets { small_text?: string; } - interface ActivityButton { label: string; url: string; @@ -256,7 +255,6 @@ export default definePlugin({ const res = await fetch(`https://api.stats.fm/api/v1/users/${settings.store.username}/streams/current`); if (!res.ok) throw `${res.status} ${res.statusText}`; - const json = await res.json() as SFMR; if (!json.item) { logger.error("Error from Stats.fm API", json); diff --git a/src/equicordplugins/statusPresets/index.tsx b/src/equicordplugins/statusPresets/index.tsx index 75e5416f2..2a604f6c7 100644 --- a/src/equicordplugins/statusPresets/index.tsx +++ b/src/equicordplugins/statusPresets/index.tsx @@ -124,7 +124,6 @@ const RenderStatusMenuItem = ({ status, update, disabled }: { status: DiscordSta ; }; - const StatusSubMenuComponent = () => { const premiumType = OverridePremiumTypeStore.getState().premiumTypeActual ?? 0; const update = useForceUpdater(); @@ -143,7 +142,6 @@ const StatusSubMenuComponent = () => { ; }; - export default definePlugin({ name: "StatusPresets", description: "Allows you to remember your statuses and set them later", diff --git a/src/equicordplugins/stickerBlocker/index.tsx b/src/equicordplugins/stickerBlocker/index.tsx index f1325c333..aebdea50e 100644 --- a/src/equicordplugins/stickerBlocker/index.tsx +++ b/src/equicordplugins/stickerBlocker/index.tsx @@ -64,7 +64,6 @@ function blockedComponentRender(sticker) { return <>{elements}; } - const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const { favoriteableId, favoriteableType } = props ?? {}; @@ -116,7 +115,6 @@ function toggleBlock(name) { } } - function isStickerBlocked(name) { if (settings.store.blockedStickers === undefined || settings.store.blockedStickers == null) { return; @@ -133,7 +131,7 @@ export default definePlugin({ find: /\i\.\i\.STICKER_MESSAGE/, replacement: { match: /}\),\(null!=\i\?\i:(\i)\)\.name]}\);/, - replace: "$& if(Vencord.Settings.plugins.StickerBlocker.blockedStickers.split(\", \").includes($1.id)) { return($self.blockedComponent($1)) }" + replace: "$& if($self.isBlocked($1.id)) return($self.blockedComponent($1));" } } ], @@ -144,6 +142,13 @@ export default definePlugin({ start() { DataStore.createStore("StickerBlocker", "data"); }, + isBlocked(stickerId) { + if (settings.store.blockedStickers.split(", ").includes(stickerId)) { + return true; + } + + return false; + }, blockedComponent: ErrorBoundary.wrap(blockedComponentRender, { fallback: () =>

Failed to render :(

}), settings, }); diff --git a/src/equicordplugins/themeLibrary/components/ThemeCard.tsx b/src/equicordplugins/themeLibrary/components/ThemeCard.tsx index 6ece6b390..a1ee47071 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeCard.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeCard.tsx @@ -57,7 +57,7 @@ export const ThemeCard: React.FC = ({ theme, themeLinks, likedTh : [...themeLinks, `${apiUrl}/${theme.id}`]; setThemeLinks(onlineThemeLinks); - Vencord.Settings.themeLinks = onlineThemeLinks; + Settings.themeLinks = onlineThemeLinks; }; const handleThemeAttributesCheck = () => { diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx index 468e49f23..588c729fc 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx @@ -59,7 +59,7 @@ const SearchTags = { function ThemeTab() { const [themes, setThemes] = useState([]); const [filteredThemes, setFilteredThemes] = useState([]); - const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks); + const [themeLinks, setThemeLinks] = useState(Settings.themeLinks); const [likedThemes, setLikedThemes] = useState(); const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL }); const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard); @@ -123,7 +123,7 @@ function ThemeTab() { }, []); useEffect(() => { - setThemeLinks(Vencord.Settings.themeLinks); + setThemeLinks(Settings.themeLinks); }, []); useEffect(() => { @@ -292,7 +292,6 @@ function SubmitThemes() { ); } - function ThemeLibrary() { const [currentTab, setCurrentTab] = useState(TabItem.THEMES); diff --git a/src/equicordplugins/themeLibrary/utils/settings.tsx b/src/equicordplugins/themeLibrary/utils/settings.tsx index 3c13daa46..f7992a469 100644 --- a/src/equicordplugins/themeLibrary/utils/settings.tsx +++ b/src/equicordplugins/themeLibrary/utils/settings.tsx @@ -6,11 +6,11 @@ import * as DataStore from "@api/DataStore"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { HeadingSecondary } from "@components/Heading"; import { OpenExternalIcon } from "@components/Icons"; import { Paragraph } from "@components/Paragraph"; import { copyToClipboard } from "@utils/clipboard"; +import { classNameFactory } from "@utils/css"; import { OptionType } from "@utils/types"; import { Button, Toasts } from "@webpack/common"; diff --git a/src/equicordplugins/tidalEmbeds/index.tsx b/src/equicordplugins/tidalEmbeds/index.tsx index 7de2a1da7..7ca7d629a 100644 --- a/src/equicordplugins/tidalEmbeds/index.tsx +++ b/src/equicordplugins/tidalEmbeds/index.tsx @@ -7,7 +7,6 @@ import { EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; - export default definePlugin({ name: "TidalEmbeds", description: "Embeds TIDAL songs to make them playable in Discord.", diff --git a/src/equicordplugins/timezones/TimezoneModal.tsx b/src/equicordplugins/timezones/TimezoneModal.tsx index 3da79bf86..8d599fd1f 100644 --- a/src/equicordplugins/timezones/TimezoneModal.tsx +++ b/src/equicordplugins/timezones/TimezoneModal.tsx @@ -5,8 +5,8 @@ */ import * as DataStore from "@api/DataStore"; -import { classNameFactory } from "@api/Styles"; import { HeadingPrimary, HeadingSecondary } from "@components/Heading"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { Button, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common"; diff --git a/src/equicordplugins/toastNotifications/index.tsx b/src/equicordplugins/toastNotifications/index.tsx index 006be4dce..7cf4e4ab8 100644 --- a/src/equicordplugins/toastNotifications/index.tsx +++ b/src/equicordplugins/toastNotifications/index.tsx @@ -20,13 +20,13 @@ import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { makeRange, OptionType } from "@utils/types"; import { Channel, Message, User } from "@vencord/discord-types"; -import { MessageType } from "@vencord/discord-types/enums"; +import { MessageType, RelationshipType } from "@vencord/discord-types/enums"; import { findByPropsLazy, findStore } from "@webpack"; import { Button, ChannelStore, GuildRoleStore, NavigationRouter, RelationshipStore, SelectedChannelStore, StreamerModeStore, UserStore } from "@webpack/common"; import { ReactNode } from "react"; import { NotificationData, showNotification } from "./components/Notifications"; -import { RelationshipType, StreamingTreatment } from "./types"; +import { StreamingTreatment } from "./types"; let ignoredUsers: string[] = []; let notifyFor: string[] = []; @@ -437,8 +437,6 @@ async function handleGuildMessage(message: Message) { const all = notifyFor.includes(message.channel_id); const friend = settings.store.friendServerNotifications && RelationshipStore.isFriend(message.author.id); - - if (!all && !friend) { t = true; const isMention: boolean = message.content.includes(`<@${UserStore.getCurrentUser().id}>`); @@ -554,7 +552,6 @@ async function relationshipAdd(user: User, type: Number) { Notification.body = "You can now message them directly."; Notification.onClick = () => switchChannels(null, user.id); - await showNotification(Notification); } else if (type === RelationshipType.INCOMING_REQUEST) { diff --git a/src/equicordplugins/toastNotifications/types.ts b/src/equicordplugins/toastNotifications/types.ts index 4959fe853..6081eac0f 100644 --- a/src/equicordplugins/toastNotifications/types.ts +++ b/src/equicordplugins/toastNotifications/types.ts @@ -16,13 +16,6 @@ * along with this program. If not, see . */ -export const enum RelationshipType { - FRIEND = 1, - BLOCKED = 2, - INCOMING_REQUEST = 3, - OUTGOING_REQUEST = 4, -} - export const enum StreamingTreatment { NORMAL = 0, NO_CONTENT = 1, diff --git a/src/equicordplugins/tosuRPC/index.ts b/src/equicordplugins/tosuRPC/index.ts index 32f282372..c0b990db5 100644 --- a/src/equicordplugins/tosuRPC/index.ts +++ b/src/equicordplugins/tosuRPC/index.ts @@ -50,7 +50,6 @@ export default definePlugin({ } }); - async function onMessage(data: string) { const json: TosuApi = JSON.parse(data); // @ts-ignore diff --git a/src/equicordplugins/translatePlus/misc/types.ts b/src/equicordplugins/translatePlus/misc/types.ts index dde38179e..291be4576 100644 --- a/src/equicordplugins/translatePlus/misc/types.ts +++ b/src/equicordplugins/translatePlus/misc/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; export const cl = classNameFactory("eq-trans-"); diff --git a/src/equicordplugins/unitConverter/ConverterAccessory.tsx b/src/equicordplugins/unitConverter/ConverterAccessory.tsx index 1ec77ada4..2cc736ff1 100644 --- a/src/equicordplugins/unitConverter/ConverterAccessory.tsx +++ b/src/equicordplugins/unitConverter/ConverterAccessory.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { Message } from "@vencord/discord-types"; import { useState } from "@webpack/common"; diff --git a/src/equicordplugins/universalMention/index.tsx b/src/equicordplugins/universalMention/index.tsx index 4b3c75f20..1e893d2c2 100644 --- a/src/equicordplugins/universalMention/index.tsx +++ b/src/equicordplugins/universalMention/index.tsx @@ -5,7 +5,7 @@ */ import { definePluginSettings } from "@api/Settings"; -import { Alert } from "@components/Alert"; +import { Notice } from "@components/Notice"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { User } from "@vencord/discord-types"; @@ -41,9 +41,9 @@ export default definePlugin({ description: "Mention any user, regardless of channel access.", settings, settingsAboutComponent: () => ( - + Using Global Mention can cause performance issues and show an absurd amount of users in the autocomplete. - + ), patches: [ diff --git a/src/equicordplugins/urlHighlighter/index.tsx b/src/equicordplugins/urlHighlighter/index.tsx index ad525c9e5..e717ef883 100644 --- a/src/equicordplugins/urlHighlighter/index.tsx +++ b/src/equicordplugins/urlHighlighter/index.tsx @@ -7,11 +7,11 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; import { Heading } from "@components/Heading"; import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { findComponentByCodeLazy } from "@webpack"; import { ColorPicker, TextInput } from "@webpack/common"; diff --git a/src/equicordplugins/userpfp/AvatarModal.tsx b/src/equicordplugins/userpfp/AvatarModal.tsx new file mode 100644 index 000000000..35708dc80 --- /dev/null +++ b/src/equicordplugins/userpfp/AvatarModal.tsx @@ -0,0 +1,70 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { set } from "@api/DataStore"; +import { classNameFactory } from "@api/Styles"; +import { Button } from "@components/Button"; +import { Flex } from "@components/Flex"; +import { Heading } from "@components/Heading"; +import { Margins } from "@utils/margins"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; +import { TextInput, useState } from "@webpack/common"; + +import { data, KEY_DATASTORE } from "."; +const cl = classNameFactory("vc-custom-avatars-"); + +export function SetAvatarModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) { + const { avatars } = data; + const initialAvatarUrl = avatars[userId] || ""; + const [url, setUrl] = useState(initialAvatarUrl); + + function handleKey(e: KeyboardEvent) { + if (e.key === "Enter") saveUserAvatar(); + } + + async function saveUserAvatar() { + if (!url.trim()) { + await deleteUserAvatar(); + return; + } + + avatars[userId] = url.trim(); + await set(KEY_DATASTORE, avatars); + modalProps.onClose(); + } + + async function deleteUserAvatar() { + delete avatars[userId]; + await set(KEY_DATASTORE, avatars); + modalProps.onClose(); + } + + return ( + + + Custom Avatar + + + +
+ Enter PNG/GIF URL + +
+
+ + + {avatars[userId] && ()} + + + +
+ ); +} diff --git a/src/equicordplugins/userpfp/index.tsx b/src/equicordplugins/userpfp/index.tsx index 10fd0967a..ebbdaf4b2 100644 --- a/src/equicordplugins/userpfp/index.tsx +++ b/src/equicordplugins/userpfp/index.tsx @@ -6,22 +6,39 @@ import "./styles.css"; +import { get } from "@api/DataStore"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { Flex } from "@components/Flex"; import { Heart } from "@components/Heart"; +import { PencilIcon } from "@components/Icons"; +import { Margins } from "@components/margins"; +import { Notice } from "@components/Notice"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { openInviteModal } from "@utils/discord"; +import { openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; import { User } from "@vencord/discord-types"; +import { extractAndLoadChunksLazy } from "@webpack"; +import { IconUtils, Menu, UserStore } from "@webpack/common"; + +import { SetAvatarModal } from "./AvatarModal"; const cl = classNameFactory("vc-userpfp-"); const DONO_URL = "https://ko-fi.com/coolesding"; const INVITE_LINK = "userpfp-1129784704267210844"; -let data = { avatars: {} as Record }; +export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/); +export const KEY_DATASTORE = "vencord-custom-avatars"; +export const data = { avatars: {} as Record }; + const settings = definePluginSettings({ + overrideServerAvatars: { + type: OptionType.BOOLEAN, + description: "Override server avatars with custom avatars or the default user avatar if no custom avatar is set.", + default: true + }, preferNitro: { description: "Which avatar to use if both default animated (Nitro) pfp and UserPFP avatars are present", type: OptionType.SELECT, @@ -42,31 +59,37 @@ const settings = definePluginSettings({ } return true; }) - } + }, }); export default definePlugin({ name: "UserPFP", description: "Allows you to use an animated avatar without Nitro", - authors: [EquicordDevs.nexpid, Devs.thororen], + authors: [EquicordDevs.nexpid, Devs.thororen, EquicordDevs.soapphia], settings, + data, settingsAboutComponent: () => ( - - - - + <> + + Using the set avatar feature is local only meaning only you see it change. + + + + + + ), patches: [ { @@ -75,10 +98,33 @@ export default definePlugin({ { match: /(getUserAvatarURL:)(\i),/, replace: "$1$self.getAvatarHook($2)," + }, + { + match: /(getGuildMemberAvatarURLSimple:)(\i),/, + replace: "$1$self.getAvatarServerHook($2),", + predicate: () => settings.store.overrideServerAvatars } ] } ], + contextMenus: { + "user-context": (children, { user }) => { + if (!user?.id) return; + + children.push( + , + { + await requireSettingsMenu(); + openModal(modalProps => ); + }} + /> + ); + } + }, getAvatarHook: (original: any) => (user: User, animated: boolean, size: number) => { if (settings.store.preferNitro && user.avatar?.startsWith("a_")) return original(user, animated, size); if (!data.avatars[user.id]) return original(user, animated, size); @@ -91,11 +137,36 @@ export default definePlugin({ return res.toString(); }, + getAvatarServerHook: (original: any) => (config: any) => { + const { userId, avatar, size, canAnimate } = config; + const { avatars } = data; + + if (avatars[userId]) { + const customUrl = avatars[userId]; + try { + const res = new URL(customUrl); + if (size) res.searchParams.set("size", size.toString()); + return res.toString(); + } catch { + return customUrl; + } + } + + if (avatar) { + const user = UserStore.getUser(userId); + if (user?.avatar) { + return IconUtils.getUserAvatarURL(user, canAnimate, size); + } + } + + return original(config); + }, async start() { + data.avatars = await get>(KEY_DATASTORE) || {}; + await fetch(settings.store.databaseSource) - .then(async res => { - if (res.ok) data = await res.json(); - }) + .then(res => res.ok && res.json()) + .then(remote => remote?.avatars && Object.assign(data.avatars, remote.avatars)) .catch(() => null); } }); diff --git a/src/equicordplugins/vcNarratorCustom/index.tsx b/src/equicordplugins/vcNarratorCustom/index.tsx deleted file mode 100644 index ddbc6319b..000000000 --- a/src/equicordplugins/vcNarratorCustom/index.tsx +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { playAudio } from "@api/AudioPlayer"; -import { definePluginSettings } from "@api/Settings"; -import { Heading } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { Devs, EquicordDevs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { wordsToTitle } from "@utils/text"; -import definePlugin, { - OptionType, -} from "@utils/types"; -import { - Button, - ChannelStore, - GuildMemberStore, - React, - SelectedChannelStore, - SelectedGuildStore, - useMemo, - UserStore, - VoiceStateStore, -} from "@webpack/common"; - -// Create an in-memory cache (temporary, lost on restart) -const ttsCache = new Map(); - -// Helper function to open (or create) an IndexedDB database. -function openDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open("VcNarratorDB", 1); - request.onupgradeneeded = () => { - const db = request.result; - // Create an object store called "voices" if it doesn't already exist. - if (!db.objectStoreNames.contains("voices")) { - db.createObjectStore("voices"); - } - }; - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); -} - -// Function to get a cached voice line from IndexedDB. -async function getCachedVoiceFromDB(cacheKey: string): Promise { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction("voices", "readonly"); - const store = tx.objectStore("voices"); - const request = store.get(cacheKey); - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); - }); -} - -// Function to store a voice line in IndexedDB. -async function setCachedVoiceInDB(cacheKey: string, blob: Blob): Promise { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction("voices", "readwrite"); - const store = tx.objectStore("voices"); - const request = store.put(blob, cacheKey); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); -} - -interface VoiceState { - userId: string; - channelId?: string; - oldChannelId?: string; - deaf: boolean; - mute: boolean; - selfDeaf: boolean; - selfMute: boolean; -} - -// Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying -// Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would -// not say the second mute, which would lead you to believe they're unmuted - -async function speak(text: string, { volume, rate, customVoice } = settings.store) { - if (text.trim().length === 0) return; - - // Create a unique cache key using the voice setting and the text. - const cacheKey = `${customVoice}_${text}`; - - // 1. Check the in-memory cache (fast check) - if (ttsCache.has(cacheKey)) { - const cachedUrl = ttsCache.get(cacheKey)!; - playAudio(cachedUrl, { volume: volume * 100, speed: rate }); - return; - } - - // 2. Check the persistent IndexedDB cache. - try { - const cachedBlob = await getCachedVoiceFromDB(cacheKey); - if (cachedBlob) { - // Create a URL from the stored Blob. - const url = URL.createObjectURL(cachedBlob); - // Save it in the in-memory cache for next time. - ttsCache.set(cacheKey, url); - playAudio(url, { volume: volume * 100, speed: rate }); - return; - } - } catch (err) { - console.error("Error accessing IndexedDB:", err); - } - - // 3. If not found in either cache, fetch from the TTS API. - const response = await fetch( - "https://tiktok-tts.weilnet.workers.dev/api/generation", - { - method: "POST", - mode: "cors", - cache: "no-cache", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - }, - referrerPolicy: "no-referrer", - body: JSON.stringify({ - text: text, - voice: customVoice, - }), - } - ); - - const data = await response.json(); - const audioData = atob(data.data); - - const binaryData: number[] = []; - for (let i = 0; i < audioData.length; i++) { - binaryData.push(audioData.charCodeAt(i)); - } - - const blob = new Blob([new Uint8Array(binaryData)], { type: "audio/mpeg" }); - const url = URL.createObjectURL(blob); - - // Save the URL in the in-memory cache. - ttsCache.set(cacheKey, url); - - // Store the Blob in IndexedDB for persistence. - try { - await setCachedVoiceInDB(cacheKey, blob); - } catch (err) { - console.error("Error storing in IndexedDB:", err); - } - - playAudio(url, { volume: volume * 100, speed: rate }); -} - -function clean(str: string) { - const replacer = settings.store.latinOnly - ? /[^\p{Script=Latin}\p{Number}\p{Punctuation}\s]/gu - : /[^\p{Letter}\p{Number}\p{Punctuation}\s]/gu; - - return str - .normalize("NFKC") - .replace(replacer, "") - .replace(/_{2,}/g, "_") - .trim(); -} - -function formatText( - str: string, - user: string, - channel: string, - displayName: string, - nickname: string -) { - return str - .replaceAll("{{USER}}", clean(user) || (user ? "Someone" : "")) - .replaceAll("{{CHANNEL}}", clean(channel) || "channel") - .replaceAll( - "{{DISPLAY_NAME}}", - clean(displayName) || (displayName ? "Someone" : "") - ) - .replaceAll( - "{{NICKNAME}}", - clean(nickname) || (nickname ? "Someone" : "") - ); -} - -// For every user, channelId and oldChannelId will differ when moving channel. -// Only for the local user, channelId and oldChannelId will be the same when moving channel, -// for some ungodly reason -let myLastChannelId: string | undefined; - -function getTypeAndChannelId( - { channelId, oldChannelId }: VoiceState, - isMe: boolean -) { - if (isMe && channelId !== myLastChannelId) { - oldChannelId = myLastChannelId; - myLastChannelId = channelId; - } - - if (channelId !== oldChannelId) { - if (channelId) return [oldChannelId ? "move" : "join", channelId]; - if (oldChannelId) return ["leave", oldChannelId]; - } - return ["", ""]; -} - -function playSample(type: string) { - const currentUser = UserStore.getCurrentUser(); - const myGuildId = SelectedGuildStore.getGuildId()!; - - speak( - formatText( - settings.store[type + "Message"], - currentUser.username, - "general", - (currentUser as any).globalName ?? currentUser.username, - GuildMemberStore.getNick(myGuildId, currentUser.id) ?? - currentUser.username - ), - ); -} - -const settings = definePluginSettings({ - customVoice: { - type: OptionType.STRING, - description: "Custom voice id, currently just tiktok", - default: "en_us_001", - }, - volume: { - type: OptionType.SLIDER, - description: "Narrator Volume", - default: 1, - markers: [0, 0.25, 0.5, 0.75, 1], - stickToMarkers: false, - }, - rate: { - type: OptionType.SLIDER, - description: "Narrator Speed", - default: 1, - markers: [0.1, 0.5, 1, 2, 5, 10], - stickToMarkers: false, - }, - sayOwnName: { - description: "Say own name", - type: OptionType.BOOLEAN, - default: false, - }, - ignoreSelf: { - description: "Ignore yourself for all events.", - type: OptionType.BOOLEAN, - default: false, - }, - latinOnly: { - description: - "Strip non latin characters from names before saying them", - type: OptionType.BOOLEAN, - default: false, - }, - joinMessage: { - type: OptionType.STRING, - description: "Join Message", - default: "{{DISPLAY_NAME}} joined", - }, - leaveMessage: { - type: OptionType.STRING, - description: "Leave Message", - default: "{{DISPLAY_NAME}} left", - }, - moveMessage: { - type: OptionType.STRING, - description: "Move Message", - default: "{{DISPLAY_NAME}} moved to {{CHANNEL}}", - }, - muteMessage: { - type: OptionType.STRING, - description: "Mute Message (only self for now)", - default: "{{DISPLAY_NAME}} muted", - }, - unmuteMessage: { - type: OptionType.STRING, - description: "Unmute Message (only self for now)", - default: "{{DISPLAY_NAME}} unmuted", - }, - deafenMessage: { - type: OptionType.STRING, - description: "Deafen Message (only self for now)", - default: "{{DISPLAY_NAME}} deafened", - }, - undeafenMessage: { - type: OptionType.STRING, - description: "Undeafen Message (only self for now)", - default: "{{DISPLAY_NAME}} undeafened", - }, -}); - -export default definePlugin({ - name: "VcNarratorCustom", - description: "Announces when users join, leave, or move voice channels via narrator. TikTok TTS version; speechSynthesis is pretty boring. Ported from https://github.com/Loukious/Vencord", - authors: [Devs.Ven, Devs.Nyako, EquicordDevs.Loukios, EquicordDevs.examplegit], - settings, - flux: { - VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - const myGuildId = SelectedGuildStore.getGuildId()!; - const myChanId = SelectedChannelStore.getVoiceChannelId(); - const myId = UserStore.getCurrentUser().id; - - if ( - ChannelStore.getChannel(myChanId!)?.type === - 13 /* Stage Channel */ - ) - return; - - for (const state of voiceStates) { - const { userId, channelId, oldChannelId } = state; - const isMe = userId === myId; - if (!isMe) { - if (!myChanId) continue; - if (channelId !== myChanId && oldChannelId !== myChanId) - continue; - } - - const [type, id] = getTypeAndChannelId(state, isMe); - if (!type) continue; - - const template = settings.store[type + "Message"]; - const user = - isMe && !settings.store.sayOwnName - ? "" - : UserStore.getUser(userId).username; - const displayName = - user && - ((UserStore.getUser(userId) as any).globalName ?? user); - const nickname = - user && - (GuildMemberStore.getNick(myGuildId, userId) ?? - displayName); - const channel = ChannelStore.getChannel(id).name; - - speak( - formatText(template, user, channel, displayName, nickname) - ); - } - }, - - AUDIO_TOGGLE_SELF_MUTE() { - const chanId = SelectedChannelStore.getVoiceChannelId()!; - const s = VoiceStateStore.getVoiceStateForChannel( - chanId - ) as VoiceState; - if (!s) return; - }, - - AUDIO_TOGGLE_SELF_DEAF() { - const chanId = SelectedChannelStore.getVoiceChannelId()!; - const s = VoiceStateStore.getVoiceStateForChannel( - chanId - ) as VoiceState; - if (!s) return; - }, - }, - - settingsAboutComponent() { - const types = useMemo( - () => - Object.keys(settings.def) - .filter(k => k.endsWith("Message")) - .map(k => k.slice(0, -7)), - [] - ); - - const errorComponent: React.ReactElement | null = null; - - return ( -
- - You can customise the spoken messages below. You can disable - specific messages by setting them to nothing - - - The special placeholders {"{{USER}}"},{" "} - {"{{DISPLAY_NAME}}"},{" "} - {"{{NICKNAME}}"} and{" "} - {"{{CHANNEL}}"} will be replaced with the - user's name (nothing if it's yourself), the user's display - name, the user's nickname on current server and the - channel's name respectively - - - You can find a list of custom voices (tiktok only for now){" "} - - here - - - - Play Example Sounds - -
- {types.map(t => ( - - ))} -
- {errorComponent} -
- ); - }, -}); diff --git a/src/equicordplugins/voiceChannelLog.dev/index.tsx b/src/equicordplugins/voiceChannelLog.dev/index.tsx index 5f8a20044..94941f6e1 100644 --- a/src/equicordplugins/voiceChannelLog.dev/index.tsx +++ b/src/equicordplugins/voiceChannelLog.dev/index.tsx @@ -6,8 +6,8 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { Message, User } from "@vencord/discord-types"; import { findByCodeLazy } from "@webpack"; @@ -138,7 +138,6 @@ export default definePlugin({ timestamp: new Date() }; - if (settings.store.mode !== 2) { addLogEntry(logEntry, oldChannelId); addLogEntry(logEntry, channelId); diff --git a/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx b/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx index db77c87b4..b476a3a8e 100644 --- a/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx +++ b/src/equicordplugins/wallpaperFree/components/ctxmenu.tsx @@ -11,7 +11,6 @@ import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common"; import { SetWallpaperModal } from "./modal"; - const addWallpaperMenu = (channelId?: string, guildId?: string) => { const setWallpaper = (url?: string) => { FluxDispatcher.dispatch({ diff --git a/src/equicordplugins/wallpaperFree/index.tsx b/src/equicordplugins/wallpaperFree/index.tsx index de2ce5b75..9545ef8e7 100644 --- a/src/equicordplugins/wallpaperFree/index.tsx +++ b/src/equicordplugins/wallpaperFree/index.tsx @@ -16,7 +16,6 @@ import { ChannelContextPatch, GuildContextPatch, UserContextPatch } from "./comp import { GlobalDefaultComponent, TipsComponent } from "./components/util"; import { WallpaperFreeStore } from "./store"; - export const settings = definePluginSettings({ globalDefault: { description: "Set a global default wallpaper for all channels.", diff --git a/src/equicordplugins/whitelistedEmojis/index.tsx b/src/equicordplugins/whitelistedEmojis/index.tsx index 31889fd68..70d21eb37 100644 --- a/src/equicordplugins/whitelistedEmojis/index.tsx +++ b/src/equicordplugins/whitelistedEmojis/index.tsx @@ -49,7 +49,6 @@ function isItemAllowed(item: (CustomEmoji | UnicodeEmoji)) { return cache_allowedList.some(emoji => emoji.name === item.name); } - function itemAlreadyInList(item: ContextMenuEmoji) { return cache_allowedList.some(emoji => emoji.name === item.name); } diff --git a/src/equicordplugins/whosWatching/index.tsx b/src/equicordplugins/whosWatching/index.tsx index d503fb0c7..87a48cad4 100644 --- a/src/equicordplugins/whosWatching/index.tsx +++ b/src/equicordplugins/whosWatching/index.tsx @@ -5,12 +5,12 @@ */ import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { Heading, HeadingSecondary } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { getIntlMessage, openUserProfile } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; diff --git a/src/equicordplugins/wigglyText/index.tsx b/src/equicordplugins/wigglyText/index.tsx index 61133bac6..1f683e9e6 100644 --- a/src/equicordplugins/wigglyText/index.tsx +++ b/src/equicordplugins/wigglyText/index.tsx @@ -45,7 +45,7 @@ const classMap = [ let styles: HTMLStyleElement; const updateStyles = () => { - const inten = Vencord.Settings.plugins.WigglyText.intensity + "px"; + const inten = settings.store.intensity + "px"; styles.textContent = ` .wiggle-example { list-style-type: disc; diff --git a/src/main/csp/index.ts b/src/main/csp/index.ts index 489356284..92e96041c 100644 --- a/src/main/csp/index.ts +++ b/src/main/csp/index.ts @@ -88,7 +88,6 @@ const stringifyPolicy = (policy: PolicyMap): string => .map(directive => directive.flat().join(" ")) .join("; "); - const patchCsp = (headers: PolicyMap) => { const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only"); if (reportOnlyHeader) diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 4fc317e4e..8c6489680 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -22,16 +22,18 @@ import "./settings"; import { debounce } from "@shared/debounce"; import { IpcEvents } from "@shared/IpcEvents"; -import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; +import { BrowserWindow, ipcMain, nativeTheme, shell, systemPreferences } from "electron"; import monacoHtml from "file://monacoWin.html?minify&base64"; -import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs"; +import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile, unlink } from "fs/promises"; import { join, normalize } from "path"; import { registerCspIpcHandlers } from "./csp/manager"; -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; +import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; +const RENDERER_CSS_PATH = join(__dirname, "renderer.css"); + mkdirSync(THEMES_DIR, { recursive: true }); registerCspIpcHandlers(); @@ -43,12 +45,8 @@ export function ensureSafePath(basePath: string, path: string) { return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; } -async function readCss() { - try { - return await readFile(QUICKCSS_PATH, "utf-8"); - } catch { - return ""; - } +function readCss() { + return readFile(QUICK_CSS_PATH, "utf-8").catch(() => ""); } async function listThemes(): Promise<{ fileName: string; content: string; }[]> { @@ -67,7 +65,7 @@ function getThemeData(fileName: string) { return readFile(safePath, "utf-8"); } -ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); +ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICK_CSS_PATH)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { try { @@ -83,7 +81,7 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => - writeFileSync(QUICKCSS_PATH, css) + writeFileSync(QUICK_CSS_PATH, css) ); ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); @@ -94,33 +92,52 @@ ipcMain.handle(IpcEvents.DELETE_THEME, (_, fileName) => { if (!safePath) return Promise.reject(`Unsafe path ${fileName}`); return unlink(safePath); }); -ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({ - // win & mac only - "os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}` -})); +ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => { + let accentColor = systemPreferences.getAccentColor?.() ?? ""; + + if (accentColor.length && accentColor[0] !== "#") { + accentColor = `#${accentColor}`; + } + + return { + "os-accent-color": accentColor + }; +}); ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR)); ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR)); -export function initIpc(mainWindow: BrowserWindow) { +ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => { let quickCssWatcher: FSWatcher | undefined; + let rendererCssWatcher: FSWatcher | undefined; - open(QUICKCSS_PATH, "a+").then(fd => { + open(QUICK_CSS_PATH, "a+").then(fd => { fd.close(); - quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { - mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); + quickCssWatcher = watch(QUICK_CSS_PATH, { persistent: false }, debounce(async () => { + sender.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); }).catch(() => { }); const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { - mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); + sender.postMessage(IpcEvents.THEME_UPDATE, void 0); })); - mainWindow.once("closed", () => { + if (IS_DEV) { + rendererCssWatcher = watch(RENDERER_CSS_PATH, { persistent: false }, async () => { + sender.postMessage(IpcEvents.RENDERER_CSS_UPDATE, await readFile(RENDERER_CSS_PATH, "utf-8")); + }); + } + + sender.once("destroyed", () => { quickCssWatcher?.close(); themesWatcher.close(); + rendererCssWatcher?.close(); }); -} +}); + +ipcMain.on(IpcEvents.GET_MONACO_THEME, e => { + e.returnValue = nativeTheme.shouldUseDarkColors ? "vs-dark" : "vs-light"; +}); ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { const title = "Equicord QuickCSS Editor"; @@ -146,3 +163,11 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { await win.loadURL(`data:text/html;base64,${monacoHtml}`); }); + +ipcMain.handle(IpcEvents.GET_RENDERER_CSS, () => readFile(RENDERER_CSS_PATH, "utf-8")); + +if (IS_DISCORD_DESKTOP) { + ipcMain.on(IpcEvents.PRELOAD_GET_RENDERER_JS, e => { + e.returnValue = readFileSync(join(__dirname, "renderer.js"), "utf-8"); + }); +} diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 807d28562..6b042400e 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -20,7 +20,6 @@ import { onceDefined } from "@shared/onceDefined"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; import { dirname, join } from "path"; -import { initIpc } from "./ipcMain"; import { RendererSettings } from "./settings"; import { IS_VANILLA } from "./utils/constants"; @@ -107,8 +106,6 @@ if (!IS_VANILLA) { // Disable the Electron call entirely so that Discord can't dynamically change the size this.setMinimumSize = (width: number, height: number) => { }; } - - initIpc(this); } else super(options); } } diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts index d8a6fe28c..52caecd5c 100644 --- a/src/main/updater/http.ts +++ b/src/main/updater/http.ts @@ -61,7 +61,6 @@ async function fetchUpdates() { if (hash === gitHash) return false; - const asset = data.assets.find(a => a.name === ASAR_FILE); PendingUpdate = asset.browser_download_url; diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index e9fa39ab4..2c997461e 100644 --- a/src/main/utils/constants.ts +++ b/src/main/utils/constants.ts @@ -30,7 +30,7 @@ export const DATA_DIR = process.env.EQUICORD_USER_DATA_DIR ?? ( export const SETTINGS_DIR = join(DATA_DIR, "settings"); export const THEMES_DIR = join(DATA_DIR, "themes"); -export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); +export const QUICK_CSS_PATH = join(SETTINGS_DIR, "quickCss.css"); export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json"); export const DEV_MIGRATED = join(SETTINGS_DIR, "migration"); @@ -42,6 +42,7 @@ export const ALLOWED_PROTOCOLS = [ "com.epicgames.launcher:", "tidal:", "itunes:", + "vrcx:", ]; export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla"); @@ -61,7 +62,7 @@ if (IS_DEV) { setTimeout(() => { try { if (existsSync(settings)) copyFileSync(settings, SETTINGS_FILE); - if (existsSync(quickCss)) copyFileSync(quickCss, QUICKCSS_PATH); + if (existsSync(quickCss)) copyFileSync(quickCss, QUICK_CSS_PATH); writeFileSync(DEV_MIGRATED, "migrated"); app.relaunch(); app.exit(0); diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 8169aaaae..59f78f671 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -26,7 +26,6 @@ import { copyWithToast } from "@utils/discord"; import { Logger } from "@utils/Logger"; import { shouldShowContributorBadge, shouldShowEquicordContributorBadge } from "@utils/misc"; import definePlugin from "@utils/types"; -import { User } from "@vencord/discord-types"; import { ContextMenuApi, Menu, Toasts, UserStore } from "@webpack/common"; import Plugins, { PluginMeta } from "~plugins"; @@ -131,16 +130,13 @@ export default definePlugin({ authors: [Devs.Megu, Devs.Ven, Devs.TheSun], required: true, patches: [ - { - find: ".MODAL]:26", - replacement: { - match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/, - replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])" - } - }, { find: "#{intl::PROFILE_USER_BADGES}", replacement: [ + { + match: /(?<=\{[^}]*?)badges:\i(?=[^}]*?}=(\i))/, + replace: "_$&=$self.useBadges($1.displayProfile).concat($1.badges)" + }, { match: /alt:" ","aria-hidden":!0,src:.{0,50}(\i).iconSrc/, replace: "...$1.props,$&" @@ -155,13 +151,6 @@ export default definePlugin({ replace: "...$self.getBadgeMouseEventHandlers($1),$&" } ] - }, - { - find: "profileCardUsernameRow,children:", - replacement: { - match: /badges:(\i)(?<=displayProfile:(\i).+?)/, - replace: "badges:[...$self.getBadges($2),...$1]" - } } ], @@ -197,15 +186,14 @@ export default definePlugin({ clearInterval(intervalId); }, - getBadges(props: { userId: string; user?: User; guildId: string; }) { - if (!props) return []; + // doesn't use hooks itself, but some plugins might do so in their getBadges function + useBadges(profile: { userId: string; guildId: string; }) { + if (!profile) return []; try { - props.userId ??= props.user?.id!; - - return _getBadges(props); + return _getBadges(profile); } catch (e) { - new Logger("BadgeAPI#hasBadges").error(e); + new Logger("BadgeAPI#useBadges").error(e); return []; } }, @@ -215,7 +203,6 @@ export default definePlugin({ return ; }, { noop: true }), - getBadgeMouseEventHandlers(badge: ProfileBadge & BadgeUserArgs) { const handlers = {} as Record void>; diff --git a/src/plugins/_api/dynamicImageModalApi.ts b/src/plugins/_api/dynamicImageModalApi.ts index 114b48363..92f839fc5 100644 --- a/src/plugins/_api/dynamicImageModalApi.ts +++ b/src/plugins/_api/dynamicImageModalApi.ts @@ -7,7 +7,6 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; - export default definePlugin({ name: "DynamicImageModalAPI", authors: [Devs.sadan, Devs.Nuckyz], diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 16952be95..e5dab1d12 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -5,7 +5,7 @@ */ import { definePluginSettings } from "@api/Settings"; -import { BackupRestoreIcon, CloudIcon, LogIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons"; +import { BackupRestoreIcon, CloudIcon, LogIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PluginsIcon, UpdaterIcon } from "@components/Icons"; import { BackupAndRestoreTab, ChangelogTab, @@ -21,24 +21,39 @@ import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import { isTruthy } from "@utils/guards"; import definePlugin, { IconProps, OptionType } from "@utils/types"; -import { waitFor } from "@webpack"; +import { findByPropsLazy } from "@webpack"; import { React } from "@webpack/common"; import type { ComponentType, PropsWithChildren, ReactNode } from "react"; -let LayoutTypes = { - SECTION: 1, - SIDEBAR_ITEM: 2, - PANEL: 3, - PANE: 4 -}; -waitFor(["SECTION", "SIDEBAR_ITEM", "PANEL"], v => LayoutTypes = v); +const enum LayoutType { + ROOT = 0, + SECTION = 1, + SIDEBAR_ITEM = 2, + PANEL = 3, + SPLIT = 4, + CATEGORY = 5, + ACCORDION = 6, + LIST = 7, + RELATED = 8, + FIELD_SET = 9, + TAB_ITEM = 10, + STATIC = 11, + BUTTON = 12, + TOGGLE = 13, + SLIDER = 14, + SELECT = 15, + RADIO = 16, + NAVIGATOR = 17, + CUSTOM = 18 +} -const FallbackSectionTypes = { - HEADER: "HEADER", - DIVIDER: "DIVIDER", - CUSTOM: "CUSTOM" -}; -type SectionTypes = typeof FallbackSectionTypes; +const LayoutTypes: typeof LayoutType = findByPropsLazy("SECTION", "SIDEBAR_ITEM", "PANEL"); + +const enum SectionType { + HEADER = "HEADER", + DIVIDER = "DIVIDER", + CUSTOM = "CUSTOM" +} type SettingsLocation = | "top" @@ -49,7 +64,7 @@ type SettingsLocation = | "bottom"; interface SettingsLayoutNode { - type: number; + type: LayoutType; key?: string; legacySearchKey?: string; getLegacySearchKey?(): string; @@ -62,12 +77,13 @@ interface SettingsLayoutNode { } interface EntryOptions { - key: string, - title: string, - panelTitle?: string, - Component: ComponentType<{}>, + key: string; + title: string; + panelTitle?: string; + Component: ComponentType<{}>; Icon: ComponentType; } + interface SettingsLayoutBuilder { key?: string; buildLayout(): SettingsLayoutNode[]; @@ -100,7 +116,6 @@ export default definePlugin({ { find: ".versionHash", replacement: [ - // for whatever reason the first letter of RELEASE_CHANNEL is now lowercase, so we fix that here because it looks better imo { match: /\.RELEASE_CHANNEL/, replace: "$&.replace(/^./, c => c.toUpperCase())" @@ -130,13 +145,7 @@ export default definePlugin({ replacement: [ { match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/, - replace: ( - _, - sectionTypes, - commaOrSemi, - elements, - element, - ) => + replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`, }, { @@ -146,12 +155,10 @@ export default definePlugin({ }, ], }, - // Fix the settings cog context menu to work properly { find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}", replacement: { - // Skip the check Discord performs to make sure the section being selected in the user settings context menu is valid - match: /(?<=function\((\i),(\i),\i\)\{)(?=let \i=Object.values\(\i\.\i\).+?(\(0,\i\.openUserSettings\))\()/, + match: /(?<=function\((\i),(\i),\i\)\{)(?=let \i=Object\.values\(\i\.\i\).+?(\(0,\i\.openUserSettings\))\()/, replace: (_, settingsPanel, section, openUserSettings) => `${openUserSettings}(${settingsPanel},{section:${section}});return;` } }, @@ -178,40 +185,20 @@ export default definePlugin({ key: key + "_panel", type: LayoutTypes.PANEL, useTitle: () => panelTitle, - }; - - const render = { - // FIXME + buildLayout: () => [], StronglyDiscouragedCustomComponent: () => , render: () => , }; - // FIXME - if (LayoutTypes.PANE) { - panel.buildLayout = () => [ - { - key: key + "_pane", - type: LayoutTypes.PANE, - useTitle: () => panelTitle, - buildLayout: () => [], - ...render - } - ]; - } else { - Object.assign(panel, render); - panel.buildLayout = () => []; - } - - return ({ + return { key, type: LayoutTypes.SIDEBAR_ITEM, - // FIXME legacySearchKey: title.toUpperCase(), getLegacySearchKey: () => title.toUpperCase(), useTitle: () => title, icon: () => , buildLayout: () => [panel] - }); + }; }, getSettingsSectionMappings() { @@ -235,7 +222,6 @@ export default definePlugin({ const layout = originalLayoutBuilder.buildLayout(); if (originalLayoutBuilder.key !== "$Root") return layout; if (!Array.isArray(layout)) return layout; - if (layout.some(s => s?.key === "equicord_section")) return layout; const { buildEntry } = this; @@ -292,19 +278,7 @@ export default definePlugin({ Component: PatchHelperTab, Icon: PatchHelperIcon }), - ...this.customEntries.map(buildEntry), - // TODO: Remove deprecated customSections in a future update - ...this.customSections.map((func, i) => { - const { section, element, label } = func(FallbackSectionTypes); - if (section !== "Equibop") return null; - - return buildEntry({ - key: `equicord_deprecated_custom_${section}`, - title: label, - Component: element, - Icon: VesktopSettingsIcon - }); - }) + ...this.customEntries.map(buildEntry) ].filter(isTruthy); const equicordSection: SettingsLayoutNode = { @@ -339,11 +313,10 @@ export default definePlugin({ return layout; }, - /** @deprecated Use customEntries */ - customSections: [] as ((SectionTypes: SectionTypes) => any)[], + customSections: [] as ((SectionTypes: Record) => { section: string; element: ComponentType; label: string; id?: string; })[], customEntries: [] as EntryOptions[], - makeSettingsCategories(SectionTypes: SectionTypes) { + makeSettingsCategories(SectionTypes: Record) { return [ { section: SectionTypes.HEADER, @@ -414,15 +387,12 @@ export default definePlugin({ isRightSpot({ header, settings: s }: { header?: string; settings?: string[]; }) { const firstChild = s?.[0]; - // lowest two elements... sanity backup - if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") - return true; + if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true; const { settingsLocation } = settings.store; if (settingsLocation === "bottom") return firstChild === "LOGOUT"; - if (settingsLocation === "belowActivity") - return firstChild === "CHANGELOG"; + if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG"; if (!header) return; @@ -434,10 +404,7 @@ export default definePlugin({ aboveActivity: getIntlMessage("ACTIVITY_SETTINGS"), }; - if ( - !names[settingsLocation] || - names[settingsLocation].endsWith("_SETTINGS") - ) + if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS")) return firstChild === "PREMIUM"; return header === names[settingsLocation]; @@ -451,49 +418,39 @@ export default definePlugin({ addSettings( elements: any[], element: { header?: string; settings: string[]; }, - sectionTypes: SectionTypes, + SectionTypes: Record, ) { - if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) - return; + if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return; this.patchedSettings.add(elements); - - elements.push(...this.makeSettingsCategories(sectionTypes)); + elements.push(...this.makeSettingsCategories(SectionTypes)); }, - wrapSettingsHook( - originalHook: (...args: any[]) => Record[], - ) { + wrapSettingsHook(originalHook: (...args: any[]) => Record[]) { return (...args: any[]) => { const elements = originalHook(...args); if (!this.patchedSettings.has(elements)) - elements.unshift(...this.makeSettingsCategories(FallbackSectionTypes)); + elements.unshift(...this.makeSettingsCategories({ HEADER: SectionType.HEADER, DIVIDER: SectionType.DIVIDER, CUSTOM: SectionType.CUSTOM }) as Record[]); return elements; }; }, get electronVersion() { - return ( - VencordNative.native.getVersions().electron || - window.legcord?.electron || - null - ); + return VencordNative.native.getVersions().electron ?? window.legcord?.electron ?? null; }, get chromiumVersion() { try { return ( - VencordNative.native.getVersions().chrome || - // @ts-expect-error Typescript will add userAgentData IMMEDIATELY + VencordNative.native.getVersions().chrome ?? + // @ts-expect-error userAgentData types navigator.userAgentData?.brands?.find( - b => - b.brand === "Chromium" || b.brand === "Google Chrome", - )?.version || + (b: { brand: string; }) => b.brand === "Chromium" || b.brand === "Google Chrome", + )?.version ?? null ); } catch { - // inb4 some stupid browser throws unsupported error for navigator.userAgentData, it's only in chromium return null; } }, diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 5a88cc883..0e561c013 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -28,6 +28,7 @@ import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { openSettingsTabModal, UpdaterTab } from "@components/settings"; import { platformName } from "@equicordplugins/equicordHelper/utils"; +import customIdle from "@plugins/customIdle"; import { gitHash, gitHashShort } from "@shared/vencordUserAgent"; import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, EQUIBOP_CONTRIB_ROLE_ID, EQUICORD_TEAM, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_CONTRIB_ROLE_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID, VC_REGULAR_ROLE_ID, VENCORD_CONTRIB_ROLE_ID } from "@utils/constants"; import { sendMessage } from "@utils/discord"; @@ -135,7 +136,7 @@ async function generateDebugInfoMessage() { let clientString = `${clientInfo.name}`; clientString += `${clientInfo.version ? ` v${clientInfo.version}` : ""}`; clientString += `${clientInfo.info ? ` • ${clientInfo.info}` : ""}`; - clientString += `${clientInfo.shortHash ? ` • [${clientInfo.shortHash}](${clientInfo.hash})` : ""}`; + clientString += `${clientInfo.shortHash ? ` • [${clientInfo.shortHash}]()` : ""}`; const spoofInfo = IS_EQUIBOP ? tryOrElse(() => VesktopNative.app.getPlatformSpoofInfo?.(), null) : null; const platformDisplay = spoofInfo?.spoofed @@ -158,13 +159,13 @@ async function generateDebugInfoMessage() { "NoRPC", "NoProfileThemes", "NoMosaic", "NoRoleHeaders", "NoSystemBadge", "AlwaysAnimate", "ClientTheme", "SoundTroll", "Ingtoninator", "NeverPausePreviews", "IdleAutoRestart", - ].filter(Vencord.Plugins.isPluginEnabled) ?? []).sort(); + ].filter(isPluginEnabled) ?? []).sort(); - if (Vencord.Plugins.isPluginEnabled("CustomIdle") && Vencord.Settings.plugins.CustomIdle.idleTimeout === 0) { - potentiallyProblematicPlugins.push("CustomIdle"); + if (isPluginEnabled(customIdle.name) && customIdle.settings.store.idleTimeout === 0) { + potentiallyProblematicPlugins.push(customIdle.name); } - const potentiallyProblematicPluginsNote = "-# Note: said plugin(s) might be the not be the cause of your problem. They are just plug-ins that cause common issues."; + const potentiallyProblematicPluginsNote = "-# Note: These plugins might not be the cause of your problem. They are simply plugins that cause common issues."; const commonIssues = { "Activity Sharing Disabled": tryOrElse(() => !ShowCurrentGame.getSetting(), false), @@ -194,7 +195,6 @@ function generatePluginList() { const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin); const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin); - let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`; if (enabledUserPlugins.length) { diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index cd92ef311..1109d28a7 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -150,23 +150,25 @@ export default definePlugin({ upload.filename = newFilename; }, - commands: [{ - name: "Spoiler", - description: "Toggle your spoiler", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "value", - description: "Toggle your Spoiler (default is toggle)", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, + commands: [ + { + name: "Spoiler", + description: "Toggle your spoiler", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "value", + description: "Toggle your Spoiler (default is toggle)", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + ], + execute: async (args, ctx) => { + settings.store.spoilerMessages = !!findOption(args, "value", !settings.store.spoilerMessages); + sendBotMessage(ctx.channel.id, { + content: settings.store.spoilerMessages ? "Spoiler enabled!" : "Spoiler disabled!", + }); }, - ], - execute: async (args, ctx) => { - settings.store.spoilerMessages = !!findOption(args, "value", !settings.store.spoilerMessages); - sendBotMessage(ctx.channel.id, { - content: settings.store.spoilerMessages ? "Spoiler enabled!" : "Spoiler disabled!", - }); - }, - }], + } + ], }); diff --git a/src/plugins/betterFolders/FolderSideBar.tsx b/src/plugins/betterFolders/FolderSideBar.tsx index 0e643958e..fb33d9317 100644 --- a/src/plugins/betterFolders/FolderSideBar.tsx +++ b/src/plugins/betterFolders/FolderSideBar.tsx @@ -17,13 +17,12 @@ */ import ErrorBoundary from "@components/ErrorBoundary"; -import { findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { Animations, useStateFromStores } from "@webpack/common"; +import { findComponentByCodeLazy } from "@webpack"; +import { Animations, ChannelRTCStore, useStateFromStores } from "@webpack/common"; import type { CSSProperties } from "react"; import { ExpandedGuildFolderStore, settings, SortedGuildStore } from "."; -const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const GuildsBar = findComponentByCodeLazy('("guildsnav")'); function getExpandedFolderIds() { diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 6f26206e8..f94331bbd 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -325,7 +325,6 @@ export default definePlugin({ FolderSideBar, closeFolders, - wrapGuildNodeComponent(node: any, originalComponent: () => ReactNode, isBetterFolders: boolean, expandedFolderIds?: Set) { if ( !isBetterFolders || diff --git a/src/equicordplugins/allCallTimers/Timer.tsx b/src/plugins/callTimer/Timer.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/Timer.tsx rename to src/plugins/callTimer/Timer.tsx diff --git a/src/equicordplugins/allCallTimers/TimerIcon.tsx b/src/plugins/callTimer/TimerIcon.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/TimerIcon.tsx rename to src/plugins/callTimer/TimerIcon.tsx diff --git a/src/plugins/callTimer/alignedChatInputFix.css b/src/plugins/callTimer/alignedChatInputFix.css index 6ec48b39f..4374d53ab 100644 --- a/src/plugins/callTimer/alignedChatInputFix.css +++ b/src/plugins/callTimer/alignedChatInputFix.css @@ -1,4 +1,4 @@ [class*="panels"] [class*="inner"], [class*="rtcConnectionStatus"] { height: fit-content !important; -} \ No newline at end of file +} diff --git a/src/plugins/callTimer/index.tsx b/src/plugins/callTimer/index.tsx index a51bdc0c2..198c47ea8 100644 --- a/src/plugins/callTimer/index.tsx +++ b/src/plugins/callTimer/index.tsx @@ -1,38 +1,28 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginToSettings, migrateSettingsFromPlugin } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { useTimer } from "@utils/react"; import { formatDurationMs } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; +import { PassiveUpdateState, VoiceState } from "@vencord/discord-types"; +import { FluxDispatcher, GuildStore, React, UserStore } from "@webpack/common"; import alignedChatInputFix from "./alignedChatInputFix.css?managed"; +import { Timer } from "./Timer"; -const settings = definePluginSettings({ +export const settings = definePluginSettings({ format: { type: OptionType.SELECT, - description: "The timer format. This can be any valid moment.js format", + description: "Compact or human readable format:", options: [ { - label: "30d 23:00:42", + label: "30:23:00:42", value: "stopwatch", default: true }, @@ -42,34 +32,246 @@ const settings = definePluginSettings({ } ] }, + allCallTimers: { + type: OptionType.BOOLEAN, + description: "Add call timer to all users in a server voice channel", + restartNeeded: true, + default: false + }, + showWithoutHover: { + type: OptionType.BOOLEAN, + description: "Always show the timer without needing to hover", + restartNeeded: true, + default: false + }, + showRoleColor: { + type: OptionType.BOOLEAN, + description: "Show the user's role color (if this plugin in enabled)", + restartNeeded: false, + default: false + }, + trackSelf: { + type: OptionType.BOOLEAN, + description: "Also track yourself", + restartNeeded: false, + default: false + }, + showSeconds: { + type: OptionType.BOOLEAN, + description: "Show seconds in the timer", + restartNeeded: false, + default: false + }, + watchLargeGuilds: { + type: OptionType.BOOLEAN, + description: "Track users in large guilds. This may cause lag if you're in a lot of large guilds with active voice users. Tested with up to 2000 active voice users with no issues.", + restartNeeded: true, + default: false + } }); +// Save the join time of all users in a Map +type userJoinData = { channelId: string, time: number; guildId: string; }; +const userJoinTimes = new Map(); + +/** + * The function `addUserJoinTime` stores the join time of a user in a specific channel within a guild. + * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of + * the user who is joining a channel in a guild. + * @param {string} channelId - The `channelId` parameter represents the unique identifier of the + * channel where the user joined. + * @param {string} guildId - The `guildId` parameter in the `addUserJoinTime` function represents the + * unique identifier of the guild (server) to which the user belongs. It is used to associate the + * user's join time with a specific guild within the application or platform. + */ +function addUserJoinTime(userId: string, channelId: string, guildId: string) { + // create a random number + userJoinTimes.set(userId, { channelId, time: Date.now(), guildId }); +} + +/** + * The function `removeUserJoinTime` removes the join time of a user identified by their user ID. + * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of + * a user whose join time needs to be removed. + */ +function removeUserJoinTime(userId: string) { + userJoinTimes.delete(userId); +} + +// For every user, channelId and oldChannelId will differ when moving channel. +// Only for the local user, channelId and oldChannelId will be the same when moving channel, +// for some ungodly reason +let myLastChannelId: string | undefined; + +// Allow user updates on discord first load +let runOneTime = true; + +migratePluginToSettings(false, "CallTimer", "AllCallTimers", "allCallTimers"); +migrateSettingsFromPlugin("CallTimer", "AllCallTimers", "showWithoutHover", "showRoleColor", "trackSelf", "showSeconds", "watchLargeGuilds"); export default definePlugin({ name: "CallTimer", - description: "Adds a timer to vcs", - authors: [Devs.Ven], + description: "Add call timers for all users in voice channels and in the connection status.", + authors: [Devs.Ven, EquicordDevs.MaxHerbold, Devs.D3SOX], managedStyle: alignedChatInputFix, settings, + isModified: true, startTime: 0, interval: void 0 as NodeJS.Timeout | undefined, - patches: [{ - find: "renderConnectionStatus(){", - replacement: { - // in renderConnectionStatus() - match: /(lineClamp:1,children:)(\i)(?=,|}\))/, - replace: "$1[$2,$self.renderTimer(this.props.channel.id)]" + patches: [ + { + find: ".usernameSpeaking]", + predicate: () => settings.store.allCallTimers, + replacement: [ + { + match: /(?<=user:(\i).*?)iconGroup,.{0,200}children:\[/, + replace: "$&$self.renderTimer($1.id),", + predicate: () => !settings.store.showWithoutHover, + }, + { + match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, + replace: "$&,$self.renderTimer($1.id),", + predicate: () => settings.store.showWithoutHover, + } + ] + }, + { + find: "renderConnectionStatus(){", + replacement: { + match: /(lineClamp:1,children:)(\i)(?=,|}\))/, + replace: "$1[$2,$self.renderConnectionTimer(this.props.channel.id)]" + } } - }], + ], - renderTimer(channelId: string) { + flux: { + VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { + const myId = UserStore.getCurrentUser().id; + + for (const state of voiceStates) { + const { userId, channelId, guildId } = state; + const isMe = userId === myId; + + if (!guildId) { + // guildId is never undefined here + continue; + } + + // check if the state does not actually has a `oldChannelId` property + if (!("oldChannelId" in state) && !runOneTime && !settings.store.watchLargeGuilds) { + // batch update triggered. This is ignored because it + // is caused by opening a previously unopened guild + continue; + } + + let { oldChannelId } = state; + if (isMe && channelId !== myLastChannelId) { + oldChannelId = myLastChannelId; + myLastChannelId = channelId ?? undefined; + } + + if (channelId !== oldChannelId) { + if (channelId) { + // move or join + addUserJoinTime(userId, channelId, guildId); + } else if (oldChannelId) { + // leave + removeUserJoinTime(userId); + } + } + } + runOneTime = false; + }, + PASSIVE_UPDATE_V1(passiveUpdate: PassiveUpdateState) { + if (settings.store.watchLargeGuilds) { + return; + } + + const { voiceStates } = passiveUpdate; + if (!voiceStates) { + // if there are no users in a voice call + return; + } + + // find all users that have the same guildId and if that user is not in the voiceStates, remove them from the map + const { guildId } = passiveUpdate; + + // check the guildId in the userJoinTimes map + for (const [userId, data] of userJoinTimes) { + if (data.guildId === guildId) { + // check if the user is in the voiceStates + const userInVoiceStates = voiceStates.find(state => state.userId === userId); + if (!userInVoiceStates) { + // remove the user from the map + removeUserJoinTime(userId); + } + } + } + + // since we were gifted this data let's use it to update our join times + for (const state of voiceStates) { + const { userId, channelId } = state; + + if (!channelId) { + // channelId is never undefined here + continue; + } + + // check if the user is in the map + if (userJoinTimes.has(userId)) { + // check if the user is in a channel + if (channelId !== userJoinTimes.get(userId)?.channelId) { + // update the user's join time + addUserJoinTime(userId, channelId, guildId); + } + } else { + // user wasn't previously tracked, add the user to the map + addUserJoinTime(userId, channelId, guildId); + } + } + }, + }, + + subscribeToAllGuilds() { + // we need to subscribe to all guilds' events because otherwise we would miss updates on large guilds + const guilds = Object.values(GuildStore.getGuilds()).map(guild => guild.id); + const subscriptions = guilds.reduce((acc, id) => ({ ...acc, [id]: { typing: true } }), {}); + FluxDispatcher.dispatch({ type: "GUILD_SUBSCRIPTIONS_FLUSH", subscriptions }); + }, + + start() { + if (settings.store.watchLargeGuilds) { + this.subscribeToAllGuilds(); + } + }, + + renderTimer(userId: string) { + // get the user join time from the users object + const joinTime = userJoinTimes.get(userId); + if (!joinTime?.time) { + // join time is unknown + return; + } + if (userId === UserStore.getCurrentUser().id && !settings.store.trackSelf) { + // don't show for self + return; + } + + return ( + + + + ); + }, + + renderConnectionTimer(channelId: string) { return - + ; }, - Timer({ channelId }: { channelId: string; }) { + ConnectionTimer({ channelId }: { channelId: string; }) { const time = useTimer({ deps: [channelId] }); diff --git a/src/equicordplugins/allCallTimers/timerText.tsx b/src/plugins/callTimer/timerText.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/timerText.tsx rename to src/plugins/callTimer/timerText.tsx diff --git a/src/plugins/clientTheme/components/Settings.tsx b/src/plugins/clientTheme/components/Settings.tsx index 69ad76efa..e484a8127 100644 --- a/src/plugins/clientTheme/components/Settings.tsx +++ b/src/plugins/clientTheme/components/Settings.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { ErrorCard } from "@components/ErrorCard"; import { HeadingPrimary, HeadingSecondary } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import { relativeLuminance } from "@plugins/clientTheme/utils/colorUtils"; import { createOrUpdateThemeColorVars } from "@plugins/clientTheme/utils/styleUtils"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { findByCodeLazy, findStoreLazy } from "@webpack"; import { Button, ColorPicker, ThemeStore, useStateFromStores } from "@webpack/common"; diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index d11d8b68d..a4e80b23b 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -256,7 +256,6 @@ export default definePlugin({ } ], - start() { const shortcuts = makeShortcuts(); window.shortcutList = {}; diff --git a/src/plugins/cursorBuddy/index.tsx b/src/plugins/cursorBuddy/index.tsx new file mode 100644 index 000000000..f4a18e3dd --- /dev/null +++ b/src/plugins/cursorBuddy/index.tsx @@ -0,0 +1,143 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { isPluginEnabled } from "@api/PluginManager"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const ONEKO_IMAGE = "https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.gif"; +const ONEKO_SCRIPT = "https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.js"; +const FATASS_HORSE = "https://raw.githubusercontent.com/nexpid/fatass-horse/351f158bfd8fafd44d9c17faad61f2a80bcd33e3/horse.js"; + +const settings = definePluginSettings({ + buddy: { + description: "Pick a cursor buddy", + type: OptionType.SELECT, + options: [ + { + label: "Oneko", + value: "oneko", + default: true + }, + { + label: "Fatass Horse", + value: "fathorse" + } + ], + onChange: load, + }, + speed: { + description: "Speed of Da Cat :3", + type: OptionType.NUMBER, + default: 10, + isValid: (value: number) => value >= 0 || "Speed must be bigger than 0", + onChange: load, + }, + fps: { + description: "Framerate of the fatass horse", + type: OptionType.NUMBER, + default: 24, + isValid: (value: number) => value > 0 || "Framerate must be bigger than 0", + onChange: load + }, + size: { + description: "Size of the fatass horse", + type: OptionType.NUMBER, + default: 120, + isValid: (value: number) => value > 0 || "Size must be bigger than 0", + onChange: load + }, + fade: { + description: "If the horse should fade when the cursor is near", + type: OptionType.BOOLEAN, + default: true, + onChange: load + }, + freeroam: { + description: "If the horse should roam freely when idle", + type: OptionType.BOOLEAN, + default: true, + onChange: load + }, + shake: { + description: "If the horse should shake the window when it's walking", + type: OptionType.BOOLEAN, + default: false, + onChange: load + }, +}, { + fps: { + disabled() { return this.store.buddy !== "fathorse"; }, + }, + size: { + disabled() { return this.store.buddy !== "fathorse"; }, + }, + fade: { + disabled() { return this.store.buddy !== "fathorse"; }, + }, + freeroam: { + disabled() { return this.store.buddy !== "fathorse"; }, + }, + shake: { + disabled() { return this.store.buddy !== "fathorse"; }, + } +}); + +function unload() { + document.getElementById("oneko")?.remove(); + document.getElementById("fathorse")?.remove(); +} + +function load() { + if (!isPluginEnabled("CursorBuddy")) return; + unload(); + + switch (settings.store.buddy) { + case "oneko": { + fetch(ONEKO_SCRIPT) + .then(x => x.text()) + .then(s => s.replace("const nekoSpeed = 10;", `const nekoSpeed = ${settings.store.speed};`)) + .then(s => s.replace("./oneko.gif", ONEKO_IMAGE) + .replace("(isReducedMotion)", "(false)")) + .then(eval); + break; + } + case "fathorse": { + fetch(FATASS_HORSE).then(x => x.text()).then(s => (0, eval)(s)({ + speed: settings.store.speed, + fps: settings.store.fps, + size: settings.store.size, + fade: settings.store.fade, + freeroam: settings.store.freeroam, + shake: settings.store.shake + })); + } + } +} + +migratePluginSettings("CursorBuddy", "Oneko", "oneko"); +export default definePlugin({ + name: "CursorBuddy", + description: "only a slightly annoying plugin", + authors: [Devs.Ven, Devs.adryd, EquicordDevs.nexpid], + settings, + + start: load, + stop: unload, +}); diff --git a/src/plugins/customRPC/RpcSettings.tsx b/src/plugins/customRPC/RpcSettings.tsx index 019cc431b..bdca847d2 100644 --- a/src/plugins/customRPC/RpcSettings.tsx +++ b/src/plugins/customRPC/RpcSettings.tsx @@ -7,11 +7,11 @@ import "./settings.css"; import { isPluginEnabled } from "@api/PluginManager"; -import { classNameFactory } from "@api/Styles"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { resolveError } from "@components/settings/tabs/plugins/components/Common"; import { debounce } from "@shared/debounce"; +import { classNameFactory } from "@utils/css"; import { ActivityType } from "@vencord/discord-types/enums"; import { Select, Text, TextInput, useState } from "@webpack/common"; diff --git a/src/plugins/dearrow/index.tsx b/src/plugins/dearrow/index.tsx index e44fd0f27..1247472d4 100644 --- a/src/plugins/dearrow/index.tsx +++ b/src/plugins/dearrow/index.tsx @@ -63,7 +63,6 @@ async function embedDidMount(this: Component) { if (!hasTitle && !hasThumb) return; - embed.dearrow = { enabled: dearrowByDefault }; diff --git a/src/plugins/decor/lib/constants.ts b/src/plugins/decor/lib/constants.ts index 302ed64d1..212b070a0 100644 --- a/src/plugins/decor/lib/constants.ts +++ b/src/plugins/decor/lib/constants.ts @@ -15,7 +15,6 @@ export const GUILD_ID = "1096357702931841148"; export const INVITE_KEY = "dXp2SdxDcP"; export const DECORATION_FETCH_COOLDOWN = 1000 * 60 * 60 * 4; // 4 hours - export async function setBaseUrl(baseUrl: string) { try { const config = await fetch(`${baseUrl}/api/config`).then(res => res.json()); diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts index 62040508f..44e58eccf 100644 --- a/src/plugins/decor/ui/index.ts +++ b/src/plugins/decor/ui/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack"; export const cl = classNameFactory("vc-decor-"); diff --git a/src/plugins/devCompanion.dev/util.tsx b/src/plugins/devCompanion.dev/util.tsx index 436856f11..0002e7276 100644 --- a/src/plugins/devCompanion.dev/util.tsx +++ b/src/plugins/devCompanion.dev/util.tsx @@ -5,6 +5,7 @@ */ import { showNotice } from "@api/Notices"; +import { plugins, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager"; import { Settings } from "@api/Settings"; import { canonicalizeMatch } from "@utils/patches"; import { CodeFilter, stringMatches, wreq } from "@webpack"; @@ -126,7 +127,7 @@ export function toggleEnabled(name: string, beforeReload: (error?: string) => vo }); } } - const plugin = Vencord.Plugins.plugins[name]; + const plugin = plugins[name]; const settings = Settings.plugins[plugin.name]; @@ -136,7 +137,7 @@ export function toggleEnabled(name: string, beforeReload: (error?: string) => vo // If we're enabling a plugin, make sure all deps are enabled recursively. if (!wasEnabled) { - const { restartNeeded, failures } = Vencord.Plugins.startDependenciesRecursive(plugin); + const { restartNeeded, failures } = startDependenciesRecursive(plugin); if (failures.length) { console.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`); showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null); @@ -166,7 +167,7 @@ export function toggleEnabled(name: string, beforeReload: (error?: string) => vo return; } - const result = wasEnabled ? Vencord.Plugins.stopPlugin(plugin) : Vencord.Plugins.startPlugin(plugin); + const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin); if (!result) { settings.enabled = false; diff --git a/src/plugins/expressionCloner/index.tsx b/src/plugins/expressionCloner/index.tsx index 52e049fa1..a1690ced6 100644 --- a/src/plugins/expressionCloner/index.tsx +++ b/src/plugins/expressionCloner/index.tsx @@ -64,6 +64,9 @@ const PremiumTierStickerLimitMap = { 3: 60 } as const; +const MAX_EMOJI_SIZE_BYTES = 256 * 1024; +const MAX_STICKER_SIZE_BYTES = 512 * 1024; + function getGuildMaxStickerSlots(guild: Guild) { if (guild.features.has("MORE_STICKERS") && guild.premiumTier === 3) return 120; @@ -71,11 +74,11 @@ function getGuildMaxStickerSlots(guild: Guild) { return PremiumTierStickerLimitMap[guild.premiumTier] ?? PremiumTierStickerLimitMap[0]; } -function getUrl(data: Data) { +function getUrl(data: Data, size: number) { if (data.t === "Emoji") - return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}?size=4096&lossless=true`; + return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}?size=${size}&lossless=true`; - return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExtMap[data.format_type]}?size=4096&lossless=true`; + return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExtMap[data.format_type]}?size=${size}&lossless=true`; } async function fetchSticker(id: string) { @@ -99,7 +102,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) { data.append("name", sticker.name); data.append("tags", sticker.tags); data.append("description", sticker.description); - data.append("file", await fetchBlob(getUrl(sticker))); + data.append("file", await fetchBlob(sticker)); const { body } = await RestAPI.post({ url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId), @@ -117,7 +120,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) { } async function cloneEmoji(guildId: string, emoji: Emoji) { - const data = await fetchBlob(getUrl(emoji)); + const data = await fetchBlob(emoji); const dataUrl = await new Promise(resolve => { const reader = new FileReader(); @@ -163,12 +166,23 @@ function getGuildCandidates(data: Data) { }).sort((a, b) => a.name.localeCompare(b.name)); } -async function fetchBlob(url: string) { - const res = await fetch(url); - if (!res.ok) - throw new Error(`Failed to fetch ${url} - ${res.status}`); +async function fetchBlob(data: Data) { + const MAX_SIZE = data.t === "Sticker" + ? MAX_STICKER_SIZE_BYTES + : MAX_EMOJI_SIZE_BYTES; - return res.blob(); + for (let size = 4096; size >= 16; size /= 2) { + const url = getUrl(data, size); + const res = await fetch(url); + if (!res.ok) + throw new Error(`Failed to fetch ${url} - ${res.status}`); + + const blob = await res.blob(); + if (blob.size <= MAX_SIZE) + return blob; + } + + throw new Error(`Failed to fetch ${data.t} within size limit of ${MAX_SIZE / 1000}kB`); } async function doClone(guildId: string, data: Sticker | Emoji) { @@ -314,7 +328,7 @@ function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable { const res = await fetchData(); const data = { t: type, ...res } as Sticker | Emoji; - const url = getUrl(data); + const url = getUrl(data, 128); return modalProps => ( diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 8e7b68510..ce8bfa631 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -26,13 +26,11 @@ import { Logger } from "@utils/Logger"; import definePlugin, { OptionType, Patch } from "@utils/types"; import type { Emoji, Message, Sticker } from "@vencord/discord-types"; import { StickerFormatType } from "@vencord/discord-types/enums"; -import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; -import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, GuildMemberStore, lodash, OverridePremiumTypeStore, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators } from "@webpack/common"; +import { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from "@webpack"; +import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, GuildMemberStore, lodash, OverridePremiumTypeStore, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore } from "@webpack/common"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; import type { ReactElement, ReactNode } from "react"; -const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); - const BINARY_READ_OPTIONS = findByPropsLazy("readerFactory"); function searchProtoClassField(localName: string, protoClass: any) { diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx index 1eb885698..589862c4d 100644 --- a/src/plugins/fakeProfileThemes/index.tsx +++ b/src/plugins/fakeProfileThemes/index.tsx @@ -248,4 +248,3 @@ export default definePlugin({ ; }, { noop: true }), }); - diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx index b0062b8c1..1b45ffa0b 100644 --- a/src/plugins/favGifSearch/index.tsx +++ b/src/plugins/favGifSearch/index.tsx @@ -129,7 +129,6 @@ export default definePlugin({ } }); - function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) { const [query, setQuery] = useState(""); const ref = useRef(null); @@ -145,14 +144,12 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc return; } - // scroll back to top ref.current ?.closest("#gif-picker-tab-panel") ?.querySelector('[class*="scrollerBase"]') ?.scrollTo(0, 0); - const result = props.favCopy .map(gif => ({ @@ -193,8 +190,6 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc ); } - - export function getTargetString(urlStr: string) { let url: URL; try { diff --git a/src/plugins/fixImagesQuality/index.tsx b/src/plugins/fixImagesQuality/index.tsx index 4c3e6eeb3..848cedb0d 100644 --- a/src/plugins/fixImagesQuality/index.tsx +++ b/src/plugins/fixImagesQuality/index.tsx @@ -7,6 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { Card } from "@components/Card"; import { Flex } from "@components/Flex"; +import { Margins } from "@components/margins"; import { Paragraph } from "@components/Paragraph"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -15,7 +16,7 @@ import definePlugin, { OptionType } from "@utils/types"; const settings = definePluginSettings({ originalImagesInChat: { type: OptionType.BOOLEAN, - description: "Also load the original image in Chat", + description: "Also load the original image in Chat. WARNING: Read the caveats above", default: false, } }); @@ -31,7 +32,7 @@ export default definePlugin({ find: ".handleImageLoad)", replacement: { match: /getSrc\(\i\)\{/, - replace: "$&var _vcSrc=$self.getSrc(this.props);if(_vcSrc)return _vcSrc;" + replace: "$&var _vcSrc=$self.getSrc(this.props,arguments[1]);if(_vcSrc)return _vcSrc;" } } ], @@ -47,36 +48,50 @@ export default definePlugin({
  • — In the image modal, the original image will be loaded.
  • + You can also enable original image in chat, but beware of the following caveats: - You can also enable original image in chat, but this may cause performance issues! +
      +
    • — Animated images (GIF, WebP, etc.) in chat will always animate, regardless of if the App is focused.
    • +
    • — May cause lag.
    • +
    ); }, - getSrc(props: { src: string; mediaLayoutType: string; width: number; height: number; }) { - if (!props) return; + getSrc(props: { src: string; mediaLayoutType: string; width: number; height: number; contentType: string; }, freeze?: boolean) { + if (!props?.src) return; try { - if (!settings.store.originalImagesInChat && props.mediaLayoutType === "MOSAIC") { + const { contentType, height, mediaLayoutType, src, width } = props; + if (!contentType?.startsWith("image/") || src.startsWith("data:")) return; + + const url = new URL(src); + if (!url.pathname.startsWith("/attachments/")) return; + + url.searchParams.set("animated", String(!freeze)); + + if (!settings.store.originalImagesInChat && mediaLayoutType === "MOSAIC") { // make sure the image is not too large - const pixels = props.width * props.height; + const pixels = width * height; const limit = 2000 * 1200; - if (pixels <= limit) return props.src; + if (pixels <= limit) + return url.toString(); const scale = Math.sqrt(pixels / limit); - const url = new URL(props.src); - url.searchParams.set("width", Math.round(props.width / scale).toString()); - url.searchParams.set("height", Math.round(props.height / scale).toString()); + + url.searchParams.set("width", Math.round(width / scale).toString()); + url.searchParams.set("height", Math.round(height / scale).toString()); return url.toString(); } - return props.src?.replace("https://media.discordapp.net/attachments/", "https://cdn.discordapp.com/attachments/"); + url.hostname = "cdn.discordapp.com"; + return url.toString(); } catch (e) { new Logger("FixImagesQuality").error("Failed to make image src", e); - return props.src; + return; } } }); diff --git a/src/plugins/greetStickerPicker/index.tsx b/src/plugins/greetStickerPicker/index.tsx index 0cc3e170d..87ae9b717 100644 --- a/src/plugins/greetStickerPicker/index.tsx +++ b/src/plugins/greetStickerPicker/index.tsx @@ -67,7 +67,6 @@ function greet(channel: Channel, message: Message, stickers: string[]) { } } - function GreetMenu({ channel, message }: { message: Message, channel: Channel; }) { const s = settings.use(["greetMode", "multiGreetChoices"]); const { greetMode, multiGreetChoices = [] } = s; diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index d0d776f9c..3f82b2588 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -13,8 +13,7 @@ import { Paragraph } from "@components/Paragraph"; import { Devs } from "@utils/constants"; import { Margins } from "@utils/margins"; import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { Button, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "@webpack/common"; +import { Button, RunningGameStore, showToast, TextInput, Toasts, Tooltip, useEffect, useState } from "@webpack/common"; const enum ActivitiesTypes { Game, @@ -32,8 +31,6 @@ const enum FilterMode { Blacklist } -const RunningGameStore = findStoreLazy("RunningGameStore"); - const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!; function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) { diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx index dbe196072..f69ab4b37 100644 --- a/src/plugins/imageZoom/components/Magnifier.tsx +++ b/src/plugins/imageZoom/components/Magnifier.tsx @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { settings } from "@plugins/imageZoom"; import { ELEMENT_ID } from "@plugins/imageZoom/constants"; import { waitFor } from "@plugins/imageZoom/utils/waitFor"; +import { classNameFactory } from "@utils/css"; import { FluxDispatcher, useLayoutEffect, useMemo, useRef, useState } from "@webpack/common"; interface Vec2 { @@ -162,6 +162,9 @@ export const Magnifier = ErrorBoundary.wrap(({ instance, size: i const imageSrc = useMemo(() => { try { const imageUrl = new URL(instance.props.src); + if (imageUrl.pathname.startsWith("/attachments/")) + imageUrl.hostname = "cdn.discordapp.com"; + imageUrl.searchParams.set("animated", "true"); return imageUrl.toString(); } catch { diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 0f10db3e9..09589d4a2 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -79,7 +79,6 @@ export const settings = definePluginSettings({ }, }); - const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { // Discord re-uses the image context menu for links to for the copy and open buttons if ("href" in props) return; diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts index 9bfc88a73..93b87beff 100644 --- a/src/plugins/loadingQuotes/index.ts +++ b/src/plugins/loadingQuotes/index.ts @@ -84,7 +84,6 @@ export default definePlugin({ if (!enableDiscordPresetQuotes) quotes.length = 0; - if (enablePluginPresetQuotes) quotes.push(...presetQuotes); diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx index 6887af51a..e20cc8e82 100644 --- a/src/plugins/memberCount/MemberCount.tsx +++ b/src/plugins/memberCount/MemberCount.tsx @@ -6,9 +6,9 @@ import { getCurrentChannel } from "@utils/discord"; import { isObjectEmpty } from "@utils/misc"; -import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common"; +import { ChannelStore, GuildMemberCountStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common"; -import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from "."; +import { ChannelMemberStore, cl, numberFormat, settings, ThreadMemberListStore } from "."; import { CircleIcon } from "./CircleIcon"; import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; import { VoiceIcon } from "./VoiceIcon"; @@ -40,7 +40,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t const totalCount = useStateFromStores( [GuildMemberCountStore], - () => GuildMemberCountStore.getMemberCount(guildId) + () => GuildMemberCountStore.getMemberCount(guildId!) ); let onlineCount = useStateFromStores( diff --git a/src/plugins/memberCount/OnlineMemberCountStore.ts b/src/plugins/memberCount/OnlineMemberCountStore.ts index 54cc46974..37dbc65af 100644 --- a/src/plugins/memberCount/OnlineMemberCountStore.ts +++ b/src/plugins/memberCount/OnlineMemberCountStore.ts @@ -22,7 +22,7 @@ export const OnlineMemberCountStore = proxyLazy(() => { async _ensureCount(guildId: string) { if (onlineMemberMap.has(guildId)) return; - await ChannelActionCreators.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id); + await ChannelActionCreators.preload(guildId, GuildChannelStore.getDefaultChannel(guildId)!.id); } ensureCount(guildId?: string) { diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index e2d78e2b0..66d9d44b1 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -19,16 +19,15 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { FluxStore } from "@vencord/discord-types"; import { findStoreLazy } from "@webpack"; import { MemberCount } from "./MemberCount"; -export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId?: string): number | null; }; export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & { getProps(guildId?: string, channelId?: string): { groups: { count: number; id: string; }[]; }; }; @@ -72,7 +71,7 @@ export default definePlugin({ replacement: [ { match: /(?<=var\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, - replace: ":[$1?.startsWith('members')?$self.render():null,$2", + replace: ":[$1?.includes('members')?$self.render():null,$2", }, ], predicate: () => settings.store.memberList diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx index 91ffe4da8..1027f0d64 100644 --- a/src/plugins/mentionAvatars/index.tsx +++ b/src/plugins/mentionAvatars/index.tsx @@ -6,8 +6,10 @@ import "./styles.css"; +import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import showMeYourName from "@plugins/showMeYourName"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { User } from "@vencord/discord-types"; @@ -68,7 +70,7 @@ export default definePlugin({ renderUsername: ErrorBoundary.wrap(({ user, username, showMeYourNameMention }: { user: User, username: string, showMeYourNameMention: JSX.Element | null | undefined; }) => { const [isHovering, setIsHovering] = useState(false); - const nameContent = Vencord.Settings.plugins.ShowMeYourName.enabled && showMeYourNameMention + const nameContent = isPluginEnabled(showMeYourName.name) && showMeYourNameMention ? showMeYourNameMention : <>{getUsernameString(username)}; return ( diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index b9eb718d1..16dd6c7c5 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -19,11 +19,12 @@ import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import NoReplyMentionPlugin from "@plugins/noReplyMention"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { ApplicationIntegrationType, MessageFlags } from "@vencord/discord-types/enums"; import { findByPropsLazy } from "@webpack"; -import { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, WindowStore } from "@webpack/common"; +import { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, RestAPI, WindowStore } from "@webpack/common"; const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); const EditStore = findByPropsLazy("isEditing", "isEditingAny"); @@ -33,6 +34,9 @@ const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false); +let doubleClickTimeout: ReturnType | null = null; +let pendingDoubleClickAction: (() => void) | null = null; + const settings = definePluginSettings({ enableDeleteOnClick: { type: OptionType.BOOLEAN, @@ -49,6 +53,16 @@ const settings = definePluginSettings({ description: "Enable double click to reply", default: true }, + enableTripleClickToReact: { + type: OptionType.BOOLEAN, + description: "Enable triple click to react with an emoji", + default: false + }, + reactEmoji: { + type: OptionType.STRING, + description: "Emoji to react with (e.g. 💀 or pepe:123456789)", + default: "💀" + }, requireModifier: { type: OptionType.BOOLEAN, description: "Only do double click actions when shift/ctrl is held", @@ -56,10 +70,28 @@ const settings = definePluginSettings({ } }); +async function react(channelId: string, messageId: string, emoji: string) { + const trimmed = emoji.trim(); + if (!trimmed) return; + + const customMatch = trimmed.match(/^:?([\w-]+):(\d+)$/); + const emojiParam = customMatch + ? `${customMatch[1]}:${customMatch[2]}` + : encodeURIComponent(trimmed); + + try { + await RestAPI.put({ + url: `/channels/${channelId}/messages/${messageId}/reactions/${emojiParam}/@me` + }); + } catch (e) { + new Logger("MessageClickActions").error("Failed to add reaction:", e); + } +} + export default definePlugin({ name: "MessageClickActions", - description: "Hold Backspace and click to delete, double click to edit/reply", - authors: [Devs.Ven], + description: "Hold Backspace and click to delete, double click to edit/reply, triple click to react", + authors: [Devs.Ven, EquicordDevs.keyages], settings, @@ -73,26 +105,62 @@ export default definePlugin({ document.removeEventListener("keydown", keydown); document.removeEventListener("keyup", keyup); WindowStore.removeChangeListener(focusChanged); + + if (doubleClickTimeout) { + clearTimeout(doubleClickTimeout); + doubleClickTimeout = null; + } + pendingDoubleClickAction = null; }, onMessageClick(msg, channel, event) { const myId = AuthenticationStore.getId(); const isMe = msg.author.id === myId; const isSelfInvokedUserApp = msg.interactionMetadata?.authorizing_integration_owners[ApplicationIntegrationType.USER_INSTALL] === myId; - if (!isDeletePressed) { - if (event.detail < 2) return; - if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; - if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; - if (msg.deleted === true) return; + if (isDeletePressed) { + if (!settings.store.enableDeleteOnClick) return; + if (!(isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) return; + + if (msg.deleted) { + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: channel.id, + id: msg.id, + mlDeleted: true + }); + } else { + MessageActions.deleteMessage(channel.id, msg.id); + } + event.preventDefault(); + return; + } + + if (event.detail === 3) { + if (doubleClickTimeout) { + clearTimeout(doubleClickTimeout); + doubleClickTimeout = null; + pendingDoubleClickAction = null; + } + + if (settings.store.enableTripleClickToReact) { + react(channel.id, msg.id, settings.store.reactEmoji); + event.preventDefault(); + } + return; + } + + if (event.detail !== 2) return; + if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; + if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; + if (msg.deleted === true) return; + + const executeDoubleClick = () => { if (isMe) { if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return; - MessageActions.startEditMessage(channel.id, msg.id, msg.content); - event.preventDefault(); } else { if (!settings.store.enableDoubleClickToReply) return; - if (!MessageTypeSets.REPLYABLE.has(msg.type) || msg.hasFlag(MessageFlags.EPHEMERAL)) return; const isShiftPress = event.shiftKey && !settings.store.requireModifier; @@ -108,17 +176,21 @@ export default definePlugin({ showMentionToggle: channel.guild_id !== null }); } - } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) { - if (msg.deleted) { - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: channel.id, - id: msg.id, - mlDeleted: true - }); - } else { - MessageActions.deleteMessage(channel.id, msg.id); + }; + + if (settings.store.enableTripleClickToReact) { + if (doubleClickTimeout) { + clearTimeout(doubleClickTimeout); } + pendingDoubleClickAction = executeDoubleClick; + doubleClickTimeout = setTimeout(() => { + pendingDoubleClickAction?.(); + pendingDoubleClickAction = null; + doubleClickTimeout = null; + }, 300); + event.preventDefault(); + } else { + executeDoubleClick(); event.preventDefault(); } }, diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 238eb9986..ec314a184 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -127,7 +127,6 @@ const settings = definePluginSettings({ } }); - async function fetchMessage(channelID: string, messageID: string) { const cached = messageCache.get(messageID); if (cached) return cached.message; @@ -157,7 +156,6 @@ async function fetchMessage(channelID: string, messageID: string) { return message; } - function getImages(message: Message): Attachment[] { const attachments: Attachment[] = []; @@ -223,7 +221,6 @@ function withEmbeddedBy(message: Message, embeddedBy: string[]) { }); } - function MessageEmbedAccessory({ message }: { message: Message; }) { // @ts-expect-error const embeddedBy: string[] = message.vencordEmbeddedBy ?? []; diff --git a/src/plugins/messageLogger/HistoryModal.tsx b/src/plugins/messageLogger/HistoryModal.tsx index 797a2db7e..3e9007c55 100644 --- a/src/plugins/messageLogger/HistoryModal.tsx +++ b/src/plugins/messageLogger/HistoryModal.tsx @@ -4,14 +4,15 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; +import { TooltipContainer } from "@components/TooltipContainer"; +import { classNameFactory } from "@utils/css"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy } from "@webpack"; -import { TabBar, Timestamp, TooltipContainer, useState } from "@webpack/common"; +import { TabBar, Timestamp, useState } from "@webpack/common"; import { parseEditContent } from "."; diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 9c9306469..b5f2c96a5 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -11,6 +11,7 @@ import { NavContextMenuPatchCallback, } from "@api/ContextMenu"; import { updateMessage } from "@api/MessageUpdater"; +import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -110,7 +111,7 @@ const patchMessageContextMenu: NavContextMenuPatchCallback = ( let label; - if (!Vencord.Plugins.isPluginEnabled("MessageLoggerEnhanced")) { + if (!isPluginEnabled("MessageLoggerEnhanced")) { label = "Remove Message History"; } else { label = "Remove Message (Temporary)"; diff --git a/src/plugins/moreQuickReactions/README.md b/src/plugins/moreQuickReactions/README.md new file mode 100644 index 000000000..fb94a9db9 --- /dev/null +++ b/src/plugins/moreQuickReactions/README.md @@ -0,0 +1,7 @@ +# MoreQuickReactions + +Increases the number of reactions available in the Quick React hover menu. + +You can change the number of reactions shown in the plugin settings. + +![](https://github.com/user-attachments/assets/957defe3-509f-4e1c-8dd8-b1bdfdf74172) diff --git a/src/plugins/moreQuickReactions/index.ts b/src/plugins/moreQuickReactions/index.ts new file mode 100644 index 000000000..9b8e7b761 --- /dev/null +++ b/src/plugins/moreQuickReactions/index.ts @@ -0,0 +1,38 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + reactionCount: { + description: "Number of reactions (0-42)", + type: OptionType.NUMBER, + default: 5 + }, +}); + +export default definePlugin({ + name: "MoreQuickReactions", + description: "Increases the number of reactions available in the Quick React hover menu", + authors: [Devs.iamme], + settings, + + get reactionCount() { + return settings.store.reactionCount; + }, + + patches: [ + { + find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", + replacement: { + match: /(?<=length>=3\?.{0,40})\.slice\(0,3\)/, + replace: ".slice(0,$self.reactionCount)" + } + } + ], +}); diff --git a/src/plugins/oneko/index.ts b/src/plugins/oneko/index.ts deleted file mode 100644 index a8e844f8d..000000000 --- a/src/plugins/oneko/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings, migratePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - - -const settings = definePluginSettings({ - speed: { - description: "Speed of Da Cat :3", - type: OptionType.NUMBER, - default: 10, - isValid: (value: number) => { - if (value && value < 0) return "The number must be bigger than 0"; - return true; - }, - onChange: () => { - // note: cant call the start() function from here. so i just copy pasted it (This was pointed out in the last commit i made. So this is to just clear stuff up for any future devs that work on this :D ) - if (Vencord.Plugins.isPluginEnabled("Oneko")) { - document.getElementById("oneko")?.remove(); - fetch("https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.js") - .then(x => x.text()) - .then(s => s.replace("const nekoSpeed = 10;", `const nekoSpeed = ${settings.store.speed};`)) - .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.gif") - .replace("(isReducedMotion)", "(false)")) - .then(eval); - } - return; - } - } -}); - -migratePluginSettings("Oneko", "oneko"); -export default definePlugin({ - name: "Oneko", - description: "cat follow mouse (real)", - // Listing adryd here because this literally just evals her script - authors: [Devs.Ven, Devs.adryd], - settings, - - start() { - fetch("https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.js") - .then(x => x.text()) - .then(s => s.replace("const nekoSpeed = 10;", `const nekoSpeed = ${settings.store.speed};`)) - .then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/c4ee66353b11a44e4a5b7e914a81f8d33111555e/oneko.gif") - .replace("(isReducedMotion)", "(false)")) - .then(eval); - }, - - stop() { - document.getElementById("oneko")?.remove(); - } -}); diff --git a/src/plugins/openInApp/index.ts b/src/plugins/openInApp/index.ts index 5dcca372f..05e5b1c29 100644 --- a/src/plugins/openInApp/index.ts +++ b/src/plugins/openInApp/index.ts @@ -52,7 +52,7 @@ const UrlReplacementRules: Record = { description: "Open Epic Games links in the Epic Games Launcher", }, tidal: { - match: /^https:\/\/tidal\.com\/(?:browse\/)?(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/, + match: /^https:\/\/(?:listen\.)?tidal\.com\/(?:browse\/)?(track|album|artist|playlist|user|video|mix)\/([a-f0-9-]+).*/, replace: (_, type, id) => `tidal://${type}/${id}`, description: "Open Tidal links in the Tidal app", }, @@ -61,6 +61,11 @@ const UrlReplacementRules: Record = { replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`, description: "Open Apple Music links in the iTunes app" }, + vrcx: { + match: /^https:\/\/vrchat.com\/home\/(user|avatar|world|group)\/(.+)$/, + replace: (_, type, id) => `vrcx://${type}/${id}`, + description: "Open VRChat links in the VRCX app" + } }; const pluginSettings = definePluginSettings( @@ -74,7 +79,6 @@ const pluginSettings = definePluginSettings( }, {} as SettingsDefinition) ); - const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative; export default definePlugin({ diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx index c78416ed4..22fe73151 100644 --- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -24,27 +24,14 @@ import { cl, getGuildPermissionSpecMap } from "@plugins/permissionsViewer/utils" import { copyToClipboard } from "@utils/clipboard"; import { getIntlMessage, getUniqueUsername } from "@utils/discord"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Guild, Role, UnicodeEmoji, User } from "@vencord/discord-types"; +import { Guild, Role, RoleOrUserPermission, UnicodeEmoji, User } from "@vencord/discord-types"; +import { PermissionOverwriteType } from "@vencord/discord-types/enums"; import { findByCodeLazy } from "@webpack"; import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common"; import { settings } from ".."; import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons"; -export const enum PermissionType { - Role = 0, - User = 1, - Owner = 2 -} - -export interface RoleOrUserPermission { - type: PermissionType; - id?: string; - permissions?: bigint; - overwriteAllow?: bigint; - overwriteDeny?: bigint; -} - type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; }; const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji"); @@ -72,7 +59,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea useEffect(() => { const usersToRequest = permissions - .filter(p => p.type === PermissionType.User && !GuildMemberStore.isMember(guild.id, p.id!)) + .filter(p => p.type === PermissionOverwriteType.MEMBER && !GuildMemberStore.isMember(guild.id, p.id!)) .map(({ id }) => id); FluxDispatcher.dispatch({ @@ -123,7 +110,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
    { - if (permission.type === PermissionType.Role) + if (permission.type === PermissionOverwriteType.ROLE) ContextMenuApi.openContextMenu(e, () => ( )); - else if (permission.type === PermissionType.User) { + else if (permission.type === PermissionOverwriteType.MEMBER) { ContextMenuApi.openContextMenu(e, () => ( - {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( + {(permission.type === PermissionOverwriteType.ROLE || permission.type === PermissionOverwriteType.OWNER) && ( )} - {permission.type === PermissionType.Role && roleIconSrc != null && ( + {permission.type === PermissionOverwriteType.ROLE && roleIconSrc != null && ( )} - {permission.type === PermissionType.User && user != null && ( + {permission.type === PermissionOverwriteType.MEMBER && user != null && ( { - permission.type === PermissionType.Role + permission.type === PermissionOverwriteType.ROLE ? role?.name ?? "Unknown Role" - : permission.type === PermissionType.User + : permission.type === PermissionOverwriteType.MEMBER ? (user != null && getUniqueUsername(user)) ?? "Unknown User" : ( diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index ca868473b..b508f5eca 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -22,12 +22,13 @@ import { HeadingTertiary } from "@components/Heading"; import { cl, getGuildPermissionSpecMap, getSortedRolesForMember, sortUserRoles } from "@plugins/permissionsViewer/utils"; import { getIntlMessage } from "@utils/discord"; import { classes } from "@utils/misc"; -import type { Guild, GuildMember } from "@vencord/discord-types"; +import type { Guild, GuildMember, RoleOrUserPermission } from "@vencord/discord-types"; +import { PermissionOverwriteType } from "@vencord/discord-types/enums"; import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { PermissionsBits, Tooltip, useMemo, UserStore } from "@webpack/common"; import { PermissionsSortOrder, settings } from ".."; -import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./RolesAndUsersPermissions"; +import openRolesAndUsersPermissionsModal from "./RolesAndUsersPermissions"; interface UserPermission { permission: string; @@ -100,13 +101,13 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild: const userRoles = getSortedRolesForMember(guild, guildMember); const rolePermissions: Array = userRoles.map(role => ({ - type: PermissionType.Role, + type: PermissionOverwriteType.ROLE, ...role })); if (guild.ownerId === guildMember.userId) { rolePermissions.push({ - type: PermissionType.Owner, + type: PermissionOverwriteType.OWNER, permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n) }); diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 5a0ca71f1..f2e968fc6 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -22,19 +22,20 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { SafetyIcon } from "@components/Icons"; +import { TooltipContainer } from "@components/TooltipContainer"; import { Devs } from "@utils/constants"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; -import type { Guild, GuildMember } from "@vencord/discord-types"; +import { type Guild, type GuildMember, RoleOrUserPermission } from "@vencord/discord-types"; +import { PermissionOverwriteType } from "@vencord/discord-types/enums"; import { findByPropsLazy } from "@webpack"; -import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common"; +import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useRef, UserStore } from "@webpack/common"; -import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; +import openRolesAndUsersPermissionsModal from "./components/RolesAndUsersPermissions"; import UserPermissions from "./components/UserPermissions"; import { getSortedRolesForMember, sortPermissionOverwrites } from "./utils"; const PopoutClasses = findByPropsLazy("container", "scroller", "list"); -const RoleButtonClasses = findByPropsLazy("button", "icon"); export const enum PermissionsSortOrder { HighestRole, @@ -75,13 +76,13 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { const permissions: RoleOrUserPermission[] = getSortedRolesForMember(guild, member) .map(role => ({ - type: PermissionType.Role, + type: PermissionOverwriteType.ROLE, ...role })); if (guild.ownerId === id) { permissions.push({ - type: PermissionType.Owner, + type: PermissionOverwriteType.OWNER, permissions: Object.values(PermissionsBits).reduce((prev, curr) => prev | curr, 0n) }); } @@ -95,7 +96,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { const channel = ChannelStore.getChannel(id!); const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ - type: type as PermissionType, + type, id, overwriteAllow: allow, overwriteDeny: deny @@ -108,7 +109,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { }) .otherwise(() => { const permissions = GuildRoleStore.getSortedRoles(guild.id).map(role => ({ - type: PermissionType.Role, + type: PermissionOverwriteType.ROLE, ...role })); @@ -143,7 +144,6 @@ function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentT .with(MenuItemParentType.Guild, () => MenuItem(props.guild.id)) .otherwise(() => null); - if (item == null) return; if (group) { diff --git a/src/plugins/permissionsViewer/utils.ts b/src/plugins/permissionsViewer/utils.ts index c3f616727..3e07e82d6 100644 --- a/src/plugins/permissionsViewer/utils.ts +++ b/src/plugins/permissionsViewer/utils.ts @@ -16,14 +16,13 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { Guild, GuildMember, Role } from "@vencord/discord-types"; +import { PermissionOverwriteType } from "@vencord/discord-types/enums"; import { findByPropsLazy } from "@webpack"; import { GuildRoleStore } from "@webpack/common"; import { PermissionsSortOrder, settings } from "."; -import { PermissionType } from "./components/RolesAndUsersPermissions"; - export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermissionSpecMap"); export const cl = classNameFactory("vc-permviewer-"); @@ -50,7 +49,7 @@ export function sortPermissionOverwrites { - if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0; + if (a.type !== PermissionOverwriteType.ROLE || b.type !== PermissionOverwriteType.ROLE) return 0; const roleA = roles[a.id]; const roleB = roles[b.id]; diff --git a/src/plugins/petpet/index.ts b/src/plugins/petpet/index.ts index fce82547a..08cecaeec 100644 --- a/src/plugins/petpet/index.ts +++ b/src/plugins/petpet/index.ts @@ -23,7 +23,7 @@ import { makeLazy } from "@utils/lazy"; import definePlugin from "@utils/types"; import { CommandArgument, CommandContext } from "@vencord/discord-types"; import { DraftType, UploadAttachmentStore, UploadHandler, UploadManager, UserUtils } from "@webpack/common"; -import { applyPalette, GIFEncoder, quantize } from "gifenc"; +import { GIFEncoder, nearestColorIndex, quantize } from "gifenc"; const DEFAULT_DELAY = 20; const DEFAULT_RESOLUTION = 128; @@ -47,7 +47,7 @@ function loadImage(source: File | string) { URL.revokeObjectURL(url); resolve(img); }; - img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); + img.onerror = _event => reject(Error(`An error occurred while loading ${url}. Check the console for more info.`)); img.crossOrigin = "Anonymous"; img.src = url; }); @@ -83,11 +83,34 @@ async function resolveImage(options: CommandArgument[], ctx: CommandContext, noS return null; } +function rgb888_to_rgb565(r: number, g: number, b: number): number { + return ((r << 8) & 0xf800) | ((g << 3) & 0x07e0) | (b >> 3); +} + +function applyPaletteTransparent(data: Uint8Array | Uint8ClampedArray, palette: number[][], cache: number[], threshold: number): Uint8Array { + const index = new Uint8Array(Math.floor(data.length / 4)); + + for (let i = 0; i < index.length; i += 1) { + const r = data[4 * i]; + const g = data[4 * i + 1]; + const b = data[4 * i + 2]; + const a = data[4 * i + 3]; + + if (a < threshold) { + index[i] = 255; + } else { + const key = rgb888_to_rgb565(r, g, b); + index[i] = key in cache ? cache[key] : (cache[key] = nearestColorIndex(palette, [r, g, b])); + } + } + return index; +} + migratePluginSettings("PetPet", "petpet"); export default definePlugin({ name: "PetPet", description: "Adds a /petpet slash command to create headpet gifs from any image", - authors: [Devs.Ven], + authors: [Devs.Ven, Devs.u32], commands: [ { inputType: ApplicationCommandInputType.BUILT_IN, @@ -96,7 +119,7 @@ export default definePlugin({ options: [ { name: "delay", - description: "The delay between each frame. Defaults to 20.", + description: "The delay between each frame in ms. Rounded to nearest 10ms. Defaults to the minimum value of 20.", type: ApplicationCommandOptionType.INTEGER }, { @@ -143,16 +166,32 @@ export default definePlugin({ const avatar = await loadImage(url); const delay = findOption(opts, "delay", DEFAULT_DELAY); + // Frame delays < 20ms don't function correctly on chromium and firefox + if (delay < 20) return sendBotMessage(cmdCtx.channel.id, { content: "Delay must be at least 20." }); + const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION); const gif = GIFEncoder(); + const paletteImageSize = Math.min(120, resolution); + const canvas = document.createElement("canvas"); - canvas.width = canvas.height = resolution; - const ctx = canvas.getContext("2d")!; + canvas.width = resolution; + // Ensure there is sufficient space for the palette generation image + canvas.height = Math.max(resolution, 2 * paletteImageSize); + + const ctx = canvas.getContext("2d", { willReadFrequently: true })!; UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); + // Generate palette from an image where hand and avatar are fully visible + ctx.drawImage(avatar, 0, paletteImageSize, 0.8 * paletteImageSize, 0.8 * paletteImageSize); + ctx.drawImage(frames[0], 0, 0, paletteImageSize, paletteImageSize); + const { data } = ctx.getImageData(0, 0, paletteImageSize, 2 * paletteImageSize); + const palette = quantize(data, 255); + + const cache = new Array(2 ** 16); + for (let i = 0; i < FRAMES; i++) { ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -166,13 +205,13 @@ export default definePlugin({ ctx.drawImage(frames[i], 0, 0, resolution, resolution); const { data } = ctx.getImageData(0, 0, resolution, resolution); - const palette = quantize(data, 256); - const index = applyPalette(data, palette); + const index = applyPaletteTransparent(data, palette, cache, 1); gif.writeFrame(index, resolution, resolution, { transparent: true, - palette, + transparentIndex: 255, delay, + palette: i === 0 ? palette : undefined, }); } diff --git a/src/plugins/pinDms/components/CreateCategoryModal.tsx b/src/plugins/pinDms/components/CreateCategoryModal.tsx index 1bda1e8a4..b35d2cd6c 100644 --- a/src/plugins/pinDms/components/CreateCategoryModal.tsx +++ b/src/plugins/pinDms/components/CreateCategoryModal.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import { Divider } from "@components/Divider"; import { Heading } from "@components/Heading"; import { DEFAULT_COLOR, SWATCHES } from "@plugins/pinDms/constants"; import { categoryLen, createCategory, getCategory } from "@plugins/pinDms/data"; +import { classNameFactory } from "@utils/css"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack"; import { Button, ColorPicker, TextInput, Toasts, useMemo, useState } from "@webpack/common"; diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts index 7671b54f3..4c21f68c7 100644 --- a/src/plugins/pinDms/data.ts +++ b/src/plugins/pinDms/data.ts @@ -127,7 +127,6 @@ export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) return canMoveArrayInDirection(category.channels, channelIndex, direction); }; - function swapElementsInArray(array: any[], index1: number, index2: number) { if (!array[index1] || !array[index2]) return; [array[index1], array[index2]] = [array[index2], array[index1]]; diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index a0832518c..adaf2b8b3 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -124,7 +124,6 @@ export default definePlugin({ ] }, - // forceUpdate moment // https://regex101.com/r/kDN9fO/1 { @@ -306,7 +305,6 @@ export default definePlugin({ action={() => removeCategory(category.id)} /> - )); }} diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index 17d44cd45..28d0ed4d8 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -81,8 +81,7 @@ const Icons = { mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }), embedded: Icon("M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z", { viewBox: "0 0 24 24", height: 20, width: 20 }), suncord: Icon("M7 4a6 6 0 00-6 6v4a6 6 0 006 6h10a6 6 0 006-6v-4a6 6 0 00-6-6H7zm0 11a1 1 0 01-1-1v-1H5a1 1 0 010-2h1v-1a1 1 0 012 0v1h1a1 1 0 010 2H8v1a1 1 0 01-1 1zm10-4a1 1 0 100-2 1 1 0 000 2zm1 3a1 1 0 11-2 0 1 1 0 012 0zm0-2a1 1 0 102 0 1 1 0 00-2 0zm-3 1a1 1 0 110-2 1 1 0 010 2z", { viewBox: "0 0 24 24", height: 24, width: 24 }), - newVencord: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), - oldVencord: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), + vencord: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), }; type Platform = keyof typeof Icons; @@ -100,9 +99,6 @@ const PlatformIcon = ({ platform, status, small }: PlatformIconProps) => { : platform[0].toUpperCase() + platform.slice(1); let Icon = Icons[platform] ?? Icons.desktop; const { ConsoleIcon } = settings.store; - if (platform === "embedded" && ConsoleIcon === "oldVencord") { - Icon = Icons.oldVencord; - } if (platform === "embedded") { switch (ConsoleIcon) { case "equicord": @@ -111,11 +107,8 @@ const PlatformIcon = ({ platform, status, small }: PlatformIconProps) => { case "suncord": Icon = Icons.suncord; break; - case "newVencord": - Icon = Icons.newVencord; - break; - case "oldVencord": - Icon = Icons.oldVencord; + case "vencord": + Icon = Icons.vencord; break; default: Icon = Icons.embedded; @@ -233,12 +226,8 @@ const settings = definePluginSettings({ value: "suncord", }, { - label: "Old Vencord Icon", - value: "newVencord", - }, - { - label: "Old Vencord Icon", - value: "oldVencord", + label: "Vencord", + value: "vencord", }, ], } diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index d2a6a12fc..491a0d509 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -25,7 +25,6 @@ import { DraftStore, DraftType, UploadAttachmentStore, UserStore, useStateFromSt const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); - const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> => new Promise(res => { const img = new Image(); @@ -38,7 +37,6 @@ const getImageBox = (url: string): Promise<{ width: number, height: number; } | img.src = url; }); - const getAttachments = async (channelId: string) => await Promise.all( UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage) @@ -69,7 +67,6 @@ const getAttachments = async (channelId: string) => }) ); - const PreviewIcon: IconComponent = ({ height = 20, width = 20, className }) => { return ( ; -type ThreadsJoinedByParent = Record; - -interface ActiveJoinedThreadsStore { - getActiveJoinedThreadsForGuild(guildId: string): ThreadsJoinedByParent; -} - -const ActiveJoinedThreadsStore: ActiveJoinedThreadsStore = findStoreLazy("ActiveJoinedThreadsStore"); +import { ActiveJoinedThreadsStore, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common"; function onClick() { const channels: Array = []; Object.values(GuildStore.getGuilds()).forEach(guild => { - GuildChannelStore.getChannels(guild.id).SELECTABLE // Array<{ channel, comparator }> - .concat(GuildChannelStore.getChannels(guild.id).VOCAL) // Array<{ channel, comparator }> + GuildChannelStore.getChannels(guild.id).SELECTABLE + .concat(GuildChannelStore.getChannels(guild.id).VOCAL) .concat( Object.values(ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id)) .flatMap(threadChannels => Object.values(threadChannels)) diff --git a/src/plugins/relationshipNotifier/functions.ts b/src/plugins/relationshipNotifier/functions.ts index 19f360866..da7838b9c 100644 --- a/src/plugins/relationshipNotifier/functions.ts +++ b/src/plugins/relationshipNotifier/functions.ts @@ -17,11 +17,11 @@ */ import { getUniqueUsername, openUserProfile } from "@utils/discord"; -import { ChannelType } from "@vencord/discord-types/enums"; +import { ChannelType, RelationshipType } from "@vencord/discord-types/enums"; import { UserUtils } from "@webpack/common"; import settings from "./settings"; -import { ChannelDelete, GuildDelete, RelationshipRemove, RelationshipType } from "./types"; +import { ChannelDelete, GuildDelete, RelationshipRemove } from "./types"; import { deleteGroup, deleteGuild, getGroup, getGuild, GuildAvailabilityStore, notify } from "./utils"; let manuallyRemovedFriend: string | undefined; diff --git a/src/plugins/relationshipNotifier/types.ts b/src/plugins/relationshipNotifier/types.ts index 5d3d12d46..914f1a43b 100644 --- a/src/plugins/relationshipNotifier/types.ts +++ b/src/plugins/relationshipNotifier/types.ts @@ -51,10 +51,3 @@ export interface SimpleGuild { name: string; iconURL?: string; } - -export const enum RelationshipType { - FRIEND = 1, - BLOCKED = 2, - INCOMING_REQUEST = 3, - OUTGOING_REQUEST = 4, -} diff --git a/src/plugins/relationshipNotifier/utils.ts b/src/plugins/relationshipNotifier/utils.ts index 206cb63c7..24c7cdefc 100644 --- a/src/plugins/relationshipNotifier/utils.ts +++ b/src/plugins/relationshipNotifier/utils.ts @@ -21,12 +21,12 @@ import { popNotice, showNotice } from "@api/Notices"; import { showNotification } from "@api/Notifications"; import { getUniqueUsername, openUserProfile } from "@utils/discord"; import { FluxStore } from "@vencord/discord-types"; -import { ChannelType } from "@vencord/discord-types/enums"; +import { ChannelType, RelationshipType } from "@vencord/discord-types/enums"; import { findStoreLazy } from "@webpack"; import { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common"; import settings from "./settings"; -import { RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types"; +import { SimpleGroupChannel, SimpleGuild } from "./types"; export const GuildAvailabilityStore = findStoreLazy("GuildAvailabilityStore") as FluxStore & { totalGuilds: number; diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 799fa9ba5..8b690b7ea 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -122,7 +122,6 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId, type }: { refe ); } - export function ReviewsInputComponent( { discordId, isAuthor, refetch, name, modalKey, repliesTo }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; modalKey?: string; repliesTo?: number; } ) { diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index fa5d9142b..d5d13d282 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -21,12 +21,13 @@ import "./style.css"; import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import ErrorBoundary from "@components/ErrorBoundary"; import { NotesIcon, OpenExternalIcon } from "@components/Icons"; +import { TooltipContainer } from "@components/TooltipContainer"; import { Devs } from "@utils/constants"; import { classes } from "@utils/misc"; import definePlugin from "@utils/types"; import { Guild, User } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; -import { Alerts, Clickable, Menu, Parser, TooltipContainer } from "@webpack/common"; +import { Alerts, Clickable, Menu, Parser } from "@webpack/common"; import { Auth, initAuth, updateAuth } from "./auth"; import { openReviewsModal } from "./components/ReviewModal"; diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx index daef3993c..54606c9b1 100644 --- a/src/plugins/reviewDB/settings.tsx +++ b/src/plugins/reviewDB/settings.tsx @@ -78,7 +78,6 @@ export const settings = definePluginSettings({ ReviewDB website -
    ); }; - diff --git a/src/plugins/shikiCodeblocks.desktop/utils/misc.ts b/src/plugins/shikiCodeblocks.desktop/utils/misc.ts index ee782ec05..1b2fd27c5 100644 --- a/src/plugins/shikiCodeblocks.desktop/utils/misc.ts +++ b/src/plugins/shikiCodeblocks.desktop/utils/misc.ts @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; import { resolveLang } from "@plugins/shikiCodeblocks.desktop/api/languages"; import { HighlighterProps } from "@plugins/shikiCodeblocks.desktop/components/Highlighter"; import { HljsSetting } from "@plugins/shikiCodeblocks.desktop/types"; +import { classNameFactory } from "@utils/css"; import { hljs } from "@webpack/common"; export const cl = classNameFactory("vc-shiki-"); diff --git a/src/plugins/showAllMessageButtons/index.ts b/src/plugins/showAllMessageButtons/index.ts index 10069a583..2d1f4bcb1 100644 --- a/src/plugins/showAllMessageButtons/index.ts +++ b/src/plugins/showAllMessageButtons/index.ts @@ -7,8 +7,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { MessageActions } from "@webpack/common"; +import { MessageActions, PinActions } from "@webpack/common"; const settings = definePluginSettings({ noShiftDelete: { @@ -23,8 +22,6 @@ const settings = definePluginSettings({ }, }); -const PinActions = findByPropsLazy("pinMessage", "unpinMessage"); - export default definePlugin({ name: "ShowAllMessageButtons", description: "Always show all message buttons no matter if you are holding the shift key or not.", @@ -57,8 +54,7 @@ export default definePlugin({ MessageActions.deleteMessage(channel_id, id); }, toggleMessagePin({ channel, message }) { - if (message.pinned) - return PinActions.unpinMessage(channel, message.id); + if (message.pinned) return PinActions.unpinMessage(channel, message.id); PinActions.pinMessage(channel, message.id); }, diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx index 645682752..4579df18c 100644 --- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -21,11 +21,11 @@ import { Settings } from "@api/Settings"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import PermissionsViewerPlugin from "@plugins/permissionsViewer"; -import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "@plugins/permissionsViewer/components/RolesAndUsersPermissions"; +import openRolesAndUsersPermissionsModal from "@plugins/permissionsViewer/components/RolesAndUsersPermissions"; import { sortPermissionOverwrites } from "@plugins/permissionsViewer/utils"; import { classes } from "@utils/misc"; import { formatDuration } from "@utils/text"; -import type { Channel } from "@vencord/discord-types"; +import type { Channel, RoleOrUserPermission } from "@vencord/discord-types"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Timestamp, Tooltip, useEffect, useState } from "@webpack/common"; @@ -47,22 +47,6 @@ interface DefaultReaction { emojiName: string | null; } -interface Tag { - id: string; - name: string; - emojiId: string | null; - emojiName: string | null; - moderated: boolean; -} - -interface ExtendedChannel extends Channel { - defaultThreadRateLimitPerUser?: number; - defaultSortOrder?: SortOrderTypes | null; - defaultForumLayout?: ForumLayoutTypes; - defaultReactionEmoji?: DefaultReaction | null; - availableTags?: Array; -} - const enum ChannelTypes { GUILD_TEXT = 0, GUILD_VOICE = 2, @@ -81,7 +65,6 @@ const enum ChannelFlags { REQUIRE_TAG = 1 << 4 } - const ChatScrollClasses = findByPropsLazy("auto", "managedReactiveScroller"); const ChatClasses = findByPropsLazy("chat", "content", "noChat", "chatContent"); const ChannelBeginHeader = findComponentByCodeLazy("#{intl::ROLE_REQUIRED_SINGLE_USER_MESSAGE}"); @@ -117,7 +100,7 @@ const VideoQualityModesToNames = { // Icon from the modal when clicking a message link you don't have access to view const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg"; -function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { +function HiddenChannelLockScreen({ channel }: { channel: Channel; }) { const { defaultAllowedUsersAndRolesDropdownState } = settings.use(["defaultAllowedUsersAndRolesDropdownState"]); const [permissions, setPermissions] = useState([]); @@ -163,7 +146,7 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { if (Settings.plugins.PermissionsViewer.enabled) { setPermissions(sortPermissionOverwrites(Object.values(permissionOverwrites).map(overwrite => ({ - type: overwrite.type as PermissionType, + type: overwrite.type, id: overwrite.id, overwriteAllow: overwrite.allow, overwriteDeny: overwrite.deny diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index b26dca9c9..da6ac2739 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -19,9 +19,9 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import type { Channel, Role } from "@vencord/discord-types"; @@ -504,7 +504,6 @@ export default definePlugin({ } ], - swapViewChannelWithConnectPermission(mergedPermissions: bigint, channel: Channel) { if (!PermissionStore.can(PermissionsBits.CONNECT, channel)) { mergedPermissions &= ~PermissionsBits.VIEW_CHANNEL; diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 29e998867..7c798952c 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -969,7 +969,7 @@ export default definePlugin({ // Tags *should* contain the guild ID nested in its structure, but on the first time // loading a guild member's preview profile, it will be undefined. This patch bypasses // that by passing the guild ID as its own prop. - find: ".hasAvatarForGuild(null==", + find: "\"UserProfilePopoutBody\"}", replacement: { match: /(pronouns,tags:)/, replace: "pronouns,guildId:arguments[0]?.guild?.id??null,tags:" @@ -977,7 +977,7 @@ export default definePlugin({ }, { // Same as above, but for bot members. - find: "BotUserProfilePopoutBody\"}", + find: "popularApplicationCommandIds)!=null&&null", replacement: { match: /(pronouns,tags:)/, replace: "pronouns,guildId:arguments[0]?.guild?.id??null,tags:" diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx index 03de52284..bcc5cd760 100644 --- a/src/plugins/showTimeoutDuration/index.tsx +++ b/src/plugins/showTimeoutDuration/index.tsx @@ -9,13 +9,14 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; +import { TooltipContainer } from "@components/TooltipContainer"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import { canonicalizeMatch } from "@utils/patches"; import definePlugin, { OptionType } from "@utils/types"; import { Message } from "@vencord/discord-types"; import { findComponentLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, TooltipContainer } from "@webpack/common"; +import { ChannelStore, GuildMemberStore } from "@webpack/common"; import { ReactNode } from "react"; const countDownFilter = canonicalizeMatch(/#{intl::MAX_AGE_NEVER}/); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 8637889ac..25171131a 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -19,6 +19,7 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { plugins } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import { openPluginModal } from "@components/index"; import { Devs, EquicordDevs } from "@utils/constants"; @@ -162,7 +163,7 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { channel.guild_id ? toggleLocation(channel.guild_id, effectiveList, defaultHidden) : null; break; case "settings": - openPluginModal(Vencord.Plugins.plugins.SilentTyping); + openPluginModal(plugins.SilentTyping); break; } } @@ -341,96 +342,98 @@ export default definePlugin({ }, ], - commands: [{ - name: "silent-typing", - description: "Hide your typing indicator from chat.", - inputType: ApplicationCommandInputType.BUILT_IN, + commands: [ + { + name: "silent-typing", + description: "Hide your typing indicator from chat.", + inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "toggle", - description: "Toggle functionality globally, for the channel, or for the guild.", - required: false, - type: ApplicationCommandOptionType.STRING, - choices: [ - { name: "Global", label: "Global", value: "global" }, - { name: "Channel", label: "Channel", value: "channel" }, - { name: "Guild", label: "Guild", value: "guild" }, - ] - }, - { - name: "chat-bar-indicators", - description: "Hide other users' typing indicators from above the chat bar.", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - { - name: "members-list-indicators", - description: "Hide other users' typing indicators from the members list.", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - { - name: "chat-icon", - description: "Show an icon in the chat bar for toggling the plugin on the go.", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - { - name: "chat-context-menu", - description: "Show a dropdown in the chat context menu to toggle plugin settings on the go.", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - { - name: "default-hidden", - description: "Whether to hide typing in DMs/channels/guilds by default or not.", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - } - ], - - execute: async (args, ctx) => { - let updated = false; - const location = findOption(args, "toggle"); - - if (typeof location === "string") { - updated = true; - - if (location === "global") { - toggleGlobal(); - } else { - const locationId = location === "guild" ? ctx.channel.guild_id : ctx.channel.id; - toggleLocation(locationId, getEffectiveList(), settings.store.defaultHidden); + options: [ + { + name: "toggle", + description: "Toggle functionality globally, for the channel, or for the guild.", + required: false, + type: ApplicationCommandOptionType.STRING, + choices: [ + { name: "Global", label: "Global", value: "global" }, + { name: "Channel", label: "Channel", value: "channel" }, + { name: "Guild", label: "Guild", value: "guild" }, + ] + }, + { + name: "chat-bar-indicators", + description: "Hide other users' typing indicators from above the chat bar.", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + { + name: "members-list-indicators", + description: "Hide other users' typing indicators from the members list.", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + { + name: "chat-icon", + description: "Show an icon in the chat bar for toggling the plugin on the go.", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + { + name: "chat-context-menu", + description: "Show a dropdown in the chat context menu to toggle plugin settings on the go.", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + { + name: "default-hidden", + description: "Whether to hide typing in DMs/channels/guilds by default or not.", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, } - } + ], - const updateChatIcon = findOption(args, "chat-icon"); + execute: async (args, ctx) => { + let updated = false; + const location = findOption(args, "toggle"); - if (typeof updateChatIcon === "boolean") { - updated = true; - settings.store.chatIcon = !!updateChatIcon; - } + if (typeof location === "string") { + updated = true; - const updateChatContextMenu = findOption(args, "chat-context-menu"); + if (location === "global") { + toggleGlobal(); + } else { + const locationId = location === "guild" ? ctx.channel.guild_id : ctx.channel.id; + toggleLocation(locationId, getEffectiveList(), settings.store.defaultHidden); + } + } - if (typeof updateChatContextMenu === "boolean") { - updated = true; - settings.store.chatContextMenu = !!updateChatContextMenu; - } + const updateChatIcon = findOption(args, "chat-icon"); - const updateDefaultHidden = findOption(args, "default-hidden"); + if (typeof updateChatIcon === "boolean") { + updated = true; + settings.store.chatIcon = !!updateChatIcon; + } - if (typeof updateDefaultHidden === "boolean") { - updated = true; - settings.store.defaultHidden = !!updateDefaultHidden; - } + const updateChatContextMenu = findOption(args, "chat-context-menu"); - sendBotMessage(ctx.channel.id, { - content: updated ? "Silent typing settings updated." : "No changes made to silent typing settings.", - }); - }, - }], + if (typeof updateChatContextMenu === "boolean") { + updated = true; + settings.store.chatContextMenu = !!updateChatContextMenu; + } + + const updateDefaultHidden = findOption(args, "default-hidden"); + + if (typeof updateDefaultHidden === "boolean") { + updated = true; + settings.store.defaultHidden = !!updateDefaultHidden; + } + + sendBotMessage(ctx.channel.id, { + content: updated ? "Silent typing settings updated." : "No changes made to silent typing settings.", + }); + }, + } + ], async startTyping(channelId: string) { if (checkEnabled(channelId)) return; diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 7f53c5702..9df269a12 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -19,14 +19,15 @@ import "./styles.css"; import { definePluginSettings, migratePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; +import { TooltipContainer } from "@components/TooltipContainer"; import { Devs, EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; import { User } from "@vencord/discord-types"; -import { DateUtils, RelationshipStore, TooltipContainer } from "@webpack/common"; +import { DateUtils, RelationshipStore } from "@webpack/common"; import { PropsWithChildren } from "react"; const formatter = new Intl.DateTimeFormat(undefined, { diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts index beb108af8..2f45a040f 100644 --- a/src/plugins/themeAttributes/index.ts +++ b/src/plugins/themeAttributes/index.ts @@ -10,7 +10,6 @@ import definePlugin from "@utils/types"; import { Message } from "@vencord/discord-types"; import { UserStore } from "@webpack/common"; - export default definePlugin({ name: "ThemeAttributes", description: "Adds data attributes to various elements for theming purposes", diff --git a/src/plugins/translate/TranslateModal.tsx b/src/plugins/translate/TranslateModal.tsx index 32796ba9b..d03097080 100644 --- a/src/plugins/translate/TranslateModal.tsx +++ b/src/plugins/translate/TranslateModal.tsx @@ -73,7 +73,6 @@ function AutoTranslateToggle() { ); } - export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) { return ( diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index be9c5d279..d5d36c121 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -49,7 +49,6 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { m )); }; - function getMessageContent(message: Message) { // Message snapshots is an array, which allows for nested snapshots, which Discord does not do yet. // no point collecting content or rewriting this to render in a certain way that makes sense diff --git a/src/plugins/translate/utils.ts b/src/plugins/translate/utils.ts index 9bb175adf..4ee16c93e 100644 --- a/src/plugins/translate/utils.ts +++ b/src/plugins/translate/utils.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { classNameFactory } from "@api/Styles"; +import { classNameFactory } from "@utils/css"; import { onlyOnce } from "@utils/onlyOnce"; import { PluginNative } from "@utils/types"; import { showToast, Toasts } from "@webpack/common"; diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 50b50c2c0..6c3f9a4c6 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -24,18 +24,16 @@ import { buildSeveralUsers } from "@plugins/typingTweaks"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common"; +import { findComponentByCodeLazy } from "@webpack"; +import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, TypingStore, UserGuildSettingsStore, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common"; const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:"); -const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); const enum IndicatorMode { Dots = 1 << 0, Avatars = 1 << 1 } - function getDisplayName(guildId: string, userId: string) { const user = UserStore.getUser(userId); return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username; diff --git a/src/plugins/typingTweaks/index.tsx b/src/plugins/typingTweaks/index.tsx index 7e3342ce6..caa72cd4c 100644 --- a/src/plugins/typingTweaks/index.tsx +++ b/src/plugins/typingTweaks/index.tsx @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings, migratePluginToSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { getCustomColorString } from "@equicordplugins/customUserColors"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { openUserProfile } from "@utils/discord"; import { isNonNullish } from "@utils/guards"; import { Logger } from "@utils/Logger"; @@ -45,6 +45,12 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true, description: "Show a more useful message when several users are typing" + }, + amITyping: { + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true, + description: "Shows you if other people can see you typing" } }); @@ -104,11 +110,13 @@ const TypingUser = ErrorBoundary.wrap(function TypingUser({ user, guildId }: Typ ); }, { noop: true }); +migratePluginToSettings(true, "TypingTweaks", "AmITyping", "amITyping"); export default definePlugin({ name: "TypingTweaks", description: "Show avatars and role colours in the typing indicator", - authors: [Devs.zt, Devs.sadan], + authors: [Devs.zt, Devs.sadan, EquicordDevs.MrDiamond], settings, + isModified: true, managedStyle, @@ -140,6 +148,14 @@ export default definePlugin({ predicate: () => settings.store.alternativeFormatting } ] + }, + { + find: "\"handleDismissInviteEducation\"", + predicate: () => settings.store.amITyping, + replacement: { + match: /\i\.default\.getCurrentUser\(\)/, + replace: "\"\"" + } } ], @@ -166,7 +182,6 @@ export default definePlugin({ } }, - buildSeveralUsers, renderTypingUsers: ErrorBoundary.wrap(({ guildId, users, children }: PropsWithChildren<{ guildId: string, users: User[]; }>) => { diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx index 9eecd86d7..9f4baeee3 100644 --- a/src/plugins/unsuppressEmbeds/index.tsx +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -23,7 +23,6 @@ import definePlugin from "@utils/types"; import { Channel, Message } from "@vencord/discord-types"; import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; - const EMBED_SUPPRESSED = 1 << 2; const messageContextMenuPatch: NavContextMenuPatchCallback = ( diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx index ebf9df9e1..1280bf9ec 100644 --- a/src/plugins/userVoiceShow/components.tsx +++ b/src/plugins/userVoiceShow/components.tsx @@ -5,10 +5,10 @@ */ import { isPluginEnabled } from "@api/PluginManager"; -import { classNameFactory } from "@api/Styles"; import { BaseText } from "@components/BaseText"; import ErrorBoundary from "@components/ErrorBoundary"; import ShowHiddenChannelsPlugin from "@plugins/showHiddenChannels"; +import { classNameFactory } from "@utils/css"; import { classes } from "@utils/misc"; import { Channel } from "@vencord/discord-types"; import { filters, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx index ad95a53b5..72e551c8b 100644 --- a/src/plugins/usrbg/index.tsx +++ b/src/plugins/usrbg/index.tsx @@ -19,9 +19,9 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; import { Button } from "@components/Button"; import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; const cl = classNameFactory("vc-usrbg-"); diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 44bee1e13..1944cf1b0 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -25,7 +25,6 @@ import definePlugin, { OptionType } from "@utils/types"; import type { Channel, Guild, User } from "@vencord/discord-types"; import { GuildMemberStore, IconUtils, Menu } from "@webpack/common"; - interface UserContextProps { channel: Channel; guildId?: string; diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 3a070b2cf..aad99622c 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -32,7 +32,6 @@ import definePlugin, { IconComponent, OptionType } from "@utils/types"; import { Message } from "@vencord/discord-types"; import { Button, ChannelStore, GuildRoleStore, Menu } from "@webpack/common"; - const CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => { return ( { socket.onopen = resolve; socket.onerror = reject; @@ -215,7 +215,6 @@ export default definePlugin({ titleString = `${message.author.username} (${guild.name}, #${channel.name})`; } - switch (channel.type) { case ChannelTypes.DM: titleString = message.author.username.trim(); @@ -249,7 +248,6 @@ export default definePlugin({ && e?.content_type.startsWith("image") ); - images.forEach(img => { finalMsg += ` [image: ${img.filename}] `; }); diff --git a/src/preload.ts b/src/preload.ts index 7c61a181d..5f7adb991 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -17,48 +17,25 @@ */ import { debounce } from "@shared/debounce"; -import { contextBridge, webFrame } from "electron"; -import { readFileSync, watch } from "fs"; -import { join } from "path"; +import { IpcEvents } from "@shared/IpcEvents"; +import { contextBridge, webFrame } from "electron/renderer"; -import VencordNative from "./VencordNative"; +import VencordNative, { invoke, sendSync } from "./VencordNative"; contextBridge.exposeInMainWorld("VencordNative", VencordNative); // Discord if (location.protocol !== "data:") { - // #region cssInsert - const rendererCss = join(__dirname, "renderer.css"); - - const style = document.createElement("style"); - style.id = "vencord-css-core"; - style.textContent = readFileSync(rendererCss, "utf-8"); - - if (document.readyState === "complete") { - document.documentElement.appendChild(style); - } else { - document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), { - once: true - }); - } - - if (IS_DEV) { - // persistent means keep process running if watcher is the only thing still running - // which we obviously don't want - watch(rendererCss, { persistent: false }, () => { - document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8"); - }); - } - // #endregion + invoke(IpcEvents.INIT_FILE_WATCHERS); if (IS_DISCORD_DESKTOP) { - webFrame.executeJavaScript(readFileSync(join(__dirname, "renderer.js"), "utf-8")); + webFrame.executeJavaScript(sendSync(IpcEvents.PRELOAD_GET_RENDERER_JS)); + // Not supported in sandboxed preload scripts but Discord doesn't support it either so who cares require(process.env.DISCORD_PRELOAD!); } } // Monaco popout else { contextBridge.exposeInMainWorld("setCss", debounce(VencordNative.quickCss.set)); contextBridge.exposeInMainWorld("getCurrentCss", VencordNative.quickCss.get); - // shrug - contextBridge.exposeInMainWorld("getTheme", () => "vs-dark"); + contextBridge.exposeInMainWorld("getTheme", VencordNative.quickCss.getEditorTheme); } diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 0354e6979..974fe0b5f 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -17,8 +17,9 @@ */ export const enum IpcEvents { + INIT_FILE_WATCHERS = "VencordInitFileWatchers", QUICK_CSS_UPDATE = "VencordQuickCssUpdate", - THEME_UPDATE = "VencordThemeUpdate", + OPEN_QUICKCSS = "VencordOpenQuickCss", GET_QUICK_CSS = "VencordGetQuickCss", SET_QUICK_CSS = "VencordSetQuickCss", UPLOAD_THEME = "VencordUploadTheme", @@ -30,23 +31,24 @@ export const enum IpcEvents { GET_SETTINGS_DIR = "VencordGetSettingsDir", GET_SETTINGS = "VencordGetSettings", SET_SETTINGS = "VencordSetSettings", + THEME_UPDATE = "VencordThemeUpdate", OPEN_EXTERNAL = "VencordOpenExternal", - OPEN_QUICKCSS = "VencordOpenQuickCss", GET_UPDATES = "VencordGetUpdates", GET_REPO = "VencordGetRepo", UPDATE = "VencordUpdate", BUILD = "VencordBuild", OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor", + GET_MONACO_THEME = "VencordGetMonacoTheme", GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap", - OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect", - VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording", - CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed", CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride", CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride", OPEN_THEMES_FOLDER = "VencordOpenThemesFolder", OPEN_SETTINGS_FOLDER = "VencordOpenSettingsFolder", + GET_RENDERER_CSS = "VencordGetRendererCss", + RENDERER_CSS_UPDATE = "VencordRendererCssUpdate", + PRELOAD_GET_RENDERER_JS = "VencordPreloadGetRendererJs", } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bdedaf1e8..5b9b20f69 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -509,6 +509,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Sqaaakoi", id: 259558259491340288n }, + iamme: { + name: "i am me", + id: 984392761929256980n + }, Byeoon: { name: "byeoon", id: 1167275288036655133n @@ -615,11 +619,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, jamesbt365: { name: "jamesbt365", - id: 158567567487795200n, + id: 158567567487795200n }, samsam: { name: "samsam", - id: 400482410279469056n, + id: 400482410279469056n }, Cootshk: { name: "Cootshk", @@ -637,6 +641,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "VV", id: 254866377087778816n }, + u32: { + name: "u32", + id: 1063237286818488351n, + } } satisfies Record); export const EquicordDevs = Object.freeze({ @@ -1031,23 +1039,23 @@ export const EquicordDevs = Object.freeze({ }, cassie: { name: "cassie", - id: 280411966126948353n, + id: 280411966126948353n }, mochienya: { name: "mochie", - id: 1043599230247374869n, + id: 1043599230247374869n }, okiso: { name: "okiso", - id: 274178934143451137n, + id: 274178934143451137n }, port22exposed: { name: "port", - id: 1318383159645311009n, + id: 1318383159645311009n }, PhoenixAceVFX: { name: "PhoenixAceVFX", - id: 1016895892055396484n, + id: 1016895892055396484n }, TheArmagan: { name: "TheArmagan", @@ -1099,27 +1107,27 @@ export const EquicordDevs = Object.freeze({ }, Reycko: { name: "Reycko", - id: 1123725368004726794n, + id: 1123725368004726794n }, Campfire: { name: "Campfire", - id: 376414446840578081n, + id: 376414446840578081n }, Cootshk: { name: "Cootshk", - id: 921605971577548820n, + id: 921605971577548820n }, sliwka: { name: "sliwka", - id: 1165286199628419129n, + id: 1165286199628419129n }, bbgaming25k: { name: "bbgaming25k", - id: 851222385528274964n, + id: 851222385528274964n }, davidkra230: { name: "davidkra230", - id: 652699312631054356n, + id: 652699312631054356n }, GroupXyz: { name: "GroupXyz", @@ -1143,7 +1151,7 @@ export const EquicordDevs = Object.freeze({ }, Etorix: { name: "Etorix", - id: 94597845868355584n, + id: 94597845868355584n }, Johannes7k75: { name: "Johannes7k75", @@ -1171,11 +1179,11 @@ export const EquicordDevs = Object.freeze({ }, qouesm: { name: "qouesm", - id: 130388483494641664n, + id: 130388483494641664n }, CallMeGii: { name: "gii", - id: 156481332652802048n, + id: 156481332652802048n }, mmeta: { name: "mmeta", @@ -1213,6 +1221,18 @@ export const EquicordDevs = Object.freeze({ name: "KamiRu", id: 819191621676695563n }, + soapphia: { + name: "soap phia", + id: 1012095822957133976n + }, + benjii: { + name: "Benjii", + id: 463702169443368970n + }, + keyages: { + name: "Key", + id: 1230319937155760131n + }, } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/utils/css.ts b/src/utils/css.ts index 3c46b7a4d..80fd3dedf 100644 --- a/src/utils/css.ts +++ b/src/utils/css.ts @@ -4,9 +4,32 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export function createAndAppendStyle(id: string, target = document.documentElement) { +export function createAndAppendStyle(id: string, target: HTMLElement) { const style = document.createElement("style"); style.id = id; target.append(style); return style; } + +export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join(""); + +export type ClassNameFactoryArg = string | string[] | Record | false | null | undefined | 0 | ""; + +/** + * @param prefix The prefix to add to each class, defaults to `""` + * @returns A classname generator function + * @example + * const cl = classNameFactory("plugin-"); + * + * cl("base", ["item", "editable"], { selected: null, disabled: true }) + * // => "plugin-base plugin-item plugin-editable plugin-disabled" + */ +export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => { + const classNames = new Set(); + for (const arg of args) { + if (arg && typeof arg === "string") classNames.add(arg); + else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name)); + else if (arg && typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name)); + } + return Array.from(classNames, name => prefix + name).join(" "); +}; diff --git a/src/utils/patches.ts b/src/utils/patches.ts index 2716003e0..53cf05dbc 100644 --- a/src/utils/patches.ts +++ b/src/utils/patches.ts @@ -41,7 +41,11 @@ export function canonicalizeMatch(match: T): T { return partialCanon as T; } - const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`); + const canonSource = partialCanon.replaceAll(/(\\*)\\i/g, (match, leadingEscapes) => + leadingEscapes.length % 2 === 0 + ? `${leadingEscapes}${String.raw`(?:[A-Za-z_$][\w$]*)`}` + : match.slice(1) + ); const canonRegex = new RegExp(canonSource, match.flags); canonRegex.toString = match.toString.bind(match); diff --git a/src/utils/web-metadata.ts b/src/utils/web-metadata.ts index cca07ad4c..8e39766fd 100644 --- a/src/utils/web-metadata.ts +++ b/src/utils/web-metadata.ts @@ -4,14 +4,19 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export let EXTENSION_BASE_URL: string; export let EXTENSION_VERSION: string; +export let EXTENSION_BASE_URL: string; +export let RENDERER_CSS_URL: string; + +let resolveMetaReady: Function; +export const metaReady = new Promise(res => resolveMetaReady = res); if (IS_EXTENSION) { const listener = (e: MessageEvent) => { if (e.data?.type === "vencord:meta") { - ({ EXTENSION_BASE_URL, EXTENSION_VERSION } = e.data.meta); + ({ EXTENSION_BASE_URL, EXTENSION_VERSION, RENDERER_CSS_URL } = e.data.meta); window.removeEventListener("message", listener); + resolveMetaReady(); } }; diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 659cd8dfd..867da2991 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -22,6 +22,8 @@ import { Divider } from "@components/Divider"; import { FormSwitchCompat } from "@components/FormSwitch"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; +import { TooltipContainer as TooltipContainerComponent } from "@components/TooltipContainer"; +import { TooltipFallback } from "@components/TooltipFallback"; import { LazyComponent } from "@utils/lazyReact"; import * as t from "@vencord/discord-types"; import { filters, mapMangledModuleLazy, waitFor } from "@webpack"; @@ -52,17 +54,9 @@ export const Switch = FormSwitchCompat as never; export const Card = waitForComponent("Card", filters.componentByCode(".editable),", ".outline:")); export const Checkbox = waitForComponent("Checkbox", filters.componentByCode(".checkboxWrapperDisabled:")); -const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", { - Tooltip: filters.componentByCode("this.renderTooltip()]"), - TooltipContainer: filters.componentByCode('="div"') -}) as { - Tooltip: t.Tooltip, - TooltipContainer: t.TooltipContainer; -}; - -// TODO: if these finds break, they should just return their children -export const Tooltip = LazyComponent(() => Tooltips.Tooltip); -export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer); +export const Tooltip = waitForComponent("Tooltip", m => m.prototype?.shouldShowTooltip && m.prototype.render, TooltipFallback); +/** @deprecated import from @vencord/components */ +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:")); diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 4eb8eb66f..3da77e131 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -19,22 +19,26 @@ import { Logger } from "@utils/Logger"; import { LazyComponent, LazyComponentWrapper } from "@utils/react"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "@webpack"; +import { ComponentType } from "react"; const logger = new Logger("Webpack"); -export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]) { +export function waitForComponent = ComponentType & Record>(name: string, filter: FilterFn | string | string[], fallbackValue: ComponentType | null = null) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); - let myValue: T = function () { - const error = new Error(`Equicord could not find the ${name} Component`); + let myValue: T | null = null; + + const lazyComponent = LazyComponent(() => { + if (myValue) return myValue; + + const error = new Error(`Vencord could not find the ${name} Component`); logger.error(error); if (IS_DEV) throw error; - return null; - } as any; + return fallbackValue!; + }) as LazyComponentWrapper; - const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper; waitFor(filter, (v: any) => { myValue = v; Object.assign(lazyComponent, v); diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index cc3520738..30457f3dc 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -31,25 +31,26 @@ export let MessageStore: Omit & GenericStore & { getMessages(chanId: string): any; }; -export let PermissionStore: GenericStore; -export let GuildChannelStore: GenericStore; -export let ReadStateStore: GenericStore; +export let PermissionStore: t.PermissionStore; +export let GuildChannelStore: t.GuildChannelStore; +export let ReadStateStore: t.ReadStateStore; export let PresenceStore: t.PresenceStore; +export let AccessibilityStore: t.AccessibilityStore; export let GuildStore: t.GuildStore; export let GuildRoleStore: t.GuildRoleStore; +export let GuildScheduledEventStore: t.GuildScheduledEventStore; +export let GuildMemberCountStore: t.GuildMemberCountStore; export let GuildMemberStore: t.GuildMemberStore; export let UserStore: t.UserStore; export let AuthenticationStore: t.AuthenticationStore; +export let ApplicationStore: t.ApplicationStore; export let UserProfileStore: t.UserProfileStore; export let SelectedChannelStore: t.SelectedChannelStore; export let SelectedGuildStore: t.SelectedGuildStore; export let ChannelStore: t.ChannelStore; export let TypingStore: t.TypingStore; export let RelationshipStore: t.RelationshipStore; -export let MediaEngineStore: t.MediaEngineStore; -export let SpellCheckStore: t.SpellCheckStore; -export let UploadAttachmentStore: GenericStore; export let VoiceStateStore: t.VoiceStateStore; export let EmojiStore: t.EmojiStore; @@ -58,13 +59,34 @@ export let ThemeStore: t.ThemeStore; export let WindowStore: t.WindowStore; export let DraftStore: t.DraftStore; export let StreamerModeStore: t.StreamerModeStore; -export let OverridePremiumTypeStore: GenericStore; +export let SpotifyStore: t.SpotifyStore; + +export let MediaEngineStore: t.MediaEngineStore; +export let NotificationSettingsStore: t.NotificationSettingsStore; +export let SpellCheckStore: t.SpellCheckStore; +export let UploadAttachmentStore: t.UploadAttachmentStore; +export let OverridePremiumTypeStore: t.OverridePremiumTypeStore; +export let RunningGameStore: t.RunningGameStore; +export let ActiveJoinedThreadsStore: t.ActiveJoinedThreadsStore; +export let UserGuildSettingsStore: t.UserGuildSettingsStore; +export let UserSettingsProtoStore: t.UserSettingsProtoStore; +export let CallStore: t.CallStore; +export let ChannelRTCStore: t.ChannelRTCStore; +export let FriendsStore: t.FriendsStore; +export let InstantInviteStore: t.InstantInviteStore; +export let InviteStore: t.InviteStore; +export let LocaleStore: t.LocaleStore; +export let RTCConnectionStore: t.RTCConnectionStore; +export let SoundboardStore: t.SoundboardStore; +export let ApplicationCommandIndexStore: GenericStore; /** * @see jsdoc of {@link t.useStateFromStores} */ export const useStateFromStores: t.useStateFromStores = findByCodeLazy("useStateFromStores"); +waitForStore("AccessibilityStore", s => AccessibilityStore = s); +waitForStore("ApplicationStore", s => ApplicationStore = s); waitForStore("AuthenticationStore", s => AuthenticationStore = s); waitForStore("DraftStore", s => DraftStore = s); waitForStore("UserStore", s => UserStore = s); @@ -73,16 +95,18 @@ waitForStore("ChannelStore", m => ChannelStore = m); waitForStore("SelectedChannelStore", m => SelectedChannelStore = m); waitForStore("SelectedGuildStore", m => SelectedGuildStore = m); waitForStore("GuildStore", m => GuildStore = m); -waitForStore("GuildRoleStore", m => GuildRoleStore = m); waitForStore("GuildMemberStore", m => GuildMemberStore = m); waitForStore("RelationshipStore", m => RelationshipStore = m); waitForStore("MediaEngineStore", m => MediaEngineStore = m); +waitForStore("NotificationSettingsStore", m => NotificationSettingsStore = m); waitForStore("SpellcheckStore", m => SpellCheckStore = m); waitForStore("PermissionStore", m => PermissionStore = m); waitForStore("PresenceStore", m => PresenceStore = m); waitForStore("ReadStateStore", m => ReadStateStore = m); waitForStore("GuildChannelStore", m => GuildChannelStore = m); waitForStore("GuildRoleStore", m => GuildRoleStore = m); +waitForStore("GuildScheduledEventStore", m => GuildScheduledEventStore = m); +waitForStore("GuildMemberCountStore", m => GuildMemberCountStore = m); waitForStore("MessageStore", m => MessageStore = m); waitForStore("WindowStore", m => WindowStore = m); waitForStore("EmojiStore", m => EmojiStore = m); @@ -90,8 +114,22 @@ waitForStore("StickersStore", m => StickersStore = m); waitForStore("TypingStore", m => TypingStore = m); waitForStore("VoiceStateStore", m => VoiceStateStore = m); waitForStore("StreamerModeStore", m => StreamerModeStore = m); +waitForStore("SpotifyStore", m => SpotifyStore = m); waitForStore("OverridePremiumTypeStore", m => OverridePremiumTypeStore = m); waitForStore("UploadAttachmentStore", m => UploadAttachmentStore = m); +waitForStore("RunningGameStore", m => RunningGameStore = m); +waitForStore("ActiveJoinedThreadsStore", m => ActiveJoinedThreadsStore = m); +waitForStore("UserGuildSettingsStore", m => UserGuildSettingsStore = m); +waitForStore("UserSettingsProtoStore", m => UserSettingsProtoStore = m); +waitForStore("CallStore", m => CallStore = m); +waitForStore("ChannelRTCStore", m => ChannelRTCStore = m); +waitForStore("FriendsStore", m => FriendsStore = m); +waitForStore("InstantInviteStore", m => InstantInviteStore = m); +waitForStore("InviteStore", m => InviteStore = m); +waitForStore("LocaleStore", m => LocaleStore = m); +waitForStore("RTCConnectionStore", m => RTCConnectionStore = m); +waitForStore("SoundboardStore", m => SoundboardStore = m); +waitForStore("ApplicationCommandIndexStore", m => ApplicationCommandIndexStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies. diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 46bc56d12..86d00ebce 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -189,6 +189,8 @@ export const ChannelActionCreators = findByPropsLazy("openPrivateChannel"); export const VoiceActions = findByPropsLazy("toggleSelfMute"); export const GuildActions = findByPropsLazy("setServerMute", "setServerDeaf"); export const ChannelActions = findByPropsLazy("selectChannel", "selectVoiceChannel"); +export const DraftActions = findByPropsLazy("saveDraft", "changeDraft"); +export const PinActions = findByPropsLazy("pinMessage", "unpinMessage"); export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL"); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 2d8813ab4..10edec0ac 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -527,7 +527,6 @@ export function findComponentByCode(...code: CodeFilter) { export function findComponentLazy(filter: FilterFn) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponent", [filter]]); - return LazyComponent(() => { const res = find(filter, { isIndirect: true }); if (!res)