Merge branch 'dev' into page

This commit is contained in:
thororen1234 2026-02-11 18:20:34 -05:00
commit db77edf1ad
No known key found for this signature in database
479 changed files with 36577 additions and 23000 deletions

View file

@ -83,10 +83,11 @@ jobs:
- name: Publish extension
if: steps.check.outputs.release == 'true'
run: pnpx publish-browser-extension@latest --chrome-zip dist/extension-chrome.zip --firefox-zip dist/extension-firefox.zip --firefox-sources-zip dist/extension-sources.zip --edge-zip dist/extension-chrome.zip
run: pnpx @equicord/publish-browser-extension@latest --chrome-zip dist/extension-chrome.zip --firefox-zip dist/extension-firefox.zip --firefox-sources-zip dist/extension-sources.zip --edge-zip dist/extension-chrome.zip
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}

View file

@ -4,20 +4,19 @@ Our community is welcoming to everyone, regardless of their characteristics.
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
DO
**DO:**
- have empathy and be nice to others
- be respectful of differing opinions, even if you disagree
- give and accept constructive criticism
- Have empathy and be nice to others.
- Be respectful of differing opinions, even if you disagree.
- Hive and accept constructive criticism.
DON'T
**DON'T:**
- use offensive or derogatory language
- troll or spam
- personally attack or harass others
- Use offensive or derogatory language.
- Troll or spam.
- Personally attack or harass others.
Repetitive violations of these guidelines might get your access to the repository restricted.
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from vigilantism
and instead report the issue to a moderator! The best way is joining our [official Discord community](https://equicord.org/discord)
and opening a modmail ticket.
and instead report the issue to a moderator! The best way is joining our [official Discord community](https://equicord.org/discord) and opening a modmail ticket.

View file

@ -1,51 +1,59 @@
# Contributing to Equicord
# Introduction
Equicord is a community project and welcomes any kind of contribution from anyone!
We have development documentation for new contributors, which can be found at <https://docs.equicord.org>.
All contributions should be made in accordance with our [Code of Conduct](./CODE_OF_CONDUCT.md).
> [!IMPORTANT]
> All contributions must follow our [Code of Conduct](./CODE_OF_CONDUCT.md).
## How to contribute
Contributions can be sent via pull requests. If you're new to Git, check [this guide](https://opensource.com/article/19/7/create-pull-request-github).
Contributions are submitted through pull requests. If you are new to Git or GitHub, we recommend reading [this guide](https://opensource.com/article/19/7/create-pull-request-github).
Pull requests can be made either to the `main` or the `dev` branch. However, unless you're an advanced user, I recommend sticking to `main`. This is because the dev branch might contain unstable changes and be force pushed frequently, which could cause conflicts in your pull request.
Pull requests can target either the `main` or `dev` branch.
Unless you are experienced and know what you are doing, **always target `main`**. The `dev` branch may contain unstable changes and can be force-pushed frequently, which may break your pull request.
## Write a plugin
## Writing a Plugin
Writing a plugin is the primary way to contribute.
Developing a plugin is the primary way to contribute.
Before starting your plugin:
- Be in our discord
- Check existing pull requests to see if someone is already working on a similar plugin
- Check our [plugin requests tracker](https://discord.com/channels/1173279886065029291/1419347113745059961) to see if there is an existing request, or if the same idea has been rejected
- If there isn't an existing request, [open one](https://discord.com/channels/1173279886065029291/1419347113745059961) yourself.
and include that you'd like to work on this yourself. Then wait for feedback to see if the idea even has any chance of being accepted. Or maybe others have some ideas to improve it!
- Familarise yourself with our plugin rules below to ensure your plugin is not banned
- Join our Discord server.
- Check existing pull requests to avoid duplicate work.
- Check the [plugin requests tracker](https://discord.com/channels/1173279886065029291/1419347113745059961) to see if your idea already exists or was rejected.
- If no request exists, open one and clearly state that you want to work on it yourself.
- Wait for feedback before starting development, as some ideas may not be accepted or may need adjustments.
- Familiarize yourself with the plugin rules below.
### Plugin Rules
> [!WARNING]
> Skipping these steps may result in your plugin being rejected, even if it is technically correct.
- No simple slash command plugins like `/cat`. Instead, make a [user installable Discord bot](https://discord.com/developers/docs/change-log#userinstallable-apps-preview)
- No simple text replace plugins like Let me Google that for you. The TextReplace plugin can do this
- No raw DOM manipulation. Use proper patches and React
- No FakeDeafen or FakeMute
- No StereoMic
- No plugins that simply hide or redesign ui elements. This can be done with CSS
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
- No plugins that require the user to enter their own API key
- Do not introduce new dependencies unless absolutely necessary and warranted
## Plugin Rules
## Improve Equicord itself
To keep Equicord stable, secure and maintainable, all plugins must follow these rules:
1. No simple slash-command plugins (e.g. `/cat`). If applicable, create a [user-installable Discord app](https://discord.com/developers/docs/change-log#userinstallable-apps-preview) instead.
2. No simple text replacement plugins (the built-in TextReplace plugin already covers this).
3. No raw DOM manipulation — always use proper patches and React.
4. No FakeDeafen or FakeMute functionality.
5. No StereoMic-related plugins.
6. No plugins that only hide or redesign UI elements (use CSS for that). This rule may be negotiable.
7. No plugins that interact with specific third-party Discord bots (official Discord apps are allowed).
8. No selfbots or API abuse (auto-replies, animated statuses, message pruning, Nitro snipers, etc.).
9. No untrusted third-party APIs (well-known services like Google or GitHub are acceptable).
10. No plugins that require users to provide their own API keys.
11. Do not introduce new dependencies unless they are strictly necessary and well justified.
If you have any ideas on how to improve Equicord itself, or want to propose a new plugin API, feel free to open a feature request so we can discuss.
**Plugins that violate any of these rules will not be accepted.**
Or if you notice any bugs or typos, feel free to fix them!
## Improving Equicord Itself
## Help out users in our Discord community
If you want to improve Equicord beyond plugins, such as internal features or performance improvements, you are welcome to open a feature request so it can be discussed.
Bug fixes, refactors, and documentation improvements are also highly appreciated!
## Helping the Community
We have an open support channel in our [Discord community](https://equicord.org/discord).
Helping out users there is always appreciated! The more, the merrier.

View file

@ -135,4 +135,10 @@ window.VencordNative = {
pluginHelpers: {} as any,
csp: {} as any,
tray: {
setUpdateState: NOOP,
onCheckUpdates: NOOP,
onRepair: NOOP,
onAbout: NOOP,
},
};

View file

@ -66,7 +66,7 @@ export default defineConfig(
"simple-header/header": [
"error",
{
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
"files": [`${import.meta.dirname}/scripts/header-new.txt`, `${import.meta.dirname}/scripts/header-old.txt`],
"templates": { "author": [".*", "Vendicated and contributors"] }
}
],

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.13.11",
"version": "1.14.2.0",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {
@ -35,13 +35,14 @@
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit"
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@intrnl/xxhash64": "^0.1.2",
"@streamparser/json": "^0.0.22",
"@types/less": "^3.0.8",
"@types/stylus": "^0.48.42",
"@types/tinycolor2": "^1.4.6",
@ -53,6 +54,7 @@
"jsqr": "1.4.0",
"monaco-editor": "^0.54.0",
"nanoid": "^5.1.6",
"native-file-system-adapter": "^3.0.1",
"usercss-meta": "^0.12.0",
"virtual-merge": "^1.0.1"
},
@ -92,7 +94,7 @@
"typescript-transform-paths": "^3.5.5",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@10.26.2",
"packageManager": "pnpm@10.29.2",
"pnpm": {
"patchedDependencies": {
"@types/less@3.0.8": "patches/@types__less@3.0.8.patch"

View file

@ -41,3 +41,25 @@ export const enum InteractionType {
APPLICATION_COMMAND_AUTOCOMPLETE = 4,
MODAL_SUBMIT = 5,
}
export const enum ApplicationCommandSectionType {
BUILT_IN = 0,
APPLICATION = 1,
}
export const enum ApplicationCommandPermissionType {
ROLE = 1,
USER = 2,
CHANNEL = 3,
}
export const enum InteractionContextType {
GUILD = 0,
BOT_DM = 1,
PRIVATE_CHANNEL = 2,
}
export const enum ApplicationCommandHandlerType {
APP_HANDLER = 1,
DISCORD_LAUNCH_ACTIVITY = 2,
}

View file

@ -5,6 +5,7 @@ export * from "./commands";
export * from "./guild";
export * from "./messages";
export * from "./misc";
export * from "./quests";
export * from "./settings";
export * from "./user";
export * from "./voice";

View file

@ -0,0 +1,390 @@
export const enum QuestRewardType {
REWARD_CODE = 1,
IN_GAME = 2,
COLLECTIBLE = 3,
VIRTUAL_CURRENCY = 4,
FRACTIONAL_PREMIUM = 5,
}
export const enum QuestPlatform {
CROSS_PLATFORM = 0,
XBOX = 1,
PLAYSTATION = 2,
SWITCH = 3,
PC = 4,
}
export const enum QuestTargetedContent {
GIFT_INVENTORY_SETTINGS_BADGE = 0,
QUEST_BAR = 1,
QUEST_INVENTORY_CARD = 2,
QUESTS_EMBED = 3,
ACTIVITY_PANEL = 4,
QUEST_LIVE_STREAM = 5,
MEMBERS_LIST = 6,
QUEST_BADGE = 7,
GIFT_INVENTORY_FOR_YOU = 8,
GIFT_INVENTORY_OTHER = 9,
QUEST_BAR_V2 = 10,
QUEST_HOME_DESKTOP = 11,
QUEST_HOME_MOBILE = 12,
QUEST_BAR_MOBILE = 13,
THIRD_PARTY_APP = 14,
QUEST_BOTTOM_SHEET = 15,
QUEST_EMBED_MOBILE = 16,
QUEST_HOME_MOVE_CALLOUT = 17,
DISCOVERY_SIDEBAR = 18,
QUEST_SHARE_LINK = 19,
CONNECTIONS_MODAL = 20,
DISCOVERY_COMPASS = 21,
TROPHY_CASE_CARD = 22,
VIDEO_MODAL = 23,
VIDEO_MODAL_END_CARD = 24,
REWARD_MODAL = 25,
EXCLUDED_QUEST_EMBED = 26,
VIDEO_MODAL_MOBILE = 27,
ORBS_ANNOUNCEMENT_MODAL = 28,
ORBS_BALANCE_MENU = 29,
QUEST_ENROLLMENT_BLOCKED_BOTTOM_SHEET = 30,
ORBS_SHOP_HERO_CTA = 31,
QUEST_ENROLLMENT_BLOCKED_MODAL = 32,
INTERNAL_PREVIEW_TOOL = 33,
ORBS_REHEAT_COACHMARK_CTA = 34,
INVALID_QUEST_EMBED = 35,
NOT_SHAREABLE_QUEST_EMBED = 36,
QUEST_HOME_MOVE_CALLOUT_DISCOVER = 37,
SPONSORED_QUEST_SHEET = 38,
MOBILE_ORBS_ONBOARDING_DC = 39,
RUNNING_ACTIVITY = 40,
VIDEO_MODAL_PRIMARY_CTA = 41,
QUEST_HOME_TAKEOVER = 42,
USER_PROFILE_ACTIVITY = 43,
}
export const enum QuestPlacement {
INVALID_PLACEMENT = 0,
DESKTOP_ACCOUNT_PANEL_AREA = 1,
MOBILE_HOME_DOCK_AREA = 2,
}
export const enum QuestRewardAssignmentMethod {
ALL = 1,
TIERED = 2,
}
export const enum QuestRewardExpirationMode {
NORMAL = 1,
PREMIUM_EXTENSION = 2,
PREMIUM_PERMANENT = 3,
}
export const enum QuestFeature {
POST_ENROLLMENT_CTA = 1,
QUEST_BAR_V2 = 3,
EXCLUDE_RUSSIA = 5,
IN_HOUSE_CONSOLE_QUEST = 6,
MOBILE_CONSOLE_QUEST = 7,
START_QUEST_CTA = 8,
REWARD_HIGHLIGHTING = 9,
FRACTIONS_QUEST = 10,
ADDITIONAL_REDEMPTION_INSTRUCTIONS = 11,
PACING_V2 = 12,
DISMISSAL_SURVEY = 13,
MOBILE_QUEST_DOCK = 14,
QUESTS_CDN = 15,
PACING_CONTROLLER = 16,
QUEST_HOME_FORCE_STATIC_IMAGE = 17,
VIDEO_QUEST_FORCE_HLS_VIDEO = 18,
VIDEO_QUEST_FORCE_END_CARD_CTA_SWAP = 19,
EXPERIMENTAL_TARGETING_TRAITS = 20,
DO_NOT_DISPLAY = 21,
EXTERNAL_DIALOG = 22,
MOBILE_ONLY_QUEST_PUSH_TO_MOBILE = 23,
MANUAL_HEARTBEAT_INITIALIZATION = 24,
CLOUD_GAMING_ACTIVITY = 25,
NON_GAMING_PLAY_QUEST = 26,
ACTIVITY_QUEST_AUTO_ENROLLMENT = 27,
PACKAGE_ACTION_ADVENTURE = 28,
PACKAGE_RPG_MMO = 29,
PACKAGE_RACING_SPORTS = 30,
PACKAGE_SANDBOX_CREATIVE = 31,
PACKAGE_FAMILY_FRIENDLY = 32,
PACKAGE_HOLIDAY_SEASON = 33,
PACKAGE_NEW_YEARS = 34,
}
export const enum QuestDismissibleContentFlags {
GIFT_INVENTORY_SETTINGS_BADGE = 1,
QUEST_BAR = 2,
ACTIVITY_PANEL = 4,
QUEST_LIVE_STREAM = 8,
}
export const enum QuestUserQuestStatus {
UNACCEPTED = 0,
ACCEPTED = 1,
IN_PROGRESS = 2,
COMPLETED = 3,
CLAIMED = 4,
}
/** Task types for quest completion requirements. */
export const enum QuestTaskType {
STREAM_ON_DESKTOP = "STREAM_ON_DESKTOP",
PLAY_ON_DESKTOP = "PLAY_ON_DESKTOP",
PLAY_ON_XBOX = "PLAY_ON_XBOX",
PLAY_ON_PLAYSTATION = "PLAY_ON_PLAYSTATION",
PLAY_ON_DESKTOP_V2 = "PLAY_ON_DESKTOP_V2",
WATCH_VIDEO = "WATCH_VIDEO",
WATCH_VIDEO_ON_MOBILE = "WATCH_VIDEO_ON_MOBILE",
PLAY_ACTIVITY = "PLAY_ACTIVITY",
ACHIEVEMENT_IN_GAME = "ACHIEVEMENT_IN_GAME",
ACHIEVEMENT_IN_ACTIVITY = "ACHIEVEMENT_IN_ACTIVITY",
}
/** Operator for combining multiple quest tasks. */
export const enum QuestTaskJoinOperator {
/** All tasks must be completed. */
AND = "and",
/** Any one task must be completed. */
OR = "or",
}
/** Error types returned from quest API operations. */
export const enum QuestErrorType {
GENERIC = "generic",
RATE_LIMITED = "rate_limited",
}
/** Platform mode for quest task selection. */
export const enum QuestPlatformMode {
DESKTOP = "desktop",
CONSOLE = "console",
/** User must select a platform. */
SELECT = "select",
}
/** Reasons for pausing quest video playback. */
export const enum QuestVideoPauseReason {
PAUSE_BUTTON = "PAUSE_BUTTON",
LOST_FOCUS = "LOST_FOCUS",
MODAL_CLOSED = "MODAL_CLOSED",
}
/** Placement types for quest home takeover. */
export const enum QuestHomePlacement {
QUEST_HOME_BANNER = "quest_home_banner",
}
/** Client platforms for quest targeting. */
export const enum QuestClientPlatform {
IOS = "ios",
ANDROID = "android",
DESKTOP = "desktop",
WEB = "web",
WEB_MOBILE = "web_mobile",
WEB_TABLET = "web_tablet",
}
/** Quest sharing policy. */
export const enum QuestSharePolicy {
SHAREABLE_EVERYWHERE = "shareable_everywhere",
NOT_SHAREABLE = "not_shareable",
}
/** Quest bar UI states. */
export const enum QuestBarState {
COLLAPSED = "collapsed",
EXPANDED = "expanded",
CLOSED = "closed",
SOFT_DISMISSED = "soft-dismissed",
RESET_TO_PREVIOUS = "reset-to-previous",
}
/** Reasons for quest embed fallback display. */
export const enum QuestEmbedFallbackReason {
EXCLUDED_QUEST = "excluded_quest",
UNKNOWN_QUEST = "unknown_quest",
NOT_SHAREABLE_QUEST = "not_shareable_quest",
}
/** Console platform selection for quest tasks. */
export const enum QuestConsolePlatformSelection {
CONSOLE = "CONSOLE",
DESKTOP = "DESKTOP",
}
/** Call-to-action types for quest UI interactions. */
export const enum QuestContentCTA {
LEARN_MORE = "LEARN_MORE",
SHOW_REWARD = "SHOW_REWARD",
CLAIM_REWARD = "CLAIM_REWARD",
GET_REWARD_CODE = "GET_REWARD_CODE",
COPY_REWARD_CODE = "COPY_REWARD_CODE",
ACCEPT_QUEST = "ACCEPT_QUEST",
COPY_QUEST_URL = "COPY_QUEST_URL",
MOBILE_SHARESHEET = "MOBILE_SHARESHEET",
TRACK_PROGRESS = "TRACK_PROGRESS",
CONNECT_CONSOLE = "CONNECT_CONSOLE",
CONNECT_CONSOLE_LINK = "CONNECT_CONSOLE_LINK",
VIEW_CONSOLE_CONNECTIONS = "VIEW_CONSOLE_CONNECTION",
VIEW_CONSOLE_CONNECTIONS_LINK = "VIEW_CONSOLE_CONNECTIONS_LINK",
VIEW_REQUIREMENTS = "VIEW_REQUIREMENTS",
SELECT_CONSOLE_PLATFORM = "SELECT_CONSOLE_PLATFORM",
SELECT_DESKTOP_PLATFORM = "SELECT_DESKTOP_PLATFORM",
DESELECT_PLATFORM = "DESELECT_PLATFORM",
DEFIBRILLATOR = "DEFIBRILLATOR",
DEFIBRILLATOR_RECONNECT_CONSOLE = "DEFIBRILLATOR_RECONNECT_CONSOLE",
OPEN_DISCLOSURE = "OPEN_DISCLOSURE",
WATCH_STREAM = "WATCH_STREAM",
WATCH_STREAM_CONFIRM = "WATCH_STREAM_CONFIRM",
REWARD_LEARN_MORE = "REWARD_LEARN_MORE",
OPEN_GAME_LINK = "OPEN_GAME_LINK",
OPEN_CONTEXT_MENU = "OPEN_CONTEXT_MENU",
OPEN_QUEST_HOME = "OPEN_QUEST_HOME",
QUEST_BAR_COPY_LINK = "QUEST_BAR.COPY_LINK",
CONTEXT_MENU_COPY_LINK = "CONTEXT_MENU.COPY_LINK",
REWARD_MODAL_COPY_LINK = "REWARD_MODAL.COPY_LINK",
CONTEXT_MENU_HIDE_CONTENT = "CONTEXT_MENU.HIDE_CONTENT",
CONTEXT_MENU_OPEN_GAME_LINK = "CONTEXT_MENU.OPEN_GAME_LINK",
CONTEXT_MENU_OPEN_DISCLOSURE = "CONTEXT_MENU.OPEN_DISCLOSURE",
CONTEXT_MENU_LEARN_MORE = "CONTEXT_MENU.LEARN_MORE",
HOW_TO_HELP_ARTICLE_XBOX = "HOW_TO_HELP_ARTICLE_XBOX",
HOW_TO_HELP_ARTICLE_PLAYSTATION = "HOW_TO_HELP_ARTICLE_PLAYSTATION",
VIEW_QUESTS = "VIEW_QUESTS",
EXPAND = "EXPAND",
COLLAPSE = "COLLAPSE",
START_QUEST = "START_QUEST",
TRANSCRIPT_ENABLE = "TRANSCRIPT_ENABLE",
TRANSCRIPT_DISABLE = "TRANSCRIPT_DISABLE",
CLOSED_CAPTIONING_ENABLE = "CLOSED_CAPTIONING_ENABLE",
CLOSED_CAPTIONING_DISABLE = "CLOSED_CAPTIONING_DISABLE",
SEEK_BACKWARD = "SEEK_BACKWARD",
SEEK_FORWARD = "SEEK_FORWARD",
WATCH_VIDEO = "WATCH_VIDEO",
QUEST_BAR_VIDEO_QUEST_PREVIEW = "QUEST_BAR_VIDEO_QUEST_PREVIEW",
QUEST_HOME_TILE_HEADER_WATCH_VIDEO = "QUEST_HOME_TILE_HEADER_WATCH_VIDEO",
REDEEM_REWARD = "REDEEM_REWARD",
VISIT_REDEMPTION_LINK = "VISIT_REDEMPTION_LINK",
SPONSORED_QUEST_SHEET = "SPONSORED_QUEST_SHEET",
GAME_PROFILE_OPEN = "GAME_PROFILE_OPEN",
GAME_STORE_OPEN_GAME_LINK = "GAME_STORE_OPEN_GAME_LINK",
MOBILE_ORBS_ONBOARDING_DC = "MOBILE_ORBS_ONBOARDING_DC",
}
/** Sort methods for quest list display. */
export const enum QuestSortMethod {
/** Default sorting by relevance. */
SUGGESTED = "suggested",
MOST_RECENT = "most_recent",
EXPIRING_SOON = "expiring_soon",
RECENTLY_ENROLLED = "recently_enrolled",
}
/** Filter types for quest tasks. */
export const enum QuestTaskFilter {
VIDEO = "task_video",
PLAY = "task_play",
}
/** Filter types for quest rewards. */
export const enum QuestRewardFilter {
VIRTUAL_CURRENCY = "reward_virtual_currency",
COLLECTIBLE = "reward_collectible",
IN_GAME = "reward_in_game",
}
/** Video playback progress states. */
export const enum QuestVideoProgressState {
UNKNOWN = "UNKNOWN",
NOT_STARTED = "NOT_STARTED",
IN_PROGRESS = "IN_PROGRESS",
COMPLETED = "COMPLETED",
}
/** Transcript fetch operation states. */
export const enum QuestTranscriptFetchStatus {
NONE = "NONE",
FETCHING = "FETCHING",
SUCCESS = "SUCCESS",
FAILURE = "FAILURE",
}
/** Result types from quest enrollment attempts. */
export const enum QuestEnrollmentResult {
SUCCESS = "success",
CAPTCHA_FAILED = "captcha_failed",
UNKNOWN_ERROR = "unknown_error",
/** Enrollment already in progress. */
PREVIOUS_IN_FLIGHT_REQUEST = "previous_in_flight_request",
}
/** Reasons why a quest embed was not found. */
export const enum QuestEmbedNotFoundReason {
NOT_FOUND = "not_found",
MOBILE_ONLY = "mobile_only",
}
/** Tab filter types for quest list. */
export const enum QuestFilterTab {
ALL = "all",
CLAIMED = "claimed",
/** Internal preview tool only. */
PREVIEW_TOOL = "preview_tool",
}
/** URL parameter types for quest filtering. */
export const enum QuestFilterParamType {
TAB = "tab",
QUEST_ID = "quest_id",
SORT = "sort",
FILTER = "filter",
}
/** Location identifiers for quest-related UI and tracking. */
export const enum QuestLocation {
ACTIVITY_PANEL = "quests_bar_activity_panel",
QUESTS_MANAGER = "quests_manager",
QUESTS_CONSOLE_OPTIMISTIC_UPDATES_MANAGER = "quests_console_optimistic_updates_manager",
USER_SETTINGS_GIFT_INVENTORY = "user_settings_gift_inventory",
USER_SETTINGS_SEARCH_GIFT_INVENTORY = "user_settings_search_gift_inventory",
USE_QUESTS = "use_quests",
STREAM_SOURCE_SELECT = "stream_source_select",
MEMBERS_LIST = "members_list",
QUESTS_BAR = "quests_bar",
QUESTS_BAR_MOBILE = "quests_bar_mobile",
REWARD_CODE_MODAL = "reward_code_modal",
INGAME_REWARD_MODAL = "ingame_reward_modal",
COLLECTIBLE_REWARD_MODAL = "collectible_reward_modal",
ORBS_REWARD_MODAL = "orbs_reward_modal",
QUEST_PREVIEW_TOOL = "quest_preview_tool",
QUEST_PREVIEW_TOOL_2 = "quest_preview_tool_2",
QUESTS_MINOR_REWARD_CAPPING_CONFIG = "QUESTS_MINOR_REWARD_CAPPING_CONFIG",
QUESTS_CARD = "quests_card",
QUESTS_STORE = "quests_store",
QUEST_CHANNEL_CALL_HEADER = "quests_channel_call_header",
QUEST_HOME_DESKTOP = "quest_home_desktop",
QUEST_HOME_MOBILE = "quest_home_mobile",
QUEST_PROGRESS_BAR = "quest_progress_bar",
EMBED_MOBILE = "embed_mobile",
EMBED_DESKTOP = "embed_desktop",
QUEST_CONTEXT_MENU = "context_menu",
CODED_LINK = "coded_link",
QUEST_DISCLOSURE_MODAL = "quest_disclosure_modal",
DISCOVERY_SIDEBAR = "discovery_sidebar",
DISCOVERY_COMPASS = "discovery_compass",
BADGE = "badge",
COLLECTIBLES_SHOP_HEADER_BAR = "collectibles_shop_header_bar",
ORBS_ANNOUNCEMENT_MODAL = "orbs_announcement_modal",
CONFLICT_CHECKS = "conflict_checks",
VIDEO_MODAL = "video_modal",
VIDEO_MODAL_MOBILE = "video_modal_mobile",
GAME_WIDGETS_POPOVER = "game_widgets_popover",
PRIVATE_CHANNELS_LIST = "private_channels_list",
INTERNAL_TOOLING = "internal_tooling",
QUEST_HOME_MOVED_CALLOUT = "quest_home_moved_callout",
IN_APP_NAVIGATION = "in_app_navigation",
NAVIGATE_TO_QUEST_HOME_UTIL = "navigate_to_quest_home_util",
QUEST_DEEP_LINK_UTIL = "quest_deep_link_util",
YOU_TAB_PROFILE_HEADER = "you_tab_profile_header",
}

View file

@ -51,3 +51,11 @@ export const enum UserFlags {
COLLABORATOR = 1125899906842624,
RESTRICTED_COLLABORATOR = 2251799813685248,
}
export const enum StandingState {
ALL_GOOD = 100,
LIMITED = 200,
VERY_LIMITED = 300,
AT_RISK = 400,
SUSPENDED = 500,
}

View file

@ -3,7 +3,7 @@
"author": "Equicord & Vencord Contributors",
"private": false,
"description": "Typescript definitions for the webpack modules of the Discord Web app",
"version": "1.0.2",
"version": "1.0.3",
"license": "LGPL-3.0-or-later",
"types": "src/index.d.ts",
"type": "module",

View file

@ -1,16 +0,0 @@
export interface ButtonWrapperClasses {
hoverScale: string;
buttonWrapper: string;
button: string;
iconMask: string;
buttonContent: string;
icon: string;
pulseIcon: string;
pulseButton: string;
notificationDot: string;
sparkleContainer: string;
sparkleStar: string;
sparklePlus: string;
sparkle: string;
active: string;
}

View file

@ -83,3 +83,52 @@ export interface UserJSON {
username: string;
globalName: string | undefined;
}
export interface ProfileEffect {
skuId: string;
title?: string;
description?: string;
accessibilityLabel?: string;
reducedMotionSrc?: string;
thumbnailPreviewSrc?: string;
effects?: any[];
animationType?: number;
staticFrameSrc?: string;
type?: number;
}
export interface Nameplate {
skuId: string;
asset: string;
label?: string;
palette?: string;
type?: number;
}
export interface ProfilePreset {
name: string;
timestamp: number;
avatarDataUrl?: string | null;
bannerDataUrl?: string | null;
bio?: string | null;
accentColor?: number | null;
themeColors?: number[] | null;
globalName?: string | null;
pronouns?: string | null;
avatarDecoration?: {
asset: string;
skuId: string;
} | null;
profileEffect?: ProfileEffect | null;
nameplate?: Nameplate | null;
primaryGuildId?: string | null;
customStatus?: CustomStatus | null;
displayNameStyles?: DisplayNameStyles | null;
}
export interface CustomStatus {
text?: string;
emojiId?: string;
emojiName?: string;
expiresAtMs?: string;
}

View file

@ -244,7 +244,7 @@ export type Select = ComponentType<PropsWithChildren<{
export type SearchableSelect = ComponentType<PropsWithChildren<{
placeholder?: string;
options: ReadonlyArray<SelectOption>; // TODO
value?: SelectOption | string[];
value?: any;
/**
* - 0 ~ Filled

View file

@ -1,5 +1,4 @@
export * from "./common";
export * from "./classes";
export * from "./components";
export * from "./flux";
export * from "./fluxEvents";

View file

@ -26,7 +26,7 @@ export interface WebUploadItem extends BaseUploadItem {
export type CloudUploadItem = ReactNativeUploadItem | WebUploadItem;
export class CloudUpload extends EventEmitter {
constructor(item: CloudUploadItem, channelId: string, showLargeMessageDialog?: boolean, reactNativeFileIndex?: number);
constructor(item: CloudUploadItem, channelId: string, reactNativeFileIndex?: number);
channelId: string;
classification: string;
@ -54,7 +54,6 @@ export class CloudUpload extends EventEmitter {
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
startTime: number;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED" | "REMOVED_FROM_MSG_DRAFT";

View file

@ -0,0 +1,443 @@
import { Application, Channel, FluxStore } from "..";
import { CommandArgument, CommandContext } from "../common/messages/Commands";
import {
ApplicationCommandHandlerType,
ApplicationCommandInputType,
ApplicationCommandOptionType,
ApplicationCommandPermissionType,
ApplicationCommandSectionType,
ApplicationCommandType,
ApplicationIntegrationType,
ChannelType,
InteractionContextType
} from "../../enums";
/** Score method for ranking command results. */
export type ApplicationCommandScoreMethod =
| "none"
| "application_only"
| "command_only"
| "command_or_application";
/** Built-in command handling mode. */
export type BuiltInCommandMode =
| "allow"
| "deny"
| "only_text";
/** Target for fetching guild command index. */
export interface ApplicationCommandIndexGuildTarget {
type: "guild";
guildId: string;
}
/** Target for fetching channel command index. */
export interface ApplicationCommandIndexChannelTarget {
type: "channel";
channelId: string;
}
/** Target for fetching user command index. */
export interface ApplicationCommandIndexUserTarget {
type: "user";
}
/** Target for fetching application-specific command index. */
export interface ApplicationCommandIndexApplicationTarget {
type: "application";
applicationId: string;
}
/** Union of all fetch target types. */
export type ApplicationCommandIndexFetchTarget =
| ApplicationCommandIndexGuildTarget
| ApplicationCommandIndexChannelTarget
| ApplicationCommandIndexUserTarget
| ApplicationCommandIndexApplicationTarget;
/** Context for querying commands with a channel. */
export interface ApplicationCommandChannelContext {
type: "channel";
channel: Channel;
}
/** Context for querying commands without a channel. */
export interface ApplicationCommandContextlessContext {
type: "contextless";
}
/** Union of query context types. */
export type ApplicationCommandContext =
| ApplicationCommandChannelContext
| ApplicationCommandContextlessContext;
/** Fetch state when not fetching. */
export interface ApplicationCommandIndexFetchStateIdle {
fetching: false;
retryAfter?: number;
}
/** Fetch state when actively fetching. */
export interface ApplicationCommandIndexFetchStateFetching {
fetching: true;
abort: AbortController;
promise: Promise<void>;
}
/** Union of fetch state types. */
export type ApplicationCommandIndexFetchState =
| ApplicationCommandIndexFetchStateIdle
| ApplicationCommandIndexFetchStateFetching;
/** Result of a successful command index fetch. */
export interface ApplicationCommandIndexResult {
/** Command sections keyed by application ID. */
sections: Record<string, ApplicationCommandSection>;
/** Maps bot user ID to application ID. */
sectionIdsByBotId: Record<string, string>;
/** Version symbol for cache invalidation. */
version: symbol;
}
/** State for a command index entry. */
export interface ApplicationCommandIndexState {
/** Server version symbol for staleness detection. */
serverVersion: symbol;
/** Current fetch state. */
fetchState: ApplicationCommandIndexFetchState;
/** Fetched result, if available. */
result?: ApplicationCommandIndexResult;
}
/** A section containing commands from a single application. */
export interface ApplicationCommandSection {
/** Section descriptor with application info. */
descriptor: ApplicationCommandSectionDescriptor;
/** Commands keyed by command ID. */
commands: Record<string, ApplicationCommand>;
}
/** Descriptor for a command section. */
export interface ApplicationCommandSectionDescriptor {
/** Section ID, typically the application ID or a special value like "-1" for built-in. */
id: string;
/** Section type. */
type: ApplicationCommandSectionType;
/** Display name for the section. */
name: string;
/** Application icon hash. */
icon?: string | null;
/** Full application object for application sections. */
application?: Application;
/** Whether this is a user-installed app. */
isUserApp?: boolean;
/** Bot user ID for this application. */
botId?: string;
/** Permissions for the application's commands. */
permissions?: ApplicationCommandPermission[] | Record<string, ApplicationCommandPermission>;
}
/** Permission override for a command. */
export interface ApplicationCommandPermission {
/** Type of permission target. */
type: ApplicationCommandPermissionType;
/** ID of the role, user, or channel. */
id: string;
/** Whether the target has permission to use the command. */
permission: boolean;
}
/** Option for an application command. */
export interface ApplicationCommandOption {
/** Option type. */
type: ApplicationCommandOptionType;
/** Option name. */
name: string;
/** Localized display name for the option. */
displayName?: string;
/** Localized option name from server. Bot commands only. */
serverLocalizedName?: string;
/** Option description. */
description: string;
/** Localized display description for the option. */
displayDescription?: string;
/** Localized option description from server. Bot commands only. */
serverLocalizedDescription?: string;
/** Whether this option is required. */
required?: boolean;
/** Choices for string/integer/number options. */
choices?: ApplicationCommandOptionChoice[];
/** Nested options for subcommands. */
options?: ApplicationCommandOption[];
/** Channel types allowed for channel options. */
channel_types?: ChannelType[];
/** Minimum value for number/integer options. */
min_value?: number;
/** Maximum value for number/integer options. */
max_value?: number;
/** Minimum length for string options. */
min_length?: number;
/** Maximum length for string options. */
max_length?: number;
/** Whether autocomplete is enabled. */
autocomplete?: boolean;
}
/** Choice for a command option. */
export interface ApplicationCommandOptionChoice {
/** Choice name. */
name: string;
/** Localized choice name, if available. */
name_localized?: string;
/** Choice value. */
value: string | number;
}
/** Raw application command from the API. */
export interface RawApplicationCommand {
id: string;
application_id: string;
version: string;
guild_id?: string;
type?: ApplicationCommandType;
name: string;
name_default?: string;
name_localized?: string;
description: string;
description_default?: string;
description_localized?: string;
options?: ApplicationCommandOption[];
default_member_permissions?: string | null;
dm_permission?: boolean;
nsfw?: boolean;
contexts?: InteractionContextType[];
integration_types?: ApplicationIntegrationType[];
permissions?: ApplicationCommandPermission[];
global_popularity_rank?: number;
handler?: ApplicationCommandHandlerType;
}
/** Processed application command. */
export interface ApplicationCommand {
/** Command version string. Bot commands only. */
version?: string;
/** Guild ID if guild-specific. Bot commands only. */
guildId?: string;
/** Composite command ID including subcommand path. */
id: string;
/** Original untranslated command name. */
untranslatedName: string;
/** Localized command name from server. Bot commands only. */
serverLocalizedName?: string;
/** Application ID. */
applicationId: string;
/** Command type. */
type: ApplicationCommandType;
/** Input type. */
inputType: ApplicationCommandInputType;
/** Original untranslated description. */
untranslatedDescription: string;
/** Command options. */
options: ApplicationCommandOption[];
/** Root command reference for subcommands. Bot commands only. */
rootCommand?: RawApplicationCommand;
/** Path of subcommand options for nested commands. Bot commands only. */
subCommandPath?: ApplicationCommandOption[];
/** Default member permissions required as bigint. Bot commands only. */
defaultMemberPermissions?: bigint;
/** Whether usable in DMs. Bot commands only. */
dmPermission?: boolean;
/** Command permissions. Bot commands only. */
permissions?: ApplicationCommandPermission[] | Record<string, ApplicationCommandPermission>;
/** Localized display name. */
displayName: string;
/** Localized display description. */
displayDescription: string;
/** Whether this is an NSFW command. Bot commands only. */
nsfw?: boolean;
/** Allowed interaction contexts. Bot commands only. */
contexts?: InteractionContextType[];
/** Integration types that can use this command. Bot commands only. */
integration_types?: ApplicationIntegrationType[];
/** Global popularity ranking. Bot commands only. */
global_popularity_rank?: number;
/** Handler type. Bot commands only. */
handler?: ApplicationCommandHandlerType;
/** Section descriptor for display purposes. */
section?: ApplicationCommandSectionDescriptor;
/** Score for search ranking. */
score?: number;
/** Predicate to check if command should be shown. Built-in commands only. */
predicate?(ctx: CommandContext): boolean;
/** Execute the command. Built-in commands only. */
execute?(args: CommandArgument[], ctx: CommandContext): void;
}
/** Sectioned command group for display. */
export interface SectionedApplicationCommands {
/** Section this group belongs to. */
section: ApplicationCommandSectionDescriptor;
/** Commands in this section. */
data: ApplicationCommand[];
}
/** Filter options for querying commands. */
export interface ApplicationCommandQueryFilters {
/** Command types to include. */
commandTypes: ApplicationCommandType[];
/** Search text. */
text?: string;
/** Built-in command handling mode. */
builtIns?: BuiltInCommandMode;
/** Whether to include application commands. */
applicationCommands?: boolean;
}
/** Options for querying commands. */
export interface ApplicationCommandQueryOptions {
/** Number of placeholder commands to show while loading. */
placeholderCount?: number;
/** Score method for ranking results. */
scoreMethod?: ApplicationCommandScoreMethod;
/** Whether to allow empty sections in results. */
allowEmptySections?: boolean;
/** Whether to allow fetching if needed. */
allowFetch?: boolean;
/** Whether to include application state. */
allowApplicationState?: boolean;
/** Specific application ID to query. */
applicationId?: string;
/** Sorting options. */
sortOptions?: ApplicationCommandSortOptions;
/** Whether to include install-on-demand apps. */
installOnDemand?: boolean;
}
/** Sorting options for application command queries. */
export interface ApplicationCommandSortOptions {
/** Sorting options for applications. */
applications: ApplicationCommandSortConfig;
/** Sorting options for commands. */
commands: ApplicationCommandSortConfig;
}
/** Sort configuration for frecency and score. */
export interface ApplicationCommandSortConfig {
/** Whether to use frecency for sorting. */
useFrecency: boolean;
/** Whether to use score for sorting. */
useScore: boolean;
}
/** Result of a command query. */
export interface ApplicationCommandQueryResult {
/** All section descriptors. */
descriptors: ApplicationCommandSectionDescriptor[];
/** Flat list of all commands. */
commands: ApplicationCommand[];
/** Commands grouped by section. */
sectionedCommands: SectionedApplicationCommands[];
/** Whether data is still loading. */
loading: boolean;
}
/** Parameters for checking context state application. */
export interface ApplicationCommandContextStateParams {
applicationId: string;
channelId: string;
guildId?: string;
}
/** Result of getting section info. */
export interface ApplicationCommandSectionInfo {
/** Section descriptor. */
descriptor?: ApplicationCommandSectionDescriptor;
/** Commands in this section. */
sectionCommands?: ApplicationCommand[];
/** Whether installed in a guild. */
isGuildInstalled: boolean;
/** Whether installed for user. */
isUserInstalled: boolean;
}
/**
* Store for application command indexes.
* Manages fetching, caching, and querying of slash commands and context menu commands.
*/
export class ApplicationCommandIndexStore extends FluxStore {
/** Internal index cache keyed by guild/channel/user ID. */
indices: Record<string, ApplicationCommandIndexState>;
/**
* Gets the command index state for the given context.
* @param context Channel context or contextless context.
* @returns Index state for the context.
*/
getContextState(context: ApplicationCommandContext): ApplicationCommandIndexState;
/**
* Checks if an application has commands in the context state.
* @param params Application and channel info.
* @returns Whether the application has commands in context.
*/
hasContextStateApplication(params: ApplicationCommandContextStateParams): boolean;
/**
* Gets the command index state for a guild.
* @param guildId Guild ID.
* @returns Index state for the guild.
*/
getGuildState(guildId: string): ApplicationCommandIndexState;
/**
* Gets the command index state for the current user's installed apps.
* @returns Index state for user-installed apps.
*/
getUserState(): ApplicationCommandIndexState;
/**
* Checks if an application has commands in the user state.
* @param applicationId Application ID.
* @returns Whether the application has user commands.
*/
hasUserStateApplication(applicationId: string): boolean;
/**
* Gets the command index state for a specific application.
* @param applicationId Application ID.
* @returns Index state for the application.
*/
getApplicationState(applicationId: string): ApplicationCommandIndexState;
/**
* Gets all application index states.
* @returns Map of application ID to index state.
*/
getApplicationStates(): Map<string, ApplicationCommandIndexState>;
/**
* Checks if an application state exists.
* @param applicationId Application ID.
* @returns Whether the application state exists.
*/
hasApplicationState(applicationId: string): boolean;
/**
* Queries commands based on context and filters.
* @param context Channel or contextless context.
* @param filters Query filters.
* @param options Query options.
* @returns Query result with commands and sections.
*/
query(
context: ApplicationCommandContext,
filters: ApplicationCommandQueryFilters,
options: ApplicationCommandQueryOptions
): ApplicationCommandQueryResult;
/**
* Queries for install-on-demand app commands.
* @param applicationId Application ID.
* @param channelId Channel ID.
*/
queryInstallOnDemandApp(applicationId: string, channelId: string): void;
}

View file

@ -5,7 +5,7 @@ export class AuthenticationStore extends FluxStore {
* Gets the id of the current user
*/
getId(): string;
getSessionId(): string;
// This Store has a lot more methods related to everything Auth, but they really should
// not be needed, so they are not typed
}

View file

@ -0,0 +1,253 @@
import { ApplicationCommandOptionType } from "../../enums";
import { FluxStore, Message } from "..";
/** Slate node type values used in the rich text editor. */
export type SlateNodeType =
| "text"
| "line"
| "s"
| "u"
| "strong"
| "em"
| "image"
| "emoji"
| "customEmoji"
| "link"
| "url"
| "autolink"
| "highlight"
| "paragraph"
| "br"
| "newline"
| "escape"
| "spoiler"
| "blockQuote"
| "inlineCode"
| "codeBlock"
| "mention"
| "userMention"
| "channelMention"
| "channel"
| "guild"
| "attachmentLink"
| "shopLink"
| "soundboard"
| "staticRouteLink"
| "roleMention"
| "commandMention"
| "timestamp"
| "timestampMentionInput"
| "list"
| "heading"
| "subtext"
| "silentPrefix"
| "gameMention"
| "gameMentionInput"
| "textMention"
| "applicationCommandOption";
/** A plain text leaf node. */
export interface SlateTextNode {
text: string;
}
/** Base interface for Slate element nodes with children. */
export interface SlateBaseElement {
type: string;
children: SlateNode[];
}
/** A line element in the editor. */
export interface SlateLineElement extends SlateBaseElement {
type: "line";
codeBlockState?: {
isInCodeBlock: boolean;
};
}
/** A block quote element. */
export interface SlateBlockQuoteElement extends SlateBaseElement {
type: "blockQuote";
}
/** An emoji element. */
export interface SlateEmojiElement extends SlateBaseElement {
type: "emoji";
emoji: {
name: string;
src?: string;
surrogate: string;
jumboable?: boolean;
};
}
/** A custom emoji element. */
export interface SlateCustomEmojiElement extends SlateBaseElement {
type: "customEmoji";
emoji: {
emojiId: string;
name: string;
animated: boolean;
jumboable?: boolean;
};
}
/** A user mention element. */
export interface SlateUserMentionElement extends SlateBaseElement {
type: "userMention";
userId: string;
}
/** A channel mention element. */
export interface SlateChannelMentionElement extends SlateBaseElement {
type: "channelMention";
channelId: string;
}
/** A role mention element. */
export interface SlateRoleMentionElement extends SlateBaseElement {
type: "roleMention";
roleId: string;
}
/** A text mention element (for @everyone, @here, etc.). */
export interface SlateTextMentionElement extends SlateBaseElement {
type: "textMention";
name: string;
}
/** A soundboard element. */
export interface SlateSoundboardElement extends SlateBaseElement {
type: "soundboard";
guildId: string;
soundId: string;
}
/** Timestamp format style characters. */
export type TimestampStyle = "t" | "T" | "d" | "D" | "f" | "F" | "R";
/** Parsed timestamp data. */
export interface ParsedTimestamp {
/** The Unix timestamp in seconds as a string. */
timestamp: string;
/** The format style character. */
format: TimestampStyle | undefined;
/** The parsed Moment.js object. */
parsed: object;
/** The full formatted date string. */
full: string;
/** The formatted string based on the format style. */
formatted: string;
}
/** A timestamp element. */
export interface SlateTimestampElement extends SlateBaseElement {
type: "timestamp";
parsed: ParsedTimestamp;
}
/** A static route link element. */
export interface SlateStaticRouteLinkElement extends SlateBaseElement {
type: "staticRouteLink";
id: string;
itemId?: string;
}
/** A game mention element. */
export interface SlateGameMentionElement extends SlateBaseElement {
type: "gameMention";
applicationId: string;
}
/** A timestamp mention input element (while typing). */
export interface SlateTimestampMentionInputElement extends SlateBaseElement {
type: "timestampMentionInput";
}
/** A game mention input element (while typing). */
export interface SlateGameMentionInputElement extends SlateBaseElement {
type: "gameMentionInput";
}
/** An application command option element. */
export interface SlateApplicationCommandOptionElement extends SlateBaseElement {
type: "applicationCommandOption";
optionType: ApplicationCommandOptionType;
}
/** Union of all Slate element types. */
export type SlateElement =
| SlateLineElement
| SlateBlockQuoteElement
| SlateEmojiElement
| SlateCustomEmojiElement
| SlateUserMentionElement
| SlateChannelMentionElement
| SlateRoleMentionElement
| SlateTextMentionElement
| SlateSoundboardElement
| SlateTimestampElement
| SlateTimestampMentionInputElement
| SlateStaticRouteLinkElement
| SlateGameMentionElement
| SlateGameMentionInputElement
| SlateApplicationCommandOptionElement
| SlateBaseElement;
/** A Slate node can be either an element or a text leaf. */
export type SlateNode = SlateElement | SlateTextNode;
/** The rich value representation used by Slate editor for message editing. */
export type SlateValue = SlateLineElement[];
export class EditMessageStore extends FluxStore {
/**
* Gets the action source that triggered editing for a channel.
* @param channelId The channel ID.
* @returns The source string or undefined if not editing.
*/
getEditActionSource(channelId: string): string | undefined;
/**
* Gets the message currently being edited in a channel.
* @param channelId The channel ID.
* @returns The message being edited or null if not editing.
*/
getEditingMessage(channelId: string): Message | null;
/**
* Gets the ID of the message currently being edited in a channel.
* @param channelId The channel ID.
* @returns The message ID or undefined if not editing.
*/
getEditingMessageId(channelId: string): string | undefined;
/**
* Gets the rich value (Slate editor format) of the editing text.
* @param channelId The channel ID.
* @returns The Slate value or undefined if not editing.
*/
getEditingRichValue(channelId: string): SlateValue | undefined;
/**
* Gets the plain text value of the editing text.
* @param channelId The channel ID.
* @returns The text value or undefined if not editing.
*/
getEditingTextValue(channelId: string): string | undefined;
/**
* Checks if a specific message is currently being edited.
* @param channelId The channel ID.
* @param messageId The message ID to check.
* @returns True if the specific message is being edited.
*/
isEditing(channelId: string, messageId: string): boolean;
/**
* Checks if any message is currently being edited in a channel.
* @param channelId The channel ID.
* @returns True if any message is being edited in the channel.
*/
isEditingAny(channelId: string): boolean;
}

View file

@ -0,0 +1,246 @@
import { FluxStore } from "..";
/**
* Known popout window key constants.
* Used as the key parameter for PopoutWindowStore and PopoutActions methods.
*/
export type PopoutWindowKey =
| "DISCORD_CHANNEL_CALL_POPOUT"
| "DISCORD_CALL_TILE_POPOUT"
| "DISCORD_SOUNDBOARD"
| "DISCORD_RTC_DEBUG_POPOUT"
| "DISCORD_CHANNEL_POPOUT"
| "DISCORD_ACTIVITY_POPOUT"
| "DISCORD_OVERLAY_POPOUT"
| "DISCORD_DEVTOOLS_POPOUT";
/**
* Popout window lifecycle event types.
* Sent via postMessage from popout to parent window.
*/
export type PopoutWindowEventType = "loaded" | "unloaded";
/**
* Persisted window position and size state.
* Saved to localStorage and restored when reopening popouts.
*/
export interface PopoutWindowState {
/** window x position on screen in pixels. */
x: number;
/** window y position on screen in pixels. */
y: number;
/** window inner width in pixels. */
width: number;
/** window inner height in pixels. */
height: number;
/** whether window stays above other windows, only on desktop app. */
alwaysOnTop?: boolean;
}
/**
* Features passed to window.open() for popout configuration.
* Merged with default features (menubar, toolbar, location, directories = false).
*/
export interface BrowserWindowFeatures {
/** whether to show browser toolbar. */
toolbar?: boolean;
/** whether to show menu bar. */
menubar?: boolean;
/** whether to show location/address bar. */
location?: boolean;
/** whether to show directory buttons. */
directories?: boolean;
/** window width in pixels. */
width?: number;
/** window height in pixels. */
height?: number;
/** default width if no persisted state exists. */
defaultWidth?: number;
/** default height if no persisted state exists. */
defaultHeight?: number;
/** window left position in pixels. */
left?: number;
/** window top position in pixels. */
top?: number;
/** default always-on-top state, defaults to false. */
defaultAlwaysOnTop?: boolean;
/** whether window can be moved by user. */
movable?: boolean;
/** whether window can be resized by user. */
resizable?: boolean;
/** whether window has a frame/border. */
frame?: boolean;
/** whether window stays above other windows. */
alwaysOnTop?: boolean;
/** whether window has a shadow (macOS). */
hasShadow?: boolean;
/** whether window background is transparent. */
transparent?: boolean;
/** whether to hide window from taskbar. */
skipTaskbar?: boolean;
/** title bar style, null for default. */
titleBarStyle?: string | null;
/** window background color as hex string. */
backgroundColor?: string;
/** whether this is an out-of-process overlay window. */
outOfProcessOverlay?: boolean;
}
/**
* Manages Discord's popout windows (voice calls, activities, etc.).
* Extends PersistedStore to save window positions across sessions.
*
* Handles Flux actions:
* - POPOUT_WINDOW_OPEN: opens a new popout window
* - POPOUT_WINDOW_CLOSE: closes a popout window
* - POPOUT_WINDOW_SET_ALWAYS_ON_TOP: toggles always-on-top (desktop only)
* - POPOUT_WINDOW_ADD_STYLESHEET: injects stylesheet into all open popouts
* - LOGOUT: closes all popout windows
*/
export class PopoutWindowStore extends FluxStore {
/**
* Gets the Window object for a popout.
* @param key unique identifier for the popout window
* @returns Window reference or undefined if not open
*/
getWindow(key: string): Window | undefined;
/**
* Gets persisted position/size state for a window.
* State is saved when window closes and restored when reopened.
* @param key unique identifier for the popout window
* @returns saved state or undefined if never opened
*/
getWindowState(key: string): PopoutWindowState | undefined;
/**
* Gets all currently open popout window keys.
* @returns array of window key identifiers
*/
getWindowKeys(): string[];
/**
* Checks if a popout window is currently open.
* @param key unique identifier for the popout window
* @returns true if window exists and is not closed
*/
getWindowOpen(key: string): boolean;
/**
* Checks if a popout window has always-on-top enabled.
* Only functional on desktop app (isPlatformEmbedded).
* @param key unique identifier for the popout window
* @returns true if always-on-top is enabled
*/
getIsAlwaysOnTop(key: string): boolean;
/**
* Checks if a popout window's document has focus.
* @param key unique identifier for the popout window
* @returns true if window document has focus
*/
getWindowFocused(key: string): boolean;
/**
* Checks if a popout window is visible (not minimized/hidden).
* Uses document.visibilityState === "visible".
* @param key unique identifier for the popout window
* @returns true if window is visible
*/
getWindowVisible(key: string): boolean;
/**
* Gets all persisted window states.
* Keyed by window identifier, contains position/size data.
* @returns record of window key to persisted state
*/
getState(): Record<string, PopoutWindowState>;
/**
* Checks if a window is fully initialized and ready for rendering.
* A window is fully initialized when it has:
* - Window object created
* - React root mounted
* - Render function stored
* @param key unique identifier for the popout window
* @returns true if window is fully initialized
*/
isWindowFullyInitialized(key: string): boolean;
/**
* Checks if a popout window is in fullscreen mode.
* Checks if document.fullscreenElement.id === "app-mount".
* @param key unique identifier for the popout window
* @returns true if window is fullscreen
*/
isWindowFullScreen(key: string): boolean;
/**
* Unmounts and closes a popout window.
* Saves current position/size before closing.
* Logs warning if window was not fully initialized.
* @param key unique identifier for the popout window
*/
unmountWindow(key: string): void;
}
/**
* Actions for managing popout windows.
* Dispatches Flux actions to PopoutWindowStore.
*/
export interface PopoutActions {
/**
* Opens a new popout window.
* If window with key already exists and is not out-of-process:
* - On desktop: focuses the existing window via native module
* - On web: calls window.focus()
* @param key unique identifier for the popout window
* @param render function that returns React element to render, receives key as arg
* @param features window features (size, position, etc.)
*/
open(key: string, render: (key: string) => React.ReactNode, features?: BrowserWindowFeatures): void;
/**
* Closes a popout window.
* Saves position/size state before closing unless preventPopoutClose setting is true.
* @param key unique identifier for the popout window
*/
close(key: string): void;
/**
* Sets always-on-top state for a popout window.
* Only functional on desktop app (isPlatformEmbedded).
* @param key unique identifier for the popout window
* @param alwaysOnTop whether window should stay above others
*/
setAlwaysOnTop(key: string, alwaysOnTop: boolean): void;
/**
* Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it
*
* Injects a stylesheet into all open popout windows.
* Validates origin matches current host or webpack public path.
* @param url stylesheet URL to inject
* @param integrity optional SRI integrity hash
*/
addStylesheet?(url: string, integrity?: string): void;
/**
* Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it
*
* Opens a channel call popout for voice/video calls.
* Dispatches CHANNEL_CALL_POPOUT_WINDOW_OPEN action.
* @param channel channel object to open call popout for
*/
openChannelCallPopout?(channel: { id: string; }): void;
/**
* Note: Not actually in the Webpack Common. You have to add it yourself if you want to use it
*
* Opens a call tile popout for a specific participant.
* Dispatches CALL_TILE_POPOUT_WINDOW_OPEN action.
* @param channelId channel ID of the call
* @param participantId user ID of the participant
*/
openCallTilePopout?(channelId: string, participantId: string): void;
}

View file

@ -0,0 +1,738 @@
import {
QuestDismissibleContentFlags,
QuestFeature,
QuestPlatform,
QuestPlacement,
QuestRewardAssignmentMethod,
QuestRewardExpirationMode,
QuestRewardType,
QuestTargetedContent,
QuestTaskType,
QuestHomePlacement,
QuestTranscriptFetchStatus,
QuestErrorType,
QuestSharePolicy,
QuestTaskJoinOperator,
QuestPlatformMode,
} from "../../enums";
import { FluxStore } from "./FluxStore";
/** Video asset for quest tasks. */
export interface QuestVideoAsset {
/** Video URL path relative to CDN. */
url: string;
/** Width in pixels. */
width: number;
/** Height in pixels. */
height: number;
/** Thumbnail image URL. */
thumbnail: string;
/** VTT caption file URL. */
caption?: string;
/** Transcript file URL. */
transcript?: string;
}
/** Video assets for video-based quest tasks. */
export interface QuestTaskVideoAssets {
/** Full resolution video (1080p). */
video: QuestVideoAsset;
/** Low resolution video (720p). */
videoLowRes?: QuestVideoAsset;
/** HLS streaming video (.m3u8). */
videoHls?: QuestVideoAsset;
}
/** Display messages for video-based quest tasks. */
export interface QuestTaskVideoMessages {
videoTitle: string;
}
/** Display messages for achievement-based quest tasks. */
export interface QuestTaskAchievementMessages {
taskTitle: string;
taskDescription: string;
}
/** Application reference in quest tasks. */
export interface QuestTaskApplication {
/** Discord application ID. */
id: string;
}
/** Base properties shared by all quest task types. */
interface QuestTaskBase {
/** Target value to complete (seconds for play tasks, seconds for video tasks). */
target: number;
}
/** Play on desktop quest task. */
export interface QuestTaskPlayOnDesktop extends QuestTaskBase {
type: QuestTaskType.PLAY_ON_DESKTOP;
/** Applications that qualify for completion. */
applications: QuestTaskApplication[];
}
/** Play on desktop v2 quest task with enhanced tracking. */
export interface QuestTaskPlayOnDesktopV2 extends QuestTaskBase {
type: QuestTaskType.PLAY_ON_DESKTOP_V2;
applications: QuestTaskApplication[];
}
/** Stream on desktop quest task. */
export interface QuestTaskStreamOnDesktop extends QuestTaskBase {
type: QuestTaskType.STREAM_ON_DESKTOP;
applications: QuestTaskApplication[];
}
/** Play Discord activity quest task. */
export interface QuestTaskPlayActivity extends QuestTaskBase {
type: QuestTaskType.PLAY_ACTIVITY;
applications: QuestTaskApplication[];
}
/** Play on Xbox quest task. */
export interface QuestTaskPlayOnXbox extends QuestTaskBase {
type: QuestTaskType.PLAY_ON_XBOX;
/** Xbox title IDs. */
externalIds: string[];
applications: QuestTaskApplication[];
}
/** Play on PlayStation quest task. */
export interface QuestTaskPlayOnPlayStation extends QuestTaskBase {
type: QuestTaskType.PLAY_ON_PLAYSTATION;
/** PlayStation title IDs (e.g., "PPSA09016_00"). */
externalIds: string[];
applications: QuestTaskApplication[];
}
/** Watch video quest task for desktop. */
export interface QuestTaskWatchVideo extends QuestTaskBase {
type: QuestTaskType.WATCH_VIDEO;
assets: QuestTaskVideoAssets;
messages: QuestTaskVideoMessages;
}
/** Watch video quest task for mobile. */
export interface QuestTaskWatchVideoOnMobile extends QuestTaskBase {
type: QuestTaskType.WATCH_VIDEO_ON_MOBILE;
assets: QuestTaskVideoAssets;
messages: QuestTaskVideoMessages;
}
/** Achievement in game quest task. */
export interface QuestTaskAchievementInGame extends QuestTaskBase {
type: QuestTaskType.ACHIEVEMENT_IN_GAME;
/** Event name dispatched when achievement is unlocked. */
eventName: string;
messages: QuestTaskAchievementMessages;
applications: QuestTaskApplication[];
}
/** Achievement in Discord activity quest task. */
export interface QuestTaskAchievementInActivity extends QuestTaskBase {
type: QuestTaskType.ACHIEVEMENT_IN_ACTIVITY;
eventName: string;
messages: QuestTaskAchievementMessages;
applications: QuestTaskApplication[];
}
/** Union of all quest task types. */
export type QuestTask =
| QuestTaskPlayOnDesktop
| QuestTaskPlayOnDesktopV2
| QuestTaskStreamOnDesktop
| QuestTaskPlayActivity
| QuestTaskPlayOnXbox
| QuestTaskPlayOnPlayStation
| QuestTaskWatchVideo
| QuestTaskWatchVideoOnMobile
| QuestTaskAchievementInGame
| QuestTaskAchievementInActivity;
/** Task configuration for a quest. */
export interface QuestTaskConfig {
/** Map of task type to task configuration. */
tasks: Partial<Record<QuestTaskType, QuestTask>>;
/** How tasks are combined. Default is "or" (any task completes the quest). */
joinOperator: QuestTaskJoinOperator;
}
/** Display messages for quest rewards. */
export interface QuestRewardMessages {
/** Redemption instructions keyed by {@link QuestPlatform}. */
redemptionInstructionsByPlatform: Partial<Record<QuestPlatform, string>>;
/** Reward display name (e.g., "700 Orbs"). */
name: string;
/** Name with article (e.g., "700 Orbs"). */
nameWithArticle: string;
}
/** Base properties shared by all quest reward types. */
interface QuestRewardBase {
/** SKU ID for the reward entitlement. */
skuId: string;
messages: QuestRewardMessages;
}
/** Reward code reward type. */
export interface QuestRewardCode extends QuestRewardBase {
type: QuestRewardType.REWARD_CODE;
/** Reward image URL. */
asset?: string;
/** Reward video URL. */
assetVideo?: string | null;
/** Approximate codes remaining, null if unlimited. */
approximateCount?: number | null;
/** External URL to redeem the code. */
redemptionLink?: string | null;
}
/** In-game reward type. */
export interface QuestRewardInGame extends QuestRewardBase {
type: QuestRewardType.IN_GAME;
asset?: string;
assetVideo?: string | null;
}
/** Collectible avatar decoration reward type. */
export interface QuestRewardCollectible extends QuestRewardBase {
type: QuestRewardType.COLLECTIBLE;
asset?: string;
assetVideo?: string | null;
/** ISO timestamp when collectible expires. */
expiresAt?: string;
/** Expiration behavior. */
expirationMode?: QuestRewardExpirationMode;
/** Extended expiration for Nitro users. */
expiresAtPremium?: string | null;
}
/** Virtual currency (Orbs) reward type. */
export interface QuestRewardVirtualCurrency extends QuestRewardBase {
type: QuestRewardType.VIRTUAL_CURRENCY;
/** Number of orbs awarded. */
orbQuantity: number;
}
/** Fractional Nitro premium time reward type. */
export interface QuestRewardFractionalPremium extends QuestRewardBase {
type: QuestRewardType.FRACTIONAL_PREMIUM;
asset?: string;
assetVideo?: string | null;
/** Premium time quantity. */
quantity: number;
}
/** Union of all quest reward types. */
export type QuestReward =
| QuestRewardCode
| QuestRewardInGame
| QuestRewardCollectible
| QuestRewardVirtualCurrency
| QuestRewardFractionalPremium;
/** Rewards configuration for a quest. */
export interface QuestRewardsConfig {
/** How rewards are distributed to users. */
assignmentMethod: QuestRewardAssignmentMethod;
/** Available reward options. */
rewards: QuestReward[];
/** ISO timestamp when rewards can no longer be claimed. */
rewardsExpireAt: string;
/** Platforms where rewards can be claimed. */
platforms: QuestPlatform[];
}
/** Visual assets for quest display. */
export interface QuestAssets {
/** Hero banner image URL. */
hero: string;
/** Hero banner video URL. */
heroVideo: string | null;
/** Quest bar hero image URL. */
questBarHero: string;
/** Quest bar hero video URL. */
questBarHeroVideo: string | null;
/** Game tile image URL. */
gameTile: string;
/** Game logotype image URL. */
logotype: string;
/** Light theme game tile URL. */
gameTileLight: string;
/** Dark theme game tile URL. */
gameTileDark: string;
/** Light theme logotype URL. */
logotypeLight: string;
/** Dark theme logotype URL. */
logotypeDark: string;
}
/** Color scheme for quest UI theming. */
export interface QuestColors {
/** Primary hex color (e.g., "#4752C4"). */
primary: string;
/** Secondary hex color. */
secondary: string;
}
/** Display messages for quest UI. */
export interface QuestMessages {
/** Quest display name. */
questName: string;
/** Game title. */
gameTitle: string;
/** Game publisher name. */
gamePublisher: string;
}
/** Application reference in quest configuration. */
export interface QuestApplication {
/** Discord application ID. */
id: string;
/** Application display name. */
name: string;
}
/** Platform-specific app store configuration. */
export interface QuestCtaPlatformConfig {
/** Android package name. */
androidAppId?: string;
/** iOS App Store ID. */
iosAppId?: string;
}
/** Call-to-action button configuration. */
export interface QuestCtaConfig {
/** CTA destination URL. */
link: string;
/** Button label text. */
buttonLabel: string;
/** Optional subtitle text. */
subtitle?: string;
/** Android-specific configuration. */
android?: QuestCtaPlatformConfig;
/** iOS-specific configuration. */
ios?: QuestCtaPlatformConfig;
}
/** Co-sponsor branding metadata for partnered quests. */
export interface QuestCosponsorMetadata {
/** Co-sponsor company name. */
name: string;
/** Logotype image URL. */
logotype: string;
/** Redemption instructions text. */
redemptionInstructions: string;
/** Light theme logotype URL. */
logotypeLight: string;
/** Dark theme logotype URL. */
logotypeDark: string;
}
/** Complete quest configuration. */
export interface QuestConfig {
/** Quest ID (snowflake). */
id: string;
/** Configuration version number. */
configVersion: number;
/** ISO timestamp when quest becomes available. */
startsAt: string;
/** ISO timestamp when quest expires. */
expiresAt: string;
/** Enabled feature flags for this quest. */
features: QuestFeature[];
/** Associated Discord application. */
application: QuestApplication;
/** Visual assets. */
assets: QuestAssets;
/** Color scheme. */
colors: QuestColors;
/** Display messages. */
messages: QuestMessages;
/** Task requirements configuration. */
taskConfigV2: QuestTaskConfig;
/** Rewards configuration. */
rewardsConfig: QuestRewardsConfig;
/** Optional co-sponsor branding. */
cosponsorMetadata?: QuestCosponsorMetadata;
/** Sharing policy. */
sharePolicy: QuestSharePolicy;
/** Call-to-action configuration. */
ctaConfig: QuestCtaConfig;
}
/** Heartbeat tracking for quest progress. Updated via QUESTS_SEND_HEARTBEAT_SUCCESS. */
export interface QuestProgressHeartbeat {
/** ISO timestamp of last heartbeat. */
lastBeatAt: string;
/** ISO timestamp when heartbeat tracking expires. */
expiresAt: string | null;
}
/** Progress tracking for a single quest task. Updated via QUESTS_USER_STATUS_UPDATE. */
export interface QuestTaskProgress {
/** Task event name matching {@link QuestTaskType}. */
eventName: string;
/** Current progress value (seconds played/watched). */
value: number;
/** ISO timestamp of last progress update. */
updatedAt: string;
/** ISO timestamp when task was completed, null if incomplete. */
completedAt: string | null;
/** Heartbeat tracking for active progress. */
heartbeat: QuestProgressHeartbeat | null;
}
/** User's status for a specific quest. Updated via QUESTS_USER_STATUS_UPDATE. */
export interface QuestUserStatus {
/** User ID (snowflake). */
userId: string;
/** Quest ID (snowflake). */
questId: string;
/** ISO timestamp when user enrolled. */
enrolledAt: string;
/** ISO timestamp when quest was completed, null if incomplete. */
completedAt: string | null;
/** ISO timestamp when reward was claimed, null if unclaimed. */
claimedAt: string | null;
/** Claimed reward tier for tiered rewards, null otherwise. */
claimedTier: number | null;
/** ISO timestamp of last stream heartbeat. */
lastStreamHeartbeatAt: string | null;
/** Total stream progress in seconds. */
streamProgressSeconds: number;
/** Bitmask of dismissed UI content. */
dismissedQuestContent: QuestDismissibleContentFlags;
/** Progress by task event name. */
progress: Record<string, QuestTaskProgress>;
}
/** Quest object with configuration and user status. */
export interface Quest {
/** Quest ID (snowflake). */
id: string;
/** Whether this is a preview/test quest. */
preview: boolean;
/** Quest configuration. */
config: QuestConfig;
/** User's status, null if not enrolled. */
userStatus: QuestUserStatus | null;
/** UI placements where this quest should appear. */
targetedContent: QuestTargetedContent[];
}
/** Reference to an excluded quest. */
export interface ExcludedQuest {
/** Excluded quest ID. */
id: string;
/** Replacement quest ID to show instead. */
replacementId: string;
}
/** Ad tracking identifiers for quest delivery. */
export interface QuestAdIdentifiers {
ad_id?: string;
adset_id?: string;
ad_set_id?: string;
campaign_id?: string;
creative_id?: string;
creative_type?: string;
/** Request ID from ad decision. */
decision_id?: string;
/** Whether quest was served via targeting. */
is_targeted: boolean;
}
/** Quest delivery information for a placement. */
export interface QuestDeliveryInfo {
/** Quest to deliver. */
quest: Quest;
/** Ad tracking data. */
adDecisionData: QuestAdIdentifiers;
/** Ad context for analytics. */
adContext: string | null;
/** Raw metadata for ad attribution. */
metadataRaw: string | null;
/** Sealed metadata for verification. */
metadataSealed: string | null;
}
/** Ad decision cache entry for quest delivery. */
export interface QuestAdDecision {
/** Quest ID or null if no quest to deliver. */
questId: string | null;
/** Unix timestamp when decision was fetched. */
fetchedAt: number;
/** Cache TTL in milliseconds. Default 6 hours. */
ttlMillis: number;
/** Ad tracking data. */
adDecisionData: QuestAdIdentifiers;
adContext: string | null;
metadataRaw: string | null;
metadataSealed: string | null;
}
/** Stream heartbeat failure tracking. Updated via QUESTS_SEND_HEARTBEAT_FAILURE. */
export interface StreamHeartbeatFailure {
/** Quest ID. */
questId: string;
/** Stream identifier. */
streamKey: string;
/** Unix timestamp of first failure. */
firstFailedAt: number;
}
/** Asset for quest home takeover display. */
export interface QuestHomeTakeoverAsset {
/** Accessibility text. */
altText: string;
/** Asset type identifier. */
assetType: string;
/** Asset URL. */
url: string;
}
/** Sponsor CTA for quest home takeover. */
export interface QuestHomeTakeoverCtaSponsor {
ctaType: string;
title: string;
/** Sponsor destination URL. */
url: string;
}
/** Quest CTA for quest home takeover. */
export interface QuestHomeTakeoverCtaQuest {
ctaType: string;
title: string;
/** Quest ID to navigate to. */
questId: string;
}
/** Quest home takeover banner configuration. */
export interface QuestHomeTakeoverConfig {
placementType: QuestHomePlacement;
/** Campaign identifier. */
campaignId: string;
labelTitle: string;
labelSubtitle: string;
/** Hero banner asset. */
assetHeroImage: QuestHomeTakeoverAsset;
/** Sponsor logo asset. */
assetSponsorImage: QuestHomeTakeoverAsset;
/** Sponsor link CTA. */
ctaSponsorUrl: QuestHomeTakeoverCtaSponsor;
/** Quest navigation CTAs. */
ctaQuests: QuestHomeTakeoverCtaQuest[];
/** ISO timestamp when takeover starts. */
startsAt: string;
/** ISO timestamp when takeover expires. */
expiresAt: string;
}
/** Claimed reward code details. Returned by QUESTS_FETCH_REWARD_CODE_SUCCESS. */
export interface QuestClaimedRewardCode {
userId: string;
questId: string;
/** The reward code string. */
code: string;
/** Platform the code is for. */
platform: QuestPlatform;
/** ISO timestamp when claimed. */
claimedAt: string;
/** Reward tier if applicable. */
tier: number | null;
}
/** Entitlement item from reward claim. */
export interface QuestEntitlementItem {
/** SKU ID. */
skuId: string;
/** Metadata containing reward details. */
tenantMetadata: {
questRewards?: {
reward: {
tag: QuestRewardType;
rewardCode?: QuestClaimedRewardCode;
};
};
} | null;
/** Whether entitlement has been consumed. */
consumed: boolean;
}
/** Error from reward claim operation. */
export interface QuestEntitlementError {
code?: string;
message?: string;
}
/** Response from claiming quest rewards. Returned by QUESTS_CLAIM_REWARD_SUCCESS. */
export interface QuestEntitlements {
/** ISO timestamp when claimed. */
claimedAt: string;
/** Entitlement items received. */
items: QuestEntitlementItem[];
/** Errors encountered during claim. */
errors: QuestEntitlementError[];
}
/** Video progress tracking state. */
export interface QuestVideoProgress {
/** Current playback position in seconds. */
timestampSec: number;
/** Total video duration in seconds. */
duration: number;
/** Maximum position reached in seconds. */
maxTimestampSec: number;
}
/** Transcript asset for quest videos. */
export interface QuestTranscriptAsset {
questId: string;
fetchStatus: QuestTranscriptFetchStatus;
/** Transcript text content when loaded. */
text?: string;
/** Transcript file URL. */
url?: string;
}
/** Error hint from console quest operations. */
export interface QuestConsoleErrorHint {
type: QuestErrorType;
/** User-facing error message. */
message: string;
/** Connected account ID that caused the error. */
connected_account_id: string;
/** Account type (e.g., "xbox", "playstation"). */
connected_account_type: string;
}
/** Computed task details for display. */
export interface QuestTaskDetails {
taskType: QuestTaskType;
/** Target completion time in minutes. */
targetMinutes: number;
/** Progress as decimal (0.0 to 1.0). */
percentComplete: number;
/** Applications for this task. */
applications?: QuestTaskApplication[];
}
/** Third-party task details for external quests. */
export interface QuestThirdPartyTaskDetails {
description: string;
/** Current progress value. */
progress: number;
/** Target completion value. */
target: number;
/** Progress as decimal (0.0 to 1.0). */
percentComplete: number;
title?: string;
}
/** Store for Discord Quests data. Manages quest configs, user progress, rewards, and delivery. */
export class QuestStore extends FluxStore {
/** All quests keyed by quest ID. Updated via QUESTS_FETCH_CURRENT_QUESTS_SUCCESS. */
get quests(): Map<string, Quest>;
/** Quests the user is excluded from. Updated via QUESTS_FETCH_CURRENT_QUESTS_SUCCESS. */
get excludedQuests(): Map<string, ExcludedQuest>;
/** Quests with claimed rewards. Updated via QUESTS_FETCH_CLAIMED_QUESTS_SUCCESS. */
get claimedQuests(): Map<string, Quest>;
/** Whether fetching current quests. Set via QUESTS_FETCH_CURRENT_QUESTS_BEGIN. */
get isFetchingCurrentQuests(): boolean;
/** Whether fetching claimed quests. Set via QUESTS_FETCH_CLAIMED_QUESTS_BEGIN. */
get isFetchingClaimedQuests(): boolean;
/** Unix timestamp of last current quests fetch. Default 0. */
get lastFetchedCurrentQuests(): number;
/** Unix timestamp of last quest-to-deliver fetch. Default 0. */
get lastFetchedQuestToDeliver(): number;
/** Whether fetching quest to deliver. Set via QUESTS_FETCH_QUEST_TO_DELIVER_BEGIN. */
get isFetchingQuestToDeliver(): boolean;
/** Override quest for delivery testing. Set via QUESTS_DELIVERY_OVERRIDE. */
get questDeliveryOverride(): Quest | undefined;
/** Quest delivery info keyed by {@link QuestPlacement}. Updated via QUESTS_FETCH_QUEST_TO_DELIVER_SUCCESS. */
get questToDeliverForPlacement(): Map<QuestPlacement, QuestDeliveryInfo>;
/** Date until enrollment is blocked, null if not blocked. Set via QUESTS_ENROLL_FAILURE. */
get questEnrollmentBlockedUntil(): Date | null;
/** Ad decision cache keyed by {@link QuestPlacement}. Updated via QUESTS_FETCH_QUEST_TO_DELIVER_SUCCESS. */
get questAdDecisionByPlacement(): Map<QuestPlacement, QuestAdDecision>;
/** Quest configs keyed by quest ID. */
get questConfigs(): Map<string, QuestConfig>;
/** Whether fetching preview for a quest. */
isFetchingQuestPreview(questId: string): boolean;
/** Whether fetching quest to deliver for a placement. */
isFetchingQuestToDeliverByPlacement(placement: QuestPlacement): boolean;
/** Gets preview fetch error for a quest. */
getFetchQuestPreviewError(questId: string): QuestErrorType | undefined;
/** Whether user is currently enrolling in a quest. Set via QUESTS_ENROLL_BEGIN. */
isEnrolling(questId: string): boolean;
/** Whether user is currently claiming reward for a quest. Set via QUESTS_CLAIM_REWARD_BEGIN. */
isClaimingReward(questId: string): boolean;
/** Whether fetching reward code for a quest. Set via QUESTS_FETCH_REWARD_CODE_BEGIN. */
isFetchingRewardCode(questId: string): boolean;
/** Whether dismissing content for a quest. Set via QUESTS_DISMISS_CONTENT_BEGIN. */
isDismissingContent(questId: string): boolean;
/** Gets claimed reward code for a quest. Updated via QUESTS_FETCH_REWARD_CODE_SUCCESS. */
getRewardCode(questId: string): QuestClaimedRewardCode | undefined;
/** Gets entitlement items for a quest. Updated via QUESTS_CLAIM_REWARD_SUCCESS. */
getRewards(questId: string): QuestEntitlementItem[] | undefined;
/** Gets stream heartbeat failure info. Cleared via STREAM_CLOSE. */
getStreamHeartbeatFailure(streamKey: string): StreamHeartbeatFailure | undefined;
/** Gets a quest by ID. */
getQuest(questId: string): Quest | undefined;
/** Gets quest config by ID. */
getQuestConfig(questId: string): QuestConfig | undefined;
/** Whether quest has active desktop progress. Updated via QUESTS_SEND_HEARTBEAT_SUCCESS. */
isProgressingOnDesktop(questId: string): boolean;
/** Gets selected task platform for a quest. Set via QUESTS_SELECT_TASK_PLATFORM. */
selectedTaskPlatform(questId: string): QuestPlatformMode | null;
/** Gets optimistic progress for a task. Set via QUESTS_UPDATE_OPTIMISTIC_PROGRESS, cleared via QUESTS_RESET_OPTIMISTIC_PROGRESS. */
getOptimisticProgress(questId: string, taskEventName: string): number | undefined;
/** Gets map of quest IDs to expired status. Computed from quest config expiresAt. */
getExpiredQuestsMap(): Map<string, boolean>;
/** Whether a quest has expired. Computed from quest config expiresAt. */
isQuestExpired(questId: string): boolean;
/** Gets quest loaded via preview tool. Updated via QUESTS_FETCH_PREVIEW_SUCCESS. */
getQuestLoadedViaPreview(questId: string): Quest | undefined;
/** Whether fetching quest home takeover config. Set via QUESTS_FETCH_QUEST_HOME_TAKEOVER_BEGIN. */
isFetchingQuestHomeTakeover(): boolean;
/** Gets quest home takeover config. Updated via QUESTS_FETCH_QUEST_HOME_TAKEOVER_SUCCESS. */
getQuestHomeTakeoverConfig(): QuestHomeTakeoverConfig | null;
/** Gets Unix timestamp of last takeover fetch, null if never fetched. */
getLastFetchedQuestHomeTakeover(): number | null;
}

View file

@ -1,4 +1,4 @@
import { FluxStore, Guild, User, Application, ApplicationInstallParams } from "..";
import { FluxStore, Guild, User, Application, ApplicationInstallParams, ProfileEffect } from "..";
import { ApplicationFlags, ApplicationIntegrationType } from "../../enums";
export interface MutualFriend {
@ -70,9 +70,11 @@ export interface UserProfileBase extends Pick<User, "banner"> {
*/
badges: ProfileBadge[];
bio: string | undefined;
collectibles: ProfileEffect[];
popoutAnimationParticleType: string | null;
profileEffectExpiresAt: number | Date | undefined;
profileEffectId: undefined | string;
profileEffect: ProfileEffect;
/**
* often an empty string when not set
*/

View file

@ -1,12 +1,14 @@
// please keep in alphabetical order
export * from "./AccessibilityStore";
export * from "./ActiveJoinedThreadsStore";
export * from "./ApplicationCommandIndexStore";
export * from "./ApplicationStore";
export * from "./AuthenticationStore";
export * from "./CallStore";
export * from "./ChannelRTCStore";
export * from "./ChannelStore";
export * from "./DraftStore";
export * from "./EditMessageStore";
export * from "./EmojiStore";
export * from "./FluxStore";
export * from "./FriendsStore";
@ -24,7 +26,9 @@ export * from "./MessageStore";
export * from "./NotificationSettingsStore";
export * from "./OverridePremiumTypeStore";
export * from "./PermissionStore";
export * from "./PopoutWindowStore";
export * from "./PresenceStore";
export * from "./QuestStore";
export * from "./ReadStateStore";
export * from "./RelationshipStore";
export * from "./RTCConnectionStore";

View file

@ -232,34 +232,7 @@ export interface ExpressionPickerStore {
useExpressionPickerStore<T>(selector: (state: ExpressionPickerStoreState) => T): T;
}
export interface BrowserWindowFeatures {
toolbar?: boolean;
menubar?: boolean;
location?: boolean;
directories?: boolean;
width?: number;
height?: number;
defaultWidth?: number;
defaultHeight?: number;
left?: number;
top?: number;
defaultAlwaysOnTop?: boolean;
movable?: boolean;
resizable?: boolean;
frame?: boolean;
alwaysOnTop?: boolean;
hasShadow?: boolean;
transparent?: boolean;
skipTaskbar?: boolean;
titleBarStyle?: string | null;
backgroundColor?: string;
}
export interface PopoutActions {
open(key: string, render: (windowKey: string) => ReactNode, features?: BrowserWindowFeatures);
close(key: string): void;
setAlwaysOnTop(key: string, alwaysOnTop: boolean): void;
}
export { BrowserWindowFeatures, PopoutActions } from "./stores/PopoutWindowStore";
export type UserNameUtilsTagInclude = LiteralUnion<"auto" | "always" | "never", string>;
export interface UserNameUtilsTagOptions {

View file

@ -1,7 +1,7 @@
{
"name": "@equicord/types",
"private": false,
"version": "1.13.9",
"version": "1.14.1",
"description": "",
"types": "index.d.ts",
"scripts": {
@ -29,4 +29,4 @@
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1"
}
}
}

45
pnpm-lock.yaml generated
View file

@ -25,6 +25,9 @@ importers:
'@intrnl/xxhash64':
specifier: ^0.1.2
version: 0.1.2
'@streamparser/json':
specifier: ^0.0.22
version: 0.0.22
'@types/less':
specifier: ^3.0.8
version: 3.0.8(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076)
@ -58,6 +61,9 @@ importers:
nanoid:
specifier: ^5.1.6
version: 5.1.6
native-file-system-adapter:
specifier: ^3.0.1
version: 3.0.1
usercss-meta:
specifier: ^0.12.0
version: 0.12.0
@ -712,6 +718,9 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@streamparser/json@0.0.22':
resolution: {integrity: sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==}
'@stylistic/eslint-plugin@5.6.0':
resolution: {integrity: sha512-owEc4B8ME+O/xyZOkLVyLqPMsUgJXIM4XzCm5Vt3WvRXpyoOfYxgA+JkEiFqXPCI8+Nc2BzAT+KGAK7QleGs8Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1445,6 +1454,10 @@ packages:
fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@ -1959,6 +1972,10 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
native-file-system-adapter@3.0.1:
resolution: {integrity: sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==}
engines: {node: '>=14.8.0'}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@ -1969,6 +1986,11 @@ packages:
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@ -2143,6 +2165,7 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
deprecated: |-
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
qified@0.5.2:
@ -2534,6 +2557,10 @@ packages:
vscode-textmate@5.2.0:
resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webdriver-bidi-protocol@0.3.8:
resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==}
@ -2968,6 +2995,8 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@streamparser/json@0.0.22': {}
'@stylistic/eslint-plugin@5.6.0(eslint@9.39.1)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
@ -3908,6 +3937,12 @@ snapshots:
dependencies:
pend: 1.2.0
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
optional: true
fflate@0.8.2: {}
file-entry-cache@10.1.4:
@ -4421,6 +4456,10 @@ snapshots:
nanoid@5.1.6: {}
native-file-system-adapter@3.0.1:
optionalDependencies:
fetch-blob: 3.2.0
natural-compare@1.4.0: {}
netmask@2.0.2: {}
@ -4430,6 +4469,9 @@ snapshots:
lower-case: 2.0.2
tslib: 2.6.2
node-domexception@1.0.0:
optional: true
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
@ -5134,6 +5176,9 @@ snapshots:
vscode-textmate@5.2.0: {}
web-streams-polyfill@3.3.3:
optional: true
webdriver-bidi-protocol@0.3.8: {}
which-boxed-primitive@1.1.1:

View file

@ -36,7 +36,7 @@ const PackageJSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) * 1000 || Date.now();
export const watch = process.argv.includes("--watch");
export const IS_DEV = watch || process.argv.includes("--dev");

View file

@ -27,6 +27,7 @@ import { fileURLToPath } from "url";
const BASE_URL = "https://github.com/Equicord/Equilotl/releases/latest/download/";
const INSTALLER_PATH_DARWIN = "Equilotl.app/Contents/MacOS/Equilotl";
const INSTALLER_APP_DARWIN = "Equilotl.app";
const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
const FILE_DIR = join(BASE_DIR, "dist", "Installer");
@ -53,8 +54,11 @@ async function ensureBinary() {
const downloadName = join(FILE_DIR, filename);
const outputFile = process.platform === "darwin"
? join(FILE_DIR, "Equilotl")
? join(FILE_DIR, INSTALLER_PATH_DARWIN)
: downloadName;
const outputApp = process.platform === "darwin"
? join(FILE_DIR, INSTALLER_APP_DARWIN)
: null;
const etag = existsSync(outputFile) && existsSync(ETAG_FILE)
? readFileSync(ETAG_FILE, "utf-8")
@ -77,17 +81,14 @@ async function ensureBinary() {
writeFileSync(ETAG_FILE, res.headers.get("etag"));
if (process.platform === "darwin") {
console.log("Unzipping...");
console.log("Saving zip...");
const zip = new Uint8Array(await res.arrayBuffer());
writeFileSync(downloadName, zip);
const ff = await import("fflate");
const bytes = ff.unzipSync(zip, {
filter: f => f.name === INSTALLER_PATH_DARWIN
})[INSTALLER_PATH_DARWIN];
console.log("Unzipping app bundle...");
execSync(`ditto -x -k '${downloadName}' '${FILE_DIR}'`);
writeFileSync(outputFile, bytes, { mode: 0o755 });
console.log("Overriding security policy for installer binary (this is required to run it)");
console.log("Clearing quarantine from installer app (this is required to run it)");
console.log("xattr might error, that's okay");
const logAndRun = cmd => {
@ -96,8 +97,7 @@ async function ensureBinary() {
execSync(cmd);
} catch { }
};
logAndRun(`sudo spctl --add '${outputFile}' --label "Equilotl"`);
logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);
logAndRun(`sudo xattr -dr com.apple.quarantine '${outputApp}'`);
} else {
// WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD
const body = Readable.fromWeb(res.body);

View file

@ -34,17 +34,18 @@ import { debounce } from "@shared/debounce";
import { IS_WINDOWS } from "@utils/constants";
import { createAndAppendStyle } from "@utils/css";
import { StartAt } from "@utils/types";
import { SettingsRouter } from "@webpack/common";
import { get as dsGet } from "./api/DataStore";
import { popNotice, showNotice } from "./api/Notices";
import { NotificationData, showNotification } from "./api/Notifications";
import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager";
import { PlainSettings, Settings, SettingsStore } from "./api/Settings";
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";
import { checkForUpdates, isOutdated as getIsOutdated, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { openUserSettingsPanel } from "./webpack/common";
import { patches } from "./webpack/patchWebpack";
if (IS_REPORTER) {
@ -65,7 +66,7 @@ async function syncSettings() {
title: "Cloud Settings",
body: "Cloud sync was disabled because this account isn't connected to the cloud App. You can enable it again by connecting this account in Cloud Settings. (note: it will store your preferences separately)",
color: "var(--yellow-360)",
onClick: () => openUserSettingsPanel("equicord_cloud")
onClick: () => SettingsRouter.openUserSettings("equicord_cloud_panel")
});
// Disable cloud sync globally
Settings.cloud.authenticated = false;
@ -84,7 +85,7 @@ async function syncSettings() {
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " +
"need to re-authenticate to continue using them. Click here to go to the settings page to do so!",
color: "var(--yellow-360)",
onClick: () => openUserSettingsPanel("equicord_cloud")
onClick: () => SettingsRouter.openUserSettings("equicord_cloud_panel")
});
return;
}
@ -140,35 +141,74 @@ async function runUpdateCheck() {
try {
const isOutdated = await checkForUpdates();
if (IS_DISCORD_DESKTOP) VencordNative.tray.setUpdateState(isOutdated);
if (!isOutdated) return;
if (Settings.autoUpdate) {
await update();
if (Settings.autoUpdateNotification) {
notify({
title: "Equicord has been updated!",
body: "Click here to restart",
onClick: relaunch
});
if (notifiedForUpdatesThisSession) return;
notifiedForUpdatesThisSession = true;
showNotice(
"Equicord has been updated!",
"Restart",
relaunch
);
}
return;
}
notify({
title: "A Equicord update is available!",
body: "Click here to view the update",
onClick: () => openSettingsTabModal(UpdaterTab!)
});
if (notifiedForUpdatesThisSession) return;
notifiedForUpdatesThisSession = true;
showNotice(
"A new version of Equicord is available!",
"View Update",
() => openSettingsTabModal(UpdaterTab!)
);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
}
function initTrayIpc() {
if (IS_WEB || IS_UPDATER_DISABLED) return;
VencordNative.tray.onCheckUpdates(async () => {
try {
const isOutdated = await checkForUpdates();
VencordNative.tray.setUpdateState(isOutdated);
if (isOutdated) {
showNotice("An Equicord update is available!", "View Update", () => openSettingsTabModal(UpdaterTab!));
} else {
showNotice("No updates available, you're on the latest version!", "OK", popNotice);
}
} catch (err) {
UpdateLogger.error("Failed to check for updates from tray", err);
showNotice("Failed to check for updates, check the console for more info", "OK", popNotice);
}
});
VencordNative.tray.onRepair(async () => {
try {
await update();
relaunch();
} catch (err) {
UpdateLogger.error("Failed to repair Equicord", err);
}
});
VencordNative.tray.setUpdateState(getIsOutdated);
}
async function init() {
await onceReady;
startAllPlugins(StartAt.WebpackReady);
syncSettings();
initTrayIpc();
if (!IS_DEV && !IS_WEB && !IS_UPDATER_DISABLED) {
runUpdateCheck();

View file

@ -34,9 +34,7 @@ export default {
uploadTheme: async (fileName: string, fileData: string): Promise<void> => {
throw new Error("uploadTheme is WEB only");
},
deleteTheme: async (fileName: string): Promise<void> => {
throw new Error("deleteTheme is WEB only");
},
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
getThemesList: () => invoke<Array<{ fileName: string; content: string; }>>(IpcEvents.GET_THEMES_LIST),
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
@ -100,5 +98,12 @@ export default {
invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),
},
tray: {
setUpdateState: (available: boolean) => ipcRenderer.send(IpcEvents.SET_TRAY_UPDATE_STATE, available),
onCheckUpdates: (cb: () => void) => { ipcRenderer.on(IpcEvents.TRAY_CHECK_UPDATES, cb); },
onRepair: (cb: () => void) => { ipcRenderer.on(IpcEvents.TRAY_REPAIR, cb); },
onAbout: (cb: () => void) => { ipcRenderer.on(IpcEvents.TRAY_ABOUT, cb); },
},
pluginHelpers: PluginHelpers
};

View file

@ -235,7 +235,7 @@ export function identifyAudioType(audio: string): AudioType {
/**
* Adds a function to process an audio before it is played.
* @param key A unique identifier for this audio processor. Plugin name is recommended.
* @param processor A function that takes an audio, volume (0-100), and type (AudioType), and returns the replacement audio and volume.
* @param processor A function that takes a data object with audio, volume (0-100), and type (AudioType) attributes, and modifies the audio and volume in place.
*/
export function addAudioProcessor(key: string, processor: AudioProcessor): void {
audioProcessorFunctions[key] = processor;

View file

@ -51,7 +51,7 @@ export interface ProfileBadge {
/**
* Allows dynamically returning multiple badges.
* May call hooks but then you must not use shouldShow
* Must not call hooks
*/
getBadges?(userInfo: BadgeUserArgs): ProfileBadge[];
}

View file

@ -2,3 +2,29 @@
display: flex;
align-items: center;
}
/* FIXME: bad name
_7f7a2c5f23327d73 */
.vc-chatbar-discord-button {
opacity: .7;
align-items: center;
align-self: center;
border-radius: 8px;
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
&:hover {
background: hsl(0deg 0% 100% / 10%);
opacity: 1
}
}
/* _3f4f204c3133fb93 */
.vc-chatbar-button-wrapper {
align-self: center;
display: flex;
flex-direction: row;
gap: 8px;
}

View file

@ -11,15 +11,15 @@ import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import { IconComponent } from "@utils/types";
import { Channel } from "@vencord/discord-types";
import { waitFor } from "@webpack";
import { ButtonWrapperClasses, Clickable, Menu, Tooltip } from "@webpack/common";
import { findCssClassesLazy } from "@webpack";
import { Clickable, Menu, Tooltip } from "@webpack/common";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
import { addContextMenuPatch, findGroupChildrenByChildId } from "./ContextMenu";
import { useSettings } from "./Settings";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
const ButtonWrapperClasses = findCssClassesLazy("button", "buttonWrapper", "notificationDot");
const ChannelTextAreaClasses = findCssClassesLazy("buttonContainer", "channelTextArea", "button");
export interface ChatBarProps {
channel: Channel;
@ -113,11 +113,9 @@ function VencordChatBarButtons(props: ChatBarProps) {
}
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
if (props.disabled) return;
if (props.disabled || buttons.length === 0) return;
buttons.unshift(<VencordChatBarButtons key="vencord-chat-buttons" {...props} />);
return buttons;
}
/**
@ -140,18 +138,18 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
<Tooltip text={props.tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""}`}>
<Clickable
aria-label={props.tooltip}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={classes(ButtonWrapperClasses?.button, ChannelTextAreaClasses?.button)}
className={classes(ButtonWrapperClasses.button, ChannelTextAreaClasses?.button)}
onClick={props.onClick}
onContextMenu={props.onContextMenu}
onAuxClick={props.onAuxClick}
{...props.buttonProps}
>
<div className={ButtonWrapperClasses?.buttonWrapper}>
<div className={ButtonWrapperClasses.buttonWrapper}>
{props.children}
</div>
</Clickable>
@ -174,7 +172,7 @@ addContextMenuPatch("textarea-context", (children, args) => {
if (idx === -1) return;
group.splice(idx, 0,
<Menu.MenuItem id="vc-chat-buttons" key="vencord-chat-buttons" label="Equicord Buttons">
<Menu.MenuItem id="vc-chat-buttons" key="vencord-chat-buttons" label="Vencord Buttons">
{buttons.map(([id]) => (
<Menu.MenuCheckboxItem
label={id}

View file

@ -6,13 +6,15 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { findComponentByCodeLazy } from "@webpack";
import { useEffect, useState } from "@webpack/common";
import { classes } from "@utils/misc";
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { Clickable, Tooltip, useEffect, useState } from "@webpack/common";
import type { ComponentType, JSX, MouseEventHandler, ReactNode } from "react";
const logger = new Logger("HeaderBarAPI");
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType<HeaderBarButtonProps>;
const HeaderBarClasses = findCssClassesLazy("clickable", "withHighlight");
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '"aria-haspopup":') as ComponentType<ChannelToolbarButtonProps>;
export interface HeaderBarButtonProps {
/** The icon component to render inside the button */
@ -25,6 +27,17 @@ export interface HeaderBarButtonProps {
onContextMenu?: MouseEventHandler<HTMLDivElement>;
/** Additional CSS class names */
className?: string;
/** Size of the icon in pixels */
iconSize?: number;
/** Tooltip position relative to the button */
position?: "top" | "bottom" | "left" | "right";
/** Whether the button appears in a selected/active state */
selected?: boolean;
/** Aria label for accessibility */
"aria-label"?: string;
}
export interface ChannelToolbarButtonProps extends HeaderBarButtonProps {
/** CSS class name for the icon element */
iconClassName?: string;
/** Tooltip position relative to the button */
@ -37,10 +50,6 @@ export interface HeaderBarButtonProps {
showBadge?: boolean;
/** Position of the notification badge */
badgePosition?: "top" | "bottom";
/** Size of the icon in pixels */
iconSize?: number;
/** Ref to the button element */
ref?: React.RefObject<any>;
}
export type HeaderBarButtonFactory = () => JSX.Element | null;
@ -71,7 +80,44 @@ interface ButtonEntry {
* onClick={() => console.log("clicked")}
* />
*/
export const HeaderBarButton = HeaderBarIcon;
export function HeaderBarButton(props: HeaderBarButtonProps & { ref?: React.RefObject<any>; }) {
const {
icon: Icon,
tooltip,
onClick,
onContextMenu,
className,
iconSize = 18,
position = "bottom",
selected,
ref,
"aria-label": ariaLabel,
} = props;
const label = ariaLabel ?? (typeof tooltip === "string" ? tooltip : undefined);
return (
<Tooltip text={tooltip ?? ""} position={position} shouldShow={tooltip != null}>
{({ onMouseEnter, onMouseLeave }) => (
<Clickable
{...{ innerRef: ref } as any}
className={classes(HeaderBarClasses.clickable, HeaderBarClasses.withHighlight, className)}
style={{ width: iconSize, boxSizing: "content-box", justifyContent: "center" }}
onClick={onClick}
onContextMenu={onContextMenu}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
role="button"
tabIndex={0}
aria-label={label}
aria-expanded={selected}
>
<Icon size="custom" width={iconSize} height={iconSize} color="currentColor" />
</Clickable>
)}
</Tooltip>
);
}
/**
* Button component for the channel toolbar (below the search bar).
@ -85,7 +131,7 @@ export const HeaderBarButton = HeaderBarIcon;
* selected={isOpen}
* />
*/
export function ChannelToolbarButton(props: HeaderBarButtonProps) {
export function ChannelToolbarButton(props: ChannelToolbarButtonProps) {
return <HeaderBarIcon {...props} />;
}

View file

@ -136,6 +136,6 @@
}
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--redesign-button-danger-background);
color: var(--control-critical-primary-text-default);
background-color: var(--control-critical-primary-background-default);
}

View file

@ -9,7 +9,7 @@ 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";
import { SettingsRouter } from "@webpack/common";
import { deflateSync, inflateSync } from "fflate";
import { deauthorizeCloud, getCloudAuth, getCloudUrl } from "./cloudSetup";
@ -88,7 +88,7 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
title: "Cloud Settings",
body: "Cloud sync was disabled because this account isn't connected to the cloud App. You can enable it again by connecting this account in Cloud Settings. (note: it will store your preferences separately)",
color: "var(--yellow-360)",
onClick: () => openUserSettingsPanel("equicord_cloud")
onClick: () => SettingsRouter.openUserSettings("equicord_cloud_panel")
});
// Disable cloud sync globally
Settings.cloud.authenticated = false;

View file

@ -28,6 +28,22 @@ const toastFailure = (err: any) =>
const logger = new Logger("SettingsSync:Offline", "#39b7e0");
function deepMerge<T extends object>(target: T, source: T): T {
for (const key in source) {
const sourceVal = source[key];
if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
if (target[key] === null || target[key] === undefined || typeof target[key] !== "object" || Array.isArray(target[key])) {
target[key] = {} as any;
}
deepMerge(target[key] as object, sourceVal as object);
} else {
target[key] = sourceVal;
}
}
return target;
}
function isSafeObject(obj: any) {
if (obj == null || typeof obj !== "object") return true;
@ -60,8 +76,8 @@ export async function importSettings(data: string, type: BackupType = "all", clo
throw new Error("Invalid Settings. Plugin settings is required for this import try a different one.");
if (parsed.settings) {
Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(parsed.settings);
deepMerge(PlainSettings, parsed.settings);
await VencordNative.settings.set(PlainSettings);
}
if (parsed.quickCss) await VencordNative.quickCss.set(parsed.quickCss);
if (parsed.dataStore) await DataStore.setMany(parsed.dataStore);
@ -70,8 +86,8 @@ export async function importSettings(data: string, type: BackupType = "all", clo
case "plugins": {
if (!parsed.settings) throw new Error("Plugin settings missing");
Object.assign(PlainSettings, parsed.settings);
await VencordNative.settings.set(parsed.settings);
deepMerge(PlainSettings, parsed.settings);
await VencordNative.settings.set(PlainSettings);
break;
}
case "css": {

View file

@ -54,7 +54,7 @@ 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;
const vesktopCssNode = (IS_VESKTOP || IS_EQUIBOP) ? createAndAppendStyle("vesktop-css-core", coreStyleRootNode) : null;
createAndAppendStyle("vencord-margins", coreStyleRootNode).textContent = generateMarginCss();
VencordNative.native.getRendererCss().then(css => rendererCssNode.textContent = css);

View file

@ -19,8 +19,9 @@
import { Settings, SettingsStore } from "@api/Settings";
import { createAndAppendStyle } from "@utils/css";
import { ThemeStore } from "@vencord/discord-types";
import { PopoutWindowStore } from "@webpack/common";
import { userStyleRootNode } from "./Styles";
import { userStyleRootNode, vencordRootNode } from "./Styles";
let style: HTMLStyleElement;
let themesStyle: HTMLStyleElement;
@ -33,6 +34,7 @@ async function toggle(isEnabled: boolean) {
style.textContent = css;
// At the time of writing this, changing textContent resets the disabled state
style.disabled = !Settings.useQuickCss;
updatePopoutWindows();
});
style.textContent = await VencordNative.quickCss.get();
}
@ -76,6 +78,27 @@ async function initThemes() {
}
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
updatePopoutWindows();
}
function applyToPopout(popoutWindow: Window | undefined, key: string) {
if (!popoutWindow?.document) return;
// skip game overlay cuz it needs to stay transparent, themes broke it
if (key === "DISCORD_OutOfProcessOverlay") return;
const doc = popoutWindow.document;
doc.querySelector("vencord-root")?.remove();
doc.documentElement.appendChild(vencordRootNode.cloneNode(true));
}
function updatePopoutWindows() {
if (!PopoutWindowStore) return;
for (const key of PopoutWindowStore.getWindowKeys()) {
applyToPopout(PopoutWindowStore.getWindow(key), key);
}
}
document.addEventListener("DOMContentLoaded", () => {
@ -89,6 +112,13 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("enabledThemeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
window.addEventListener("message", event => {
const { discordPopoutEvent } = event.data || {};
if (discordPopoutEvent?.type !== "loaded") return;
applyToPopout(PopoutWindowStore.getWindow(discordPopoutEvent.key), discordPopoutEvent.key);
});
if (!IS_WEB) {
VencordNative.quickCss.addThemeChangeListener(initThemes);
}

View file

@ -9,7 +9,7 @@ import { Logger } from "@utils/Logger";
import { findComponentByCodeLazy } from "@webpack";
import type { ComponentType, MouseEventHandler, ReactNode } from "react";
const PanelButton = findComponentByCodeLazy("tooltipPositionKey", ".greenTooltip") as ComponentType<UserAreaButtonProps>;
const PanelButton = findComponentByCodeLazy("tooltipPositionKey", "positionKeyStemOverride") as ComponentType<UserAreaButtonProps>;
export interface UserAreaButtonProps {
icon: ReactNode;

54
src/components/Badge.css Normal file
View file

@ -0,0 +1,54 @@
.vc-badge-base {
display: inline-flex;
align-items: center;
align-self: center;
gap: var(--space-4, 4px);
padding: 0 var(--space-8, 8px);
border-radius: var(--radius-round, 9999px);
box-sizing: border-box;
font-family: var(--font-display);
font-size: 12px;
font-weight: 700;
line-height: 1.3333;
letter-spacing: 0.02em;
text-align: center;
text-transform: uppercase;
}
.vc-badge-icon {
width: 12px;
height: 12px;
flex-shrink: 0;
}
/* Discord variants */
.vc-badge-default {
background-color: var(--badge-background-brand);
color: var(--badge-text-brand);
}
.vc-badge-expressive {
background-color: var(--badge-expressive-background-default);
color: var(--badge-expressive-text-default);
}
/* Equicord variants */
.vc-badge-danger {
background-color: var(--status-danger);
color: var(--white);
}
.vc-badge-positive {
background-color: var(--status-positive);
color: var(--white);
}
.vc-badge-warning {
background-color: var(--status-warning);
color: var(--black);
}
.vc-badge-premium {
background-color: var(--premium-tier-2-purple-for-gradients);
color: var(--white);
}

40
src/components/Badge.tsx Normal file
View file

@ -0,0 +1,40 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./Badge.css";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import type { ComponentPropsWithoutRef, ComponentType, CSSProperties, ReactNode, SVGProps } from "react";
const cl = classNameFactory("vc-badge-");
export const BadgeVariants = ["default", "expressive", "danger", "positive", "warning", "premium"] as const;
export type BadgeVariant = typeof BadgeVariants[number];
export type BadgeProps = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
text: ReactNode;
variant?: BadgeVariant;
icon?: ComponentType<SVGProps<SVGSVGElement> & { size?: string; }>;
color?: string;
backgroundColor?: string;
};
export function Badge({ text, variant = "default", icon: Icon, className, color, backgroundColor, style, ...restProps }: BadgeProps) {
const customStyle: CSSProperties | undefined = (color || backgroundColor) ? {
...style,
...(color && { color }),
...(backgroundColor && { backgroundColor })
} : style;
return (
<div className={classes(cl("base", variant), className)} style={customStyle} {...restProps}>
{Icon && <Icon size="xxs" className={cl("icon")} />}
{text}
</div>
);
}

View file

@ -1,8 +1,26 @@
.vc-text-base {
font-family: var(--font-primary);
color: var(--text-default);
line-height: normal;
/* Discord puts an insane default margin on p tags, so reset that here */
margin: 0;
}
}
.vc-text-selectable {
user-select: text;
}
.vc-text-line-clamp-1 {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vc-text-line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.vc-text-tabular-numbers {
font-variant-numeric: tabular-nums;
}

View file

@ -14,13 +14,13 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
const textCls = classNameFactory("vc-text-");
export const TextSizes = {
xxs: "0.625rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.25rem",
xl: "1.5rem",
xxl: "2rem"
xxs: { fontSize: "10px", lineHeight: "1.2" },
xs: { fontSize: "12px", lineHeight: "1.33333" },
sm: { fontSize: "14px", lineHeight: "1.28571" },
md: { fontSize: "16px", lineHeight: "1.25" },
lg: { fontSize: "20px", lineHeight: "1.2" },
xl: { fontSize: "24px", lineHeight: "1.25" },
xxl: { fontSize: "32px", lineHeight: "1.25" },
} as const;
export const TextWeights = {
@ -42,6 +42,15 @@ export const TextColors = {
"text-brand": "var(--text-brand)",
"text-strong": "var(--text-strong)",
"text-subtle": "var(--text-subtle)",
"text-invert": "var(--text-invert)",
"text-feedback-critical": "var(--text-feedback-critical)",
"text-feedback-info": "var(--text-feedback-info)",
"text-feedback-positive": "var(--text-feedback-positive)",
"text-feedback-warning": "var(--text-feedback-warning)",
"text-status-dnd": "var(--text-status-dnd)",
"text-status-idle": "var(--text-status-idle)",
"text-status-offline": "var(--text-status-offline)",
"text-status-online": "var(--text-status-online)",
"control-text-critical": "var(--control-text-critical-secondary-default)",
"control-text-primary": "var(--control-text-primary-default)",
} as const;
@ -49,8 +58,8 @@ export const TextColors = {
export function generateTextCss() {
let css = "";
for (const [size, value] of Object.entries(TextSizes)) {
css += `.${textCls(size)}{font-size:${value};}`;
for (const [size, { fontSize, lineHeight }] of Object.entries(TextSizes)) {
css += `.${textCls(size)}{font-size:${fontSize};line-height:${lineHeight};}`;
}
for (const [weight, value] of Object.entries(TextWeights)) {
@ -70,6 +79,9 @@ export type BaseTextProps<Tag extends TextTag = "div"> = ComponentPropsWithoutRe
weight?: TextWeight;
color?: TextColor;
tag?: Tag;
selectable?: boolean;
lineClamp?: number;
tabularNumbers?: boolean;
};
export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): ReactNode {
@ -78,6 +90,9 @@ export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): Re
weight = "normal",
color,
tag: Tag = "div",
selectable = false,
lineClamp,
tabularNumbers = false,
children,
className,
style,
@ -86,10 +101,18 @@ export function BaseText<T extends TextTag = "div">(props: BaseTextProps<T>): Re
return (
<Tag
className={classes(textCls("base", size, weight), className)}
className={classes(
textCls("base", size, weight),
selectable && textCls("selectable"),
lineClamp === 1 && textCls("line-clamp-1"),
lineClamp != null && lineClamp > 1 && textCls("line-clamp"),
tabularNumbers && textCls("tabular-numbers"),
className
)}
style={{
...style,
...(color && { color: TextColors[color] })
...(color && { color: TextColors[color] }),
...(lineClamp && lineClamp > 1 && { WebkitLineClamp: lineClamp })
}}
{...restProps}
>

View file

@ -62,6 +62,26 @@
line-height: 1.25;
}
.vc-btn-iconOnly {
width: 32px;
height: 32px;
min-width: unset;
min-height: unset;
padding: 0;
background-color: transparent;
border-color: transparent;
&:hover {
background-color: var(--control-icon-only-background-hover);
border-color: var(--control-icon-only-border-hover);
}
&:active {
background-color: var(--control-icon-only-background-active);
border-color: var(--control-icon-only-border-active);
}
}
.vc-btn-primary {
background-color: var(--control-primary-background-default);
border-color: var(--control-primary-border-default);
@ -124,11 +144,11 @@
}
.vc-btn-positive {
background-color: var(--redesign-button-positive-background, var(--green-430));
background-color: var(--control-connected-background-default, var(--green-430));
color: var(--white);
&:hover {
background-color: var(--redesign-button-positive-pressed-background, var(--green-460));
background-color: var(--control-connected-background-hover, var(--green-460));
}
}
@ -195,4 +215,4 @@
.vc-text-btn-link {
color: var(--text-link);
}
}

View file

@ -19,7 +19,7 @@ const textBtnCls = classNameFactory("vc-text-btn-");
export type ButtonVariant =
"primary" | "secondary" | "dangerPrimary" | "dangerSecondary" | "overlayPrimary" | "positive" | "link" | "none";
export type ButtonSize = "min" | "xs" | "small" | "medium";
export type ButtonSize = "min" | "xs" | "small" | "medium" | "iconOnly";
export type ButtonProps = ComponentPropsWithRef<"button"> & {
variant?: ButtonVariant;

View file

@ -1,20 +1,59 @@
.vc-card-base {
background: var(--card-background-default);
border-radius: var(--radius-sm, 8px)
border-style: solid;
border-width: 1px;
border-radius: var(--radius-sm);
position: relative;
}
.vc-card-normal {
border: 1px solid var(--border-subtle);
}
.vc-card-warning {
border: 1px solid var(--icon-status-idle);
.vc-card-primary {
background-color: var(--card-background-default);
border-color: var(--border-subtle);
color: var(--text-default);
}
.vc-card-danger {
border: 1px solid var(--icon-status-dnd);
background-color: var(--background-feedback-critical);
border-color: var(--background-feedback-critical);
color: var(--text-feedback-critical);
}
.vc-card-warning {
background-color: var(--background-feedback-warning);
border-color: var(--background-feedback-warning);
color: var(--text-feedback-warning);
}
.vc-card-success {
background-color: var(--background-feedback-positive);
border-color: var(--background-feedback-positive);
color: var(--text-feedback-positive);
}
.vc-card-brand {
background-color: var(--brand-500);
border-color: var(--brand-500);
}
.vc-card-outline {
background-color: transparent;
}
.vc-card-base a {
color: var(--text-link);
}
.vc-card-danger a,
.vc-card-warning a,
.vc-card-success a,
.vc-card-brand a {
color: var(--white);
font-weight: var(--font-weight-bold);
}
.vc-card-base a:hover {
text-decoration: underline;
}
.vc-card-defaultPadding {
padding: 1em;
}
}

View file

@ -13,18 +13,19 @@ import { ComponentPropsWithRef } from "react";
const cl = classNameFactory("vc-card-");
export interface CardProps extends ComponentPropsWithRef<"div"> {
variant?: "normal" | "warning" | "danger";
variant?: "primary" | "warning" | "danger" | "success" | "brand";
outline?: boolean;
/** Add a default padding of 1em to the card. This is implied if no className prop is passed */
defaultPadding?: boolean;
}
export function Card({ variant = "normal", defaultPadding, children, className, ...restProps }: CardProps) {
export function Card({ variant = "primary", outline = false, defaultPadding, children, className, ...restProps }: CardProps) {
const addDefaultPadding = defaultPadding != null
? defaultPadding
: !className;
return (
<div className={classes(cl("base", variant, addDefaultPadding && "defaultPadding"), className)} {...restProps}>
<div className={classes(cl("base", variant, outline && "outline", addDefaultPadding && "defaultPadding"), className)} {...restProps}>
{children}
</div>
);

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByPropsLazy } from "@webpack";
import { findCssClassesLazy } from "@webpack";
import { Parser } from "@webpack/common";
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
const CodeContainerClasses = findCssClassesLazy("markup", "codeContainer");
/**
* Renders code in a Discord codeblock
@ -19,3 +19,14 @@ export function CodeBlock(props: { content?: string, lang: string; }) {
</div>
);
}
/**
* Renders inline code like `this`
*/
export function InlineCode({ children }: { children: React.ReactNode; }) {
return (
<span className={CodeContainerClasses.markup}>
<code className="inline">{children}</code>
</span>
);
}

View file

@ -1,51 +1,40 @@
.vc-h1,
.vc-h2 {
color: var(--text-strong);
font-weight: 600;
}
.vc-h2,
.vc-h3,
.vc-h4,
.vc-h5 {
color: var(--text-strong);
font-family: var(--font-display);
}
.vc-h1 {
font-size: 20px;
line-height: 24px
font-weight: 600;
line-height: 1.2;
}
.vc-h2 {
font-size: 16px;
line-height: 20px
font-weight: 600;
line-height: 1.25;
}
.vc-h3 {
font-weight: 500;
line-height: 24px
}
.vc-h3,
.vc-h4 {
font-size: 16px
}
.vc-h4 {
font-weight: 600;
letter-spacing: .3px
}
.vc-h4,
.vc-h5 {
line-height: 20px
}
.vc-h5 {
color: var(--text-strong);
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
text-transform: unset
line-height: 1.25;
}
.vc-h4 {
font-size: 16px;
font-weight: 600;
line-height: 1.25;
}
.vc-h5 {
font-size: 16px;
font-weight: 500;
line-height: 1.25;
}
.vc-h1-defaultMargin,
@ -62,4 +51,4 @@
.vc-h3-defaultMargin,
.vc-h5-defaultMargin {
margin-bottom: 8px
}
}

View file

@ -29,7 +29,7 @@ export function Heading<T extends HeadingTag>(props: HeadingProps<T>) {
} = props;
return (
<Tag className={classes(`vc-${Tag}`, !className && `vc-${Tag}-defaultMargin`, className)} {...restProps}>
<Tag className={classes(`vc-${Tag}`, `vc-${Tag}-defaultMargin`, className)} {...restProps}>
{children}
</Tag>
);

View file

@ -713,3 +713,19 @@ export function CloudUploadIcon(props: IconProps) {
</Icon>
);
}
export const ClockIcon = (props?: any) => {
return (
<Icon
{...props}
viewBox="0 0 24 24"
>
<path
fillRule="evenodd"
clipRule="evenodd"
fill={props.fill || "currentColor"}
d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm1-18a1 1 0 1 0-2 0v7c0 .27.1.52.3.7l3 3a1 1 0 0 0 1.4-1.4L13 11.58V5Z"
/>
</Icon>
);
};

View file

@ -1,10 +1,9 @@
.vc-link {
color: var(--text-link);
text-decoration: none;
cursor: pointer;
text-decoration: var(--link-decoration);
}
.vc-link:hover {
.vc-link-underline-on-hover:hover {
text-decoration: underline;
}
@ -12,3 +11,22 @@
pointer-events: none;
opacity: 0.5;
}
/* Forced colors mode (high contrast accessibility) */
.enable-forced-colors .vc-link,
.enable-forced-colors .vc-link-underline-on-hover {
background-color: canvas;
text-decoration: underline;
color: linktext !important;
}
.enable-forced-colors .vc-link svg,
.enable-forced-colors .vc-link-underline-on-hover svg {
color: currentcolor;
}
.enable-forced-colors .vc-link:not([href]),
.enable-forced-colors .vc-link-underline-on-hover:not([href]) {
background-color: buttonface;
color: buttontext !important;
}

View file

@ -11,15 +11,34 @@ import type { AnchorHTMLAttributes, PropsWithChildren } from "react";
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
disabled?: boolean;
useDefaultUnderlineStyles?: boolean;
}
export function Link({ disabled, className, children, ...restProps }: PropsWithChildren<LinkProps>) {
export function Link({
disabled,
useDefaultUnderlineStyles = true,
href,
rel,
target,
className,
children,
...restProps
}: PropsWithChildren<LinkProps>) {
const isInternal = href && /^(?:discord:\/)?\/[a-zA-Z0-9_-]/.test(href);
const needsSafeAttrs = href && !isInternal;
return (
<a
role="link"
target="_blank"
rel="noreferrer noopener"
className={classes("vc-link", disabled && "vc-link-disabled", className)}
href={href}
target={target ?? (needsSafeAttrs ? "_blank" : undefined)}
rel={rel ?? (needsSafeAttrs ? "noreferrer noopener" : undefined)}
className={classes(
"vc-link",
useDefaultUnderlineStyles && "vc-link-underline-on-hover",
disabled && "vc-link-disabled",
className
)}
aria-disabled={disabled}
{...restProps}
>

114
src/components/Notice.css Normal file
View file

@ -0,0 +1,114 @@
.vc-notice-container {
border-radius: var(--radius-sm, 8px);
box-sizing: border-box;
display: grid;
font-weight: 500;
grid-template-rows: 1fr;
padding: 8px;
width: 100%;
transition-duration: var(--expand-structural-duration, 0.2s);
transition-property: grid-template-rows, padding-bottom, padding-top, border-width;
transition-timing-function: var(--expand-easing-function, ease);
}
.vc-notice-hidden {
border-width: 0;
grid-template-rows: 0fr;
padding-bottom: 0;
padding-top: 0;
transition-delay: var(--collapse-fade-duration, 0.1s);
transition-duration: var(--collapse-structural-duration, 0.2s);
transition-timing-function: var(--collapse-easing-function, ease);
}
.vc-notice-inner {
display: flex;
min-height: 0;
opacity: 1;
overflow: hidden;
transition: opacity var(--expand-fade-duration, 0.1s) var(--expand-easing-function, ease) var(--expand-structural-duration, 0.2s);
}
.vc-notice-hidden .vc-notice-inner {
opacity: 0;
transition-delay: 0s;
transition-duration: var(--collapse-fade-duration, 0.1s);
transition-timing-function: var(--collapse-easing-function, ease);
}
.vc-notice-icon-wrapper {
display: flex;
}
.vc-notice-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
}
.vc-notice-text {
align-self: center;
flex: 1 1 0%;
margin-left: var(--space-8, 8px);
font-family: var(--font-primary);
font-size: 14px;
font-weight: 500;
line-height: 1.2857;
color: var(--text-default);
}
.vc-notice-text p {
margin: 0;
}
.vc-notice-text p~p {
margin-top: 8px;
}
.vc-notice-action {
margin: auto 0 auto 16px;
}
/* Info variant */
.vc-notice-info {
background: var(--background-feedback-info);
border: 1px solid var(--icon-feedback-info);
color: var(--text-feedback-info);
}
.vc-notice-info .vc-notice-icon {
color: var(--icon-feedback-info);
}
/* Warning variant */
.vc-notice-warning {
background: var(--background-feedback-warning);
border: 1px solid var(--icon-feedback-warning);
color: var(--text-feedback-warning);
}
.vc-notice-warning .vc-notice-icon {
color: var(--icon-feedback-warning);
}
/* Danger variant */
.vc-notice-danger {
background: var(--background-feedback-critical);
border: 1px solid var(--icon-feedback-critical);
color: var(--text-feedback-critical);
}
.vc-notice-danger .vc-notice-icon {
color: var(--icon-feedback-critical);
}
/* Positive variant */
.vc-notice-positive {
background: var(--background-feedback-positive);
border: 1px solid var(--icon-feedback-positive);
color: var(--text-feedback-positive);
}
.vc-notice-positive .vc-notice-icon {
color: var(--icon-feedback-positive);
}

View file

@ -4,69 +4,88 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { filters, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, CSSProperties, ReactNode } from "react";
import "./Notice.css";
export type NoticeType = "warn" | "info" | "danger" | "positive" | "preview";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import type { ComponentPropsWithoutRef, ComponentType, ReactNode, SVGProps } from "react";
export const NoticeTypes: Record<"WARNING" | "INFO" | "ERROR" | "POSITIVE" | "PREVIEW", NoticeType> = {
WARNING: "warn",
INFO: "info",
ERROR: "danger",
POSITIVE: "positive",
PREVIEW: "preview"
};
const cl = classNameFactory("vc-notice-");
const { HelpMessage } = mapMangledModuleLazy('POSITIVE="positive"', {
HelpMessage: filters.byCode(".iconDiv")
});
export const NoticeVariants = ["info", "warning", "danger", "positive"] as const;
export interface NoticeProps {
export type NoticeVariant = typeof NoticeVariants[number];
export type NoticeProps = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
children: ReactNode;
className?: string;
style?: CSSProperties;
icon?: ComponentType<{ className?: string; color?: string; }>;
variant?: NoticeVariant;
icon?: ComponentType<SVGProps<SVGSVGElement>>;
action?: ReactNode;
textColor?: string;
textVariant?: string;
hidden?: boolean;
}
function Warning({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.WARNING} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Info({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.INFO} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Error({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.ERROR} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
function Positive({ children, className, style, icon, action, textColor, textVariant, hidden }: NoticeProps) {
return (
<HelpMessage messageType={NoticeTypes.POSITIVE} className={className} style={style} icon={icon} action={action} textColor={textColor} textVariant={textVariant} hidden={hidden}>
{children}
</HelpMessage>
);
}
export const Notice = {
Warning,
Info,
Error,
Positive
};
function InfoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" fillRule="evenodd" d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z" clipRule="evenodd" />
</svg>
);
}
function WarningIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" fillRule="evenodd" d="M10 3.1a2.37 2.37 0 0 1 4 0l8.71 14.75c.84 1.41-.26 3.15-2 3.15H3.29c-1.74 0-2.84-1.74-2-3.15L9.99 3.1Zm3.25 14.65a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM13.06 14l.37-5.94a1 1 0 0 0-1-1.06h-.87a1 1 0 0 0-1 1.06l.38 5.94a1.06 1.06 0 0 0 2.12 0Z" clipRule="evenodd" />
</svg>
);
}
function DangerIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" fillRule="evenodd" d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm4.7-15.7a1 1 0 0 0-1.4 0L12 10.58l-3.3-3.3a1 1 0 0 0-1.4 1.42L10.58 12l-3.3 3.3a1 1 0 1 0 1.42 1.4L12 13.42l3.3 3.3a1 1 0 0 0 1.4-1.42L13.42 12l3.3-3.3a1 1 0 0 0 0-1.4Z" clipRule="evenodd" />
</svg>
);
}
function PositiveIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" {...props}>
<path fill="currentColor" fillRule="evenodd" d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm5.7-13.3a1 1 0 0 0-1.4-1.4L10 14.58l-2.3-2.3a1 1 0 0 0-1.4 1.42l3 3a1 1 0 0 0 1.4 0l7-7Z" clipRule="evenodd" />
</svg>
);
}
const DefaultIcons: Record<NoticeVariant, ComponentType<SVGProps<SVGSVGElement>>> = {
info: InfoIcon,
warning: WarningIcon,
danger: DangerIcon,
positive: PositiveIcon
};
export function Notice({ children, variant = "info", icon, action, hidden = false, className, ...restProps }: NoticeProps) {
const Icon = icon ?? DefaultIcons[variant];
return (
<div className={classes(cl("container", variant), hidden && cl("hidden"), className)} {...restProps}>
<div className={cl("inner")}>
<div className={cl("icon-wrapper")}>
<Icon className={cl("icon")} />
</div>
<div className={cl("text")}>
{children}
</div>
{action && (
<div className={cl("action")}>
{action}
</div>
)}
</div>
</div>
);
}
Notice.Info = (props: Omit<NoticeProps, "variant">) => <Notice variant="info" {...props} />;
Notice.Warning = (props: Omit<NoticeProps, "variant">) => <Notice variant="warning" {...props} />;
Notice.Danger = (props: Omit<NoticeProps, "variant">) => <Notice variant="danger" {...props} />;
Notice.Positive = (props: Omit<NoticeProps, "variant">) => <Notice variant="positive" {...props} />;

View file

@ -1,42 +1,68 @@
.vc-switch-container {
background: var(--primary-400);
border: 1px solid transparent;
border-radius: 16px;
box-sizing: border-box;
cursor: pointer;
height: 28px;
display: flex;
align-items: center;
position: relative;
width: 44px;
.high-contrast-mode & {
border-color: var(--border-strong);
}
}
.vc-switch-checked {
background: var(--brand-500);
border-color: var(--control-primary-border-default);
.vc-switch-indicator {
background: var(--switch-background-default);
border: 1px solid var(--switch-border-default);
border-radius: var(--radius-lg);
box-sizing: border-box;
cursor: pointer;
height: 24px;
position: relative;
width: 48px;
transition: background-color 200ms ease, border-color 200ms ease;
}
.vc-switch-checked .vc-switch-indicator {
background: var(--switch-background-selected-default);
border-color: var(--switch-border-selected-default);
}
.vc-switch-disabled {
cursor: not-allowed;
opacity: 0.3;
opacity: 0.5;
}
.vc-switch-focusVisible {
/* stylelint-disable-next-line custom-property-pattern */
box-shadow: 0 0 0 4px var(--__adaptive-focus-ring-color, var(--border-focus, #00b0f4));
.vc-switch-disabled .vc-switch-indicator {
cursor: not-allowed;
}
.vc-switch-slider {
display: block;
height: 20px;
left: 0;
margin: 3px;
.vc-switch-focusVisible .vc-switch-indicator {
box-shadow: 0 0 0 2px var(--border-focus, #00a8fc);
}
.vc-switch-container:hover .vc-switch-indicator {
background: var(--switch-background-hover);
border-color: var(--switch-border-hover);
}
.vc-switch-checked:hover .vc-switch-indicator {
background: var(--switch-background-selected-hover);
border-color: var(--switch-border-selected-hover);
}
.vc-switch-thumb {
align-items: center;
display: flex;
height: 24px;
justify-content: center;
left: 1px;
margin-left: -1px;
margin-top: -1px;
position: absolute;
width: 28px;
transition: 100ms transform ease-in-out;
overflow: visible;
width: 24px;
transition: left 150ms ease-in-out;
}
.vc-switch-checked .vc-switch-thumb {
left: 24px;
}
.vc-switch-checked .vc-switch-thumb rect {
fill: var(--switch-thumb-background-selected-default);
}
.vc-switch-input {
@ -54,4 +80,4 @@
pointer-events: none;
cursor: not-allowed
}
}
}

View file

@ -19,22 +19,24 @@
import "./Switch.css";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { useState } from "@webpack/common";
import type { FocusEvent } from "react";
import type { FocusEvent, Ref } from "react";
const switchCls = classNameFactory("vc-switch-");
const SWITCH_ON = "var(--brand-500)";
const SWITCH_OFF = "var(--primary-400)";
export interface SwitchProps {
disabled?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
id?: string;
// checkmark/x icon in thumb
hasIcon?: boolean;
innerRef?: Ref<HTMLInputElement>;
"aria-describedby"?: string;
"aria-labelledby"?: string;
}
export function Switch({ checked, onChange, disabled }: SwitchProps) {
export function Switch({ checked, onChange, disabled, id, hasIcon = false, innerRef, ...ariaProps }: SwitchProps) {
const [focusVisible, setFocusVisible] = useState(false);
// Due to how we wrap the invisible input, there is no good way to do this with css.
@ -48,44 +50,58 @@ export function Switch({ checked, onChange, disabled }: SwitchProps) {
};
return (
<div>
<div className={classes(switchCls("container", { checked, disabled, focusVisible }))}>
<label className={switchCls("container", { checked, disabled, focusVisible })}>
<div
className={switchCls("indicator")}
data-mana-component="switch"
>
<svg
className={switchCls("slider")}
viewBox="0 0 28 20"
preserveAspectRatio="xMinYMid meet"
className={switchCls("thumb")}
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMin meet"
aria-hidden="true"
style={{
transform: checked ? "translateX(12px)" : "translateX(-3px)",
}}
>
<rect fill="white" x="4" y="0" height="20" width="20" rx="10" />
<svg viewBox="0 0 20 20" fill="none">
{checked ? (
<>
<path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" />
<path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" />
</>
) : (
<>
<path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" />
<path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" />
</>
)}
</svg>
<rect
fill="var(--switch-thumb-background-default)"
x={4}
y={4}
width={16}
height={16}
rx={8}
/>
{hasIcon && (
<svg viewBox="0 0 24 24" fill="none">
<g transform="translate(4 4)">
{checked ? (
<>
<path fill="var(--switch-thumb-icon-active)" d="M6.31649 11.88304L5.0437 10.61032L11.44792 4.20604L12.72072 5.47883L6.31649 11.88304Z" />
<path fill="var(--switch-thumb-icon-active)" d="M3.26914 8.87224L4.54194 7.59943L7.5588 10.61632L6.28601 11.88912L3.26914 8.87224Z" />
</>
) : (
<>
<path fill="var(--switch-thumb-icon-default)" d="M4.10585 5.3837L5.37864 4.11091L11.884 10.61632L10.6112 11.88912L4.10585 5.3837Z" />
<path fill="var(--switch-thumb-icon-default)" d="M10.61632 4.11091L11.88912 5.3837L5.3837 11.88912L4.11091 10.61632L10.61632 4.11091Z" />
</>
)}
</g>
</svg>
)}
</svg>
<input
onFocus={handleFocusChange}
onBlur={handleFocusChange}
disabled={disabled}
type="checkbox"
className={switchCls("input")}
tabIndex={0}
checked={checked}
onChange={e => onChange(e.currentTarget.checked)}
/>
</div>
</div>
<input
ref={innerRef}
id={id}
type="checkbox"
role="switch"
className={switchCls("input")}
tabIndex={0}
checked={checked}
disabled={disabled}
onChange={e => onChange(e.currentTarget.checked)}
onFocus={handleFocusChange}
onBlur={handleFocusChange}
{...ariaProps}
/>
</label>
);
}

View file

@ -397,8 +397,8 @@ function ThemesTab() {
async function changeThemeLibraryURLs() {
settings.themeLinks = settings.themeLinks.map(link => {
if (link.startsWith("https://themes-delta.vercel.app/api")) {
return link.replace("https://themes-delta.vercel.app/api", "https://discord-themes.com/api");
if (link.startsWith("https://discord-themes.com/api")) {
return link.replace("https://discord-themes.com/api", "https://themes.equicord.org/api");
}
return link;
});

View file

@ -21,12 +21,12 @@ import { classNameFactory } from "@utils/css";
const marginCls = classNameFactory("vc-margin-");
const Directions = ["top", "bottom", "left", "right"] as const;
const Sizes = [8, 16, 20] as const;
const Sizes = [4, 8, 16, 20, 24, 40, 60] as const;
export type MarginDirection = (typeof Directions)[number];
export type MarginSize = (typeof Sizes)[number];
export const Margins: Record<`${MarginDirection}${MarginSize}`, string> = {} as any;
export const Margins = {} as Record<`${MarginDirection}${MarginSize}` | "reset" | "centerHorz", string>;
export function generateMarginCss() {
let css = "";
@ -39,5 +39,11 @@ export function generateMarginCss() {
}
}
Margins.reset = marginCls("reset");
css += `.${Margins.reset}{margin:0;}`;
Margins.centerHorz = marginCls("center-horz");
css += `.${Margins.centerHorz}{margin-left:auto;margin-right:auto;}`;
return css;
}

View file

@ -18,8 +18,8 @@
import "./AddonCard.css";
import { Badge } from "@components/Badge";
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";
@ -72,7 +72,7 @@ export function AddonCard({ disabled, isNew, sourceBadge, tooltip, name, infoBut
{name}
</div>
</div>
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
{isNew && <Badge text="NEW" variant="danger" />}
</BaseText>
{!!author && (

View file

@ -56,7 +56,7 @@ export function InviteButton({
type="button"
onClick={async e => {
e.preventDefault();
openInviteModal("equicord-1173279886065029291").catch(() =>
openInviteModal("wKgT9j2xfN").catch(() =>
showToast("Invalid or expired invite"),
);
}}

View file

@ -18,9 +18,9 @@
import "./AddonCard.css";
import { Badge } from "@components/Badge";
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";
@ -101,7 +101,7 @@ export function OnlineThemeCard({
)}
</div>
</div>
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
{isNew && <Badge text="NEW" variant="danger" />}
</BaseText>
{!!author && (

View file

@ -1,29 +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 <https://www.gnu.org/licenses/>.
*/
export function AddonBadge({ text, color }) {
return (
<div className="vc-addon-badge" style={{
backgroundColor: color,
justifySelf: "flex-end",
marginLeft: "auto"
}}>
{text}
</div>
);
}

View file

@ -14,8 +14,8 @@
.vc-settings-quickActions-pill {
all: unset;
background: var(--redesign-button-secondary-background);
color: var(--text-default);
background: var(--control-secondary-background-default);
color: var(--control-secondary-text-default, var(--text-default));
display: flex;
align-items: center;
justify-content: center;
@ -29,7 +29,7 @@
}
.vc-settings-quickActions-pill:hover {
background: var(--redesign-button-secondary-pressed-background);
background: var(--control-secondary-background-hover);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
@ -43,4 +43,4 @@
width: 24px;
height: 24px;
flex-shrink: 0;
}
}

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "../Badge";
export * from "../Switch";
export * from "./AddonCard";
export * from "./DonateButton";
export * from "./PluginBadge";
export * from "./QuickAction";
export * from "./SpecialCard";
export * from "./tabs";

View file

@ -10,8 +10,7 @@ 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";
import { OptionType, Plugin } from "@utils/types";
import { React, showToast, Toasts } from "@webpack/common";
import { Settings } from "Vencord";
@ -99,7 +98,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
},
{
condition: isEquicordPlugin,
src: "https://equicord.org/assets/icons/equicord/icon.png",
src: "https://equicord.org/assets/favicon.png",
alt: "Equicord",
title: "Equicord Plugin"
},
@ -147,7 +146,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={cl("info-button")}
>
{plugin.settings?.def && !isObjectEmpty(plugin.settings.def)
{plugin.settings?.def && Object.values(plugin.settings.def).some(s => s.type !== OptionType.CUSTOM && !s.hidden)
? <CogWheel className={cl("info-icon")} />
: <InfoIcon className={cl("info-icon")} />
}

View file

@ -42,7 +42,7 @@
gap: var(--space-8, 8px);
}
.vc-plugin-modal-confirm [class*="footer"]>button {
.vc-plugin-modal-confirm [class*="footer"] > button {
flex: 1;
margin: 0;
width: 100%;
@ -54,3 +54,11 @@
gap: 4px;
color: var(--text-feedback-critical);
}
.vc-settings-modal-content {
padding-bottom: 24px;
}
.vc-plugin-modal-info {
align-items: center;
}

View file

@ -34,8 +34,8 @@ import { classes, isObjectEmpty } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins";
@ -46,7 +46,7 @@ import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-plugin-modal-");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const AvatarStyles = findCssClassesLazy("moreUsers", "avatar", "clickableAvatar");
const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
const ConfirmModal = findComponentByCodeLazy('parentComponent:"ConfirmModal"');
const WarningIcon = findComponentByCodeLazy("3.15H3.29c-1.74");
@ -78,11 +78,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
// avoid layout shift by showing dummy users while loading users
const fallbackAuthors = useMemo(() => [makeDummyUser({ username: "Loading...", id: "-1465912127305809920" })], []);
const [authors, setAuthors] = useState<Partial<User>[]>([]);
useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
for (const [index, user] of plugin.authors.slice(0, 6).entries()) {
try {
const author = user.id
? await UserUtils.getUser(String(user.id))
@ -178,13 +180,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div>
</ModalHeader>
<ModalContent className={Margins.bottom16}>
<ModalContent className={"vc-settings-modal-content"}>
<section>
<BaseText size="lg" weight="semibold" color="text-strong" className={Margins.bottom8}>Authors</BaseText>
<div style={{ width: "fit-content" }}>
<ErrorBoundary noop>
<UserSummaryItem
users={authors}
users={authors.length ? authors : fallbackAuthors}
guildId={undefined}
renderIcon={false}
showDefaultAvatarsForNullUsers

View file

@ -55,28 +55,35 @@ function Section(props: {
}) {
const { buttonMap, description, title, settings } = props;
const switches = Array.from(buttonMap, ([name, { icon }]) => {
const Icon = icon ?? PlaceholderIcon;
return (
<Paragraph size="md" weight="semibold" key={name} className={cl("switches-row")}>
<Icon height={20} width={20} />
{name}
<Switch
checked={settings[name]?.enabled ?? true}
onChange={v => {
settings[name] ??= {} as any;
settings[name].enabled = v;
}}
/>
</Paragraph>
);
});
return (
<section>
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText>
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
<div className={cl("switches")}>
{Array.from(buttonMap, ([name, { icon }]) => {
const Icon = icon ?? PlaceholderIcon;
return (
<Paragraph size="md" weight="semibold" key={name} className={cl("switches-row")}>
<Icon height={20} width={20} />
{name}
<Switch
checked={settings[name]?.enabled ?? true}
onChange={v => {
settings[name] ??= {} as any;
settings[name].enabled = v;
}}
/>
</Paragraph>
);
})}
{switches.length === 0 && (
<Paragraph weight="medium" className={cl("switches-row")} style={{ color: "var(--text-muted)" }}>
Buttons of enabled plugins will appear here.
</Paragraph>
)}
{switches}
</div>
</section>
);

View file

@ -41,7 +41,11 @@ export function BooleanSetting({ option, pluginSettings, definedSettings, id, on
return (
<SettingsSection tag="label" name={id} description={option.description} error={error} inlineSetting>
<Switch checked={state} onChange={handleChange} />
<Switch
checked={state}
onChange={handleChange}
disabled={option.disabled?.call(definedSettings) ?? false}
/>
</SettingsSection>
);
}

View file

@ -17,7 +17,7 @@
*/
import { PluginOptionString } from "@utils/types";
import { React, TextInput, useState } from "@webpack/common";
import { React, TextArea, TextInput, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
@ -38,15 +38,23 @@ export function TextSetting({ option, pluginSettings, definedSettings, id, onCha
return (
<SettingsSection name={id} description={option.description} error={error}>
<TextInput
type="text"
placeholder={option.placeholder ?? "Enter a value"}
value={state}
onChange={handleChange}
maxLength={null}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
{option.multiline
? <TextArea
placeholder={option.placeholder ?? "Enter a value"}
value={state}
onChange={handleChange}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps} />
: <TextInput
type="text"
placeholder={option.placeholder ?? "Enter a value"}
value={state}
onChange={handleChange}
maxLength={null}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
}
</SettingsSection>
);
}

View file

@ -48,19 +48,6 @@
grid-template-columns: 1fr 150px;
}
.vc-addon-badge {
padding: 0 6px;
font-family: var(--font-display);
font-weight: 500;
border-radius: 8px;
height: 16px;
font-size: 12px;
line-height: 16px;
color: var(--white-500);
text-align: center;
margin-right: 8px;
}
.vc-plugins-dep-name {
margin: 0 auto;
}
@ -175,29 +162,29 @@
.vc-plugins-disable-warning,
.vc-plugin-modal-disable-warning {
background-color: color-mix(in srgb, var(--redesign-button-danger-background) 10%, transparent) !important;
background-color: color-mix(in srgb, var(--status-danger) 10%, transparent) !important;
border-style: solid !important;
border-width: .1rem !important;
border-color: color-mix(in srgb, var(--redesign-button-danger-background) 80%, transparent) !important;
border-color: color-mix(in srgb, var(--status-danger) 80%, transparent) !important;
border-radius: 5px !important;
color: color-mix(in srgb, var(--redesign-button-danger-background) 100%, transparent) !important;
color: color-mix(in srgb, var(--status-danger) 100%, transparent) !important;
}
.vc-plugins-disable-warning:hover,
.vc-plugin-modal-disable-warning:hover {
background-color: color-mix(in srgb, var(--redesign-button-danger-background) 40%, transparent) !important;
background-color: color-mix(in srgb, var(--status-danger) 40%, transparent) !important;
}
.vc-plugins-confirm-reset,
.vc-plugin-modal-confirm-reset {
background-color: var(--redesign-button-danger-background) !important;
color: var(--redesign-button-danger-pressed-text);
background-color: var(--status-danger) !important;
color: var(--text-default);
}
.vc-plugins-confirm-reset:hover,
.vc-plugin-modal-confirm-reset:hover {
background-color: var(--redesign-button-danger-pressed-background) !important;
color: var(--redesign-button-danger-pressed-text);
background-color: var(--text-feedback-critical) !important;
color: var(--text-default);
}
.vc-modal-close-button {
@ -308,4 +295,4 @@
.vc-plugins-ui-elements {
margin-top: 16px;
}
}

View file

@ -76,8 +76,8 @@
}
.vc-cloud-erase-data-danger-btn {
color: var(--white-500);
background-color: var(--redesign-button-danger-background);
color: var(--control-critical-primary-text-default);
background-color: var(--control-critical-primary-background-default);
}
.vc-cloud-icon-with-button {

View file

@ -39,7 +39,7 @@ import { Alerts, SearchableSelect, Select, useState } from "@webpack/common";
const ICON_STYLE: React.CSSProperties = { width: 20, height: 20, borderRadius: 4, verticalAlign: "middle" };
function EquicordIcon() {
return <img src="https://equicord.org/assets/icons/equicord/icon.png" alt="Equicord" style={ICON_STYLE} />;
return <img src="https://equicord.org/assets/favicon.png" alt="Equicord" style={ICON_STYLE} />;
}
function VencordIcon() {

View file

@ -18,11 +18,11 @@
.vc-joindiscordbutton {
background-color: var(--redesign-button-secondary-background) !important;
background-color: var(--channeltextarea-background) !important;
}
.vc-joindiscordbutton:hover {
background-color: var(--redesign-button-secondary-pressed-background) !important;
background-color: var(--redesign-button-tertiary-pressed-background) !important;
}
.vc-settings-card-buttons {

View file

@ -29,6 +29,18 @@ function getWebpackChunkMap() {
return v;
}
async function promiseAllSettledBatched<T>(tasks: (() => Promise<T>)[], batchSize = 8): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < tasks.length; i += batchSize) {
const batch = tasks.slice(i, i + batchSize);
const settled = await Promise.allSettled(batch.map(t => t()));
for (const r of settled) {
if (r.status === "fulfilled") results.push(r.value);
}
}
return results;
}
export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
@ -45,8 +57,8 @@ export async function loadLazyChunks() {
const chunksSearchPromises = [] as Array<() => boolean>;
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those
const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false;
@ -59,7 +71,7 @@ export async function loadLazyChunks() {
const shouldForceDefer = false;
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
await promiseAllSettledBatched(Array.from(lazyChunks).map(([, rawChunkIds, entryPoint]) => async () => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
const numChunkId = Number(m[1]);
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
@ -104,9 +116,9 @@ export async function loadLazyChunks() {
}));
// Loads all found valid chunk groups
await Promise.all(
await promiseAllSettledBatched(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
.map(([chunkIds]) => () =>
Promise.all(chunkIds.map(id => wreq.e(id)))
)
);
@ -182,7 +194,7 @@ export async function loadLazyChunks() {
return !(validChunks.has(id) || invalidChunks.has(id));
});
await Promise.all(chunksLeft.map(async id => {
await promiseAllSettledBatched(chunksLeft.map(id => async () => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => /importScripts\(|self\.postMessage/.test(t));

View file

@ -38,6 +38,7 @@ export const reporterData: ReporterData = {
findByProps: [],
findByCode: [],
findStore: [],
findCssClasses: [],
findComponent: [],
findComponentByCode: [],
findExportedComponent: [],

View file

@ -24,7 +24,7 @@ export default definePlugin({
// Uses the audio as-is if external, otherwise checks for an internal Discord sound.
// Also force loads the internal sounds module to account for the second patch group below,
// as well as accounting for not calling the module in this patch when this.type is not DISCORD.
match: /(let \i=class.{0,900}?new Audio;\i.src=)((\i\(\d+\)).{0,50}concat\()this.name(,".mp3"\)\))/,
match: /(let \i=class.{0,900}?new Audio;\i.src=)((\i\(\d+\))(?:\(`\.\/\$\{|.{0,50}concat\())this.name((?:\}\.mp3`|,".mp3"\))\))/,
replace: "$3;$1this.type!==$self.AudioType.DISCORD?this.audio:$2this.audio$4"
},
{

View file

@ -4,26 +4,26 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "HeaderBarAPI",
description: "API to add buttons to the header bar and channel toolbar.",
authors: [EquicordDevs.Prism],
authors: [Devs.prism],
patches: [
{
find: '?"BACK_FORWARD_NAVIGATION":',
replacement: {
match: /(?<=\i\.Fragment,\{children:\[.{1,500}className:\i\}\))(?=\])/,
match: /(?<="HELP".{0,150}className:\i\}\))(?=\])/,
replace: ",...Vencord.Api.HeaderBar._addHeaderBarButtons()"
}
},
{
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: {
match: /(?<=renderHeaderToolbar",\(\)=>\{.{1,200}let (\i)=\[\];)/,
match: /(?<=renderHeaderToolbar"\);let (\i)=\[\];)/,
replace: "Vencord.Api.HeaderBar._addChannelToolbarButtons($1);"
}
}

View file

@ -6,28 +6,38 @@
import { isPluginEnabled } from "@api/PluginManager";
import betterUserArea from "@equicordplugins/betterUserArea";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findCssClassesLazy } from "@webpack";
const { iconForeground } = findCssClassesLazy("iconForeground", "accountPopoutButtonWrapper");
export default definePlugin({
name: "UserAreaAPI",
description: "API to add buttons to the user area panel.",
authors: [EquicordDevs.Prism],
authors: [Devs.prism],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /(className:(\i)\.buttons,.{0,50}?children:\[)/,
replace: "$1...$self.renderButtons(arguments[0],$2),"
}
replacement: [
{
match: /(?<=className:(\i)\.\i,style:\i,)children:\[/,
replace: "children:[...$self.renderButtons(arguments[0],$1),"
},
// fix discord weird shrink with extra buttons
{
match: /(?<=\{ref:\i,)style:(\i)(?=,onMouseEnter:\i,onMouseDown:\i,onClick)/,
replace: "style:{...$1,minWidth:0}"
}
]
}
],
renderButtons(props: { nameplate?: any; }, styles: { iconForeground?: string; }) {
renderButtons(props: { nameplate?: any; }) {
return Vencord.Api.UserArea._renderButtons({
nameplate: props.nameplate,
iconForeground: props.nameplate != null ? styles.iconForeground : void 0,
iconForeground: props.nameplate != null ? iconForeground : void 0,
hideTooltips: this.shouldHideTooltips()
});
},

View file

@ -15,6 +15,12 @@ export const settings = definePluginSettings({
description: "Remove shops above DMs list",
restartNeeded: true,
},
quests: {
type: OptionType.BOOLEAN,
default: false,
description: "Remove quests above DMs list",
restartNeeded: true,
},
serverBoost: {
type: OptionType.BOOLEAN,
default: true,
@ -43,7 +49,7 @@ export const settings = definePluginSettings({
export default definePlugin({
name: "Anammox",
description: "A microbial process that plays an important part in the nitrogen cycle",
description: "Hides various features related to nitro and the store",
authors: [Devs.Kyuuhachi],
settings,
@ -53,15 +59,21 @@ export default definePlugin({
find: 'tutorialId:"direct-messages"',
replacement: [
{
match: /NAVIGATION_LINK\}\}\}\)(?=,\i(&&\(|\]))/,
match: /"nitro-tab-group"\)/,
replace: "$&&&undefined",
predicate: () => settings.store.dms
},
{
match: /NAVIGATION_LINK\}\}\}.{0,20}\)(?=,\i&&!\i)/,
match: /"discord-shop"\)/,
replace: "$&&&undefined",
predicate: () => settings.store.dms
},
{
match: /"quests"\)/,
replace: "$&&&undefined",
predicate: () => settings.store.quests
},
],
predicate: () => settings.store.dms,
},
{
// Above DMs, keyboard nav
@ -82,8 +94,8 @@ export default definePlugin({
// Channel list server boost progress bar
find: "useGuildActionRow",
replacement: {
match: /\i\.premiumProgressBarEnabled&&[^,]+/,
replace: "null"
match: /(GUILD_NEW_MEMBER_ACTIONS_PROGRESS_BAR\)):(\i(?:\.premiumProgressBarEnabled)?)/,
replace: "$1:null"
},
predicate: () => settings.store.serverBoost,
},
@ -99,12 +111,15 @@ export default definePlugin({
{
// Gift button
find: '"sticker")',
replacement: { match: /&&\i\.push\(\{[^&]*?,"gift"\)\}\)/, replace: "", },
replacement: {
match: /&&\i\.push\(\([^&]*?,"gift"\)\)/,
replace: "",
},
predicate: () => settings.store.gift,
},
{
// Emoji list
find: "#{intl::EMOJI_PICKER_CREATE_EMOJI_TITLE}),size:",
find: "#{intl::EMOJI_PICKER_EXPAND_EMOJI_SECTION}),size:",
replacement: {
match: /(\i)=\i\|\|!\i&&\i.\i.isEmojiCategoryNitroLocked\(\{[^}]*\}\);/,
replace: "$&$1||"

View file

@ -12,7 +12,7 @@ import usrbg from "@plugins/usrbg";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findStoreLazy } from "@webpack";
import { UserProfileStore } from "@webpack/common";
import style from "./style.css?managed";
@ -41,8 +41,6 @@ const settings = definePluginSettings({
const DATASTORE_KEY = "bannersEverywhere";
const UserProfileStore = findStoreLazy("UserProfileStore");
export default definePlugin({
name: "BannersEverywhere",
description: "Displays banners in the member list ",
@ -67,8 +65,8 @@ export default definePlugin({
find: "role:\"listitem\",innerRef",
replacement: {
// We cant access the user id here, so we take the banner property we set earlier
match: /focusProps.\i\}=(\i).*?children:\[/,
replace: "$&$1.banner,"
match: /children:\[(?=.{0,100}\.MEMBER_LIST)/,
replace: "$&arguments[0].banner,"
}
}
],

View file

@ -21,34 +21,29 @@ export default definePlugin({
description: "Shows activity icons in the member list and allows showing all activities",
authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN, Devs.thororen],
tags: ["activity"],
settings,
patchActivityList,
showAllActivitiesComponent,
patches: [
{
// Patch activity icons
find: "isBlockedOrIgnored(null",
find: '"ActivityStatus"),',
replacement: [
{
match: /(?<=className:\i,children:\[).*?(?=\i\(\),\i&&)/,
match: /(?<=className:\i,children:\[).*?(?=\i\(\),\i&&)/g,
replace: "",
predicate: () => settings.store.removeGameActivityStatus,
},
{
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/g,
replace: ",$self.patchActivityList($1)]",
predicate: () => settings.store.memberList,
}
],
all: true
},
{
// Show all activities in the user popout/sidebar
find: '"UserProfilePopoutBody"',
find: /\.POPOUT,onClose:\i}\),nicknameIcons:.+?\.isProvisional/,
replacement: {
match: /((\i)=.{0,10}(\i)\.id\).*?,onInteraction:\i\}\),).{0,250}onClose:\i\}\)/,
replace: "$1$self.showAllActivitiesComponent({ activity: $2, user: $3 })"

View file

@ -50,7 +50,7 @@ export function showAllActivitiesComponent({ activity, user, ...props }: Readonl
return (
<ErrorBoundary noop>
<div style={{ display: "flex", flexDirection: "column" }}>
{activity && currentActivity?.id === activity.id ? (
{activity && currentActivity?.application_id === activity.application_id ? (
<ActivityView
activity={currentActivity}
user={user}

View file

@ -66,7 +66,7 @@ async function addListeners(audioElement: HTMLAudioElement, url: string, parentB
const madeURL = new URL(url);
madeURL.searchParams.set("t", Date.now().toString());
const corsProxyUrl = "https://corsproxy.io/?url=" + encodeURIComponent(madeURL.href);
const corsProxyUrl = "https://cors.keiran0.workers.dev?url=" + encodeURIComponent(madeURL.href);
const response = await fetch(corsProxyUrl);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);

View file

@ -11,7 +11,7 @@
}
.vc-bbr-remove-button:hover {
color: var(--redesign-button-danger-pressed-background);
color: var(--text-feedback-critical);
}
.vc-bbr-add-button {

View file

@ -31,12 +31,12 @@ export default definePlugin({
replace: "$self.renderUser($1,$2)",
},
{
match: /(?<=\}=(\i).{0,10}(\i).useState\(\i\).{0,25}\};)/,
match: /(?<=\}=(\i).{0,10}(\i).useState.{0,5};)/,
replace: "let [searchResults,setSearchResults]=$2.useState([]);$self.setUpdateFunc($1,setSearchResults);"
},
{
match: /(usersList,children:)(\i)/,
replace: "$1(searchResults.length?searchResults:$2)"
match: /(?<=\i,children:)(\i)(?=\.slice)/,
replace: "(searchResults.length?searchResults:$1)"
},
]
}

View file

@ -17,7 +17,7 @@ const settings = definePluginSettings({
default: true,
},
allowNewlinesInCommands: {
description: "Allow newlines in command inputs (Shift + Enter)",
description: "Allow newlines in command inputs (CTRL + Shift + Enter)",
type: OptionType.BOOLEAN,
default: true,
}
@ -54,7 +54,7 @@ export default definePlugin({
match: /case (\i\.\i)\.TAB:if\(null!=(\i).selection&&\i\((\i)(?=.{0,300}(\i\.\i\.insertText))/,
replace: (orig, keys, editor, event, insertText) => {
return `case ${keys}.ENTER:
if(${event}.shiftKey){
if(${event}.shiftKey && ${event}.ctrlKey){
${event}.preventDefault();
${event}.stopPropagation();
${insertText}(${editor},'\\n');

View file

@ -12,10 +12,10 @@ import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import definePlugin, { StartAt } from "@utils/types";
import { Guild } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { findByPropsLazy, findCssClassesLazy } from "@webpack";
import { Parser, Tooltip, UserStore } from "@webpack/common";
const AvatarStyles = findByPropsLazy("avatar", "zalgo");
const AvatarStyles = findCssClassesLazy("avatar", "zalgo", "clickable");
const GuildManager = findByPropsLazy("joinGuild");
interface User {
@ -33,24 +33,24 @@ function lurk(id: string) {
export default definePlugin({
name: "BetterInvites",
description: "See invites expiration date, view inviter profile and preview discoverable servers before joining by clicking their name",
description: "See invites expiration date, view inviter profile and preview servers before joining by clicking the name",
authors: [EquicordDevs.iamme, Devs.thororen],
patches: [
{
find: ".hideDetailsButtonContainer,",
find: "#{intl::xdCLeM::raw}",
replacement: [
{
match: /banner\}\),.{0,25}profile:\i\}\),.{0,15}profile:\i/,
match: /profile:\i\}\),.{0,15}profile:\i/,
replace: "$&,invite:arguments[0].invite"
}
]
},
{
find: ".guildNameContainer,onClick:",
find: "onlineCount})})]",
replacement: [
{
// make the button clickable
match: /children:(\i)\.name\}\).{0,100}\.guildNameContainer/,
match: /children:(\i)\.name\}\)\}\)\}\)/,
replace: "onClick:$self.Lurkable($1),$&"
},
{

View file

@ -1,111 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { makeRange, OptionType } from "@utils/types";
export const settings = definePluginSettings({
frequentEmojis: {
description: "Use frequently used emojis instead of favourite emojis",
type: OptionType.BOOLEAN,
restartNeeded: true,
default: true
},
rows: {
description: "Rows of quick reactions to display",
type: OptionType.SLIDER,
default: 2,
markers: makeRange(1, 16, 1),
stickToMarkers: true
},
columns: {
description: "Columns of quick reactions to display",
type: OptionType.SLIDER,
default: 4,
markers: makeRange(1, 12, 1),
stickToMarkers: true
},
compactMode: {
description: "Scales the buttons to 75% of their original scale, whilst increasing the inner emoji to 125% scale. Emojis will be 93.75% of the original size. Recommended to have a minimum of 5 columns",
type: OptionType.BOOLEAN,
default: false
},
scroll: {
description: "Enable scrolling the list of emojis",
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "BetterQuickReact",
description: "Improves the quick react buttons in the message context menu.",
authors: [Devs.Ven, Devs.Sqaaakoi],
settings,
patches: [
// Remove favourite emojis from being inserted at the start of the reaction list
{
find: "this.favoriteEmojisWithoutFetchingLatest.concat",
replacement: {
match: /(this\.favoriteEmojisWithoutFetchingLatest)\.concat/,
replace: "[].concat"
},
predicate: () => settings.store.frequentEmojis
},
{
find: "#{intl::ADD_REACTION_NAMED}",
group: true,
replacement: [
{
match: /isEmojiPremiumLocked\(\{.{0,25}channel:(\i),/,
replace: "$&guild_id:$1?.guild_id??null,"
},
// Override limit of emojis to display with offset hook.
{
match: /(\i)\.length>4&&\((\i)\.length=4\);/,
replace: "let [betterQuickReactScrollValue,setBetterQuickReactScrollValue]=Vencord.Webpack.Common.React.useState(0);betterQuickReactScrollValue;"
},
// Add a custom class to identify the quick reactions have been modified and a CSS variable for the number of columns to display
{
match: /className:(\i)\.wrapper,/,
replace: "className:\"vc-better-quick-react \"+($self.settings.store.compactMode?\"vc-better-quick-react-compact \":\"\")+$1.wrapper,style:{\"--vc-better-quick-react-columns\":$self.settings.store.columns},"
},
// Scroll handler + Apply the emoji count limit from earlier with custom logic
{
match: /children:(\i)\.map\(/,
replace: "onWheel:$self.onWheelWrapper(betterQuickReactScrollValue,setBetterQuickReactScrollValue,$1.length),children:$self.applyScroll($1,betterQuickReactScrollValue).map("
}
]
},
// MenuGroup doesn't accept styles or anything special by default :/
{
find: ".groupLabel,",
replacement: {
match: /role:"group",/,
replace: "$&style:arguments[0].style,onWheel:arguments[0].onWheel,"
}
}
],
getMaxQuickReactions() {
return settings.store.rows * settings.store.columns;
},
applyScroll(emojis: any[], index: number) {
return emojis.slice(index, index + this.getMaxQuickReactions());
},
onWheelWrapper(currentScrollValue: number, setScrollHook: (value: number) => void, emojisLength: number) {
if (settings.store.scroll) return (e: WheelEvent) => {
if (e.deltaY === 0 || e.shiftKey) return;
e.stopPropagation(); // does this do anything?
const modifier = e.deltaY < 0 ? -1 : 1;
const newValue = currentScrollValue + (modifier * settings.store.columns);
setScrollHook(Math.max(0, Math.min(newValue, emojisLength - this.getMaxQuickReactions())));
};
},
});

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
@ -50,7 +50,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "BetterUserArea",
description: "Customize and make the user area more clean.",
authors: [EquicordDevs.Prism],
authors: [Devs.prism],
settings,
patches: [
{
@ -63,10 +63,24 @@ export default definePlugin({
},
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /\{showRefreshedAudioContextMenu:(\i)\}=(?=.+?location:"\w+Button")/g,
replace: "{showRefreshedAudioContextMenu:$1}=$self.settings.store.removeAudioMenus?{showRefreshedAudioContextMenu:!1}:"
},
replacement: [
{
match: /(?<=#{intl::MUTE}\),)className:\i\.\i,/,
replace: ""
},
{
match: /(?<=#{intl::DEAFEN}\),)className:\i\.\i,/,
replace: ""
},
{
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,600}#{intl::ACCOUNT_INPUT_OPTIONS}\)\}\)(?=\])/,
replace: ""
},
{
match: /,\(0,\i\.jsxs?\)\(\i\.\i,\{.{0,650}#{intl::ACCOUNT_OUTPUT_OPTIONS}\)\}\)(?=\])/,
replace: ""
}
],
predicate: () => settings.store.removeAudioMenus
},
{
@ -79,10 +93,20 @@ export default definePlugin({
},
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /tooltipText:\i,/g,
replace: "tooltipText:void 0,"
},
replacement: [
{
match: /:\{tooltipText:\i\};/,
replace: ":{tooltipText:void 0};"
},
{
match: /(?<=role:"switch",)tooltipText:\i\}/,
replace: "tooltipText:void 0}"
},
{
match: /tooltipText:\i,tooltipPositionKey/,
replace: "tooltipText:void 0,tooltipPositionKey"
}
],
predicate: () => settings.store.removeButtonTooltips
},
{

View file

@ -73,10 +73,10 @@ export default definePlugin({
authors: [EquicordDevs.catcraft],
patches: [
{
find: '"_channelMessages",{})',
find: "_channelMessages={}",
predicate: () => settings.store.blockedWords !== "",
replacement: {
match: /static commit\((.{1,2})\){/g,
match: /static commit\((\i)\)\{/g,
replace: "$&$1=$self.blockMessagesWithKeywords($1);"
}
},

View file

@ -12,38 +12,18 @@ export default definePlugin({
description: "Bypass the pin prompt when using the pin functions",
authors: [Devs.thororen],
patches: [
{
find: '"Channel Pins"',
replacement: {
match: /(\i\.\i\.unpinMessage\(\i,\i\.id\)):\i\.\i\.confirmUnpin\(\i,\i\)/,
replace: "$1:$1"
}
},
{
find: 'source:"message-actions"',
...[
'source:"message-actions"',
'id:"pin",action',
'"Channel Pins"',
].map(find => ({
find,
replacement: [
{
match: /(\i\.\i\.pinMessage\(\i,\i\.id\)):\i\.\i\.confirmPin\(\i,\i\)/,
replace: "$1:$1"
},
{
match: /(\i\.\i\.unpinMessage\(\i,\i\.id\)):\i\.\i\.confirmUnpin\(\i,\i\)/,
match: /(\i\.\i\.(unpin|pin)Message\(\i,\i\.id\)):\i\.\i\.confirm(Unpin|Pin)\(\i,\i\)/g,
replace: "$1:$1"
}
]
},
{
find: 'id:"pin",action',
replacement: [
{
match: /(\i\.\i\.pinMessage\(\i,\i\.id\)):\i\.\i\.confirmPin\(\i,\i\)/,
replace: "$1:$1"
},
{
match: /(\i\.\i\.unpinMessage\(\i,\i\.id\)):\i\.\i\.confirmUnpin\(\i,\i\)/,
replace: "$1:$1"
}
]
},
}))
],
});

View file

@ -22,6 +22,8 @@ interface IMessageCreate {
message: Message;
}
const SILENT_PING_FLAG = 1 << 12;
function Icon(enabled?: boolean): JSX.Element {
return <svg
width="18"
@ -122,6 +124,11 @@ const settings = definePluginSettings({
description: "Whether the notification sound should be played",
default: true,
},
respectSilentPings: {
type: OptionType.BOOLEAN,
description: "Respect silent pings (@silent / suppress notifications)",
default: true
},
statusToUse: {
type: OptionType.SELECT,
description: "Status to use for whitelist",
@ -161,6 +168,7 @@ export default definePlugin({
if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === currentChannelId && WindowStore.isFocused()) || userStatus !== settings.store.statusToUse) {
return;
}
if (settings.store.respectSilentPings && (message.flags & SILENT_PING_FLAG)) { return; }
const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned;
if ((settings.store.guilds.split(", ").includes(guildId) || settings.store.channels.split(", ").includes(channelId)) && mentioned) {
await showNotification(message, guildId);

View file

@ -46,7 +46,7 @@ export default definePlugin({
},
// Threads
{
find: "spineWithGuildIcon]:",
find: "18V16H9v2H6Zm3",
replacement: {
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
replace: "$&,$self.renderChannelBadges($1)"

View file

@ -12,16 +12,16 @@ import { classNameFactory } from "@utils/css";
import { getGuildAcronym, getIntlMessage, getUniqueUsername } from "@utils/discord";
import { classes } from "@utils/misc";
import { Channel, Guild, User } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { Avatar, ChannelStore, ContextMenuApi, GuildStore, PresenceStore, ReadStateStore, TypingStore, useDrag, useDrop, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
import { JSX } from "react";
import { TabContextMenu } from "./ContextMenus";
const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
const dotStyles = findByPropsLazy("numberBadge", "textBadge");
const ThreeDots = findComponentByCodeLazy("Math.min(1,Math.max(", "dotRadius:");
const dotStyles = findCssClassesLazy("numberBadge", "baseShapeRound");
const ChannelTypeIcon = findComponentByCodeLazy(".iconContainerWithGuildIcon,");
const ChannelTypeIcon = findComponentByCodeLazy('"ChannelItemIcon")');
// Custom SVG icons for pages that don't have findable components

View file

@ -8,7 +8,6 @@ import { Flex } from "@components/Flex";
import { Heading } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, closeTab, createTab, handleChannelSwitch, isNavigationFromSource, isTabSelected, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "@equicordplugins/channelTabs/util";
import { IS_MAC } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { useForceUpdater } from "@utils/react";
@ -294,7 +293,6 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
className={classes(
cl("container"),
tabBarPosition === "top" && cl("container-top"),
IS_MAC && tabBarPosition === "top" && cl("container-top-macos"),
!animationHover && cl("no-hover-animation"),
!animationSelection && cl("no-selection-animation"),
!animationDragDrop && cl("no-drag-animation"),
@ -329,6 +327,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
{openedTabs.filter(tab => tab != null).map((tab, i) =>
<ChannelTab {...tab} index={i} key={tab.id} />
)}
{GhostTabs}
</HorizontalScroller>
<button
@ -337,8 +336,6 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
>
<PlusSmallIcon />
</button>
{GhostTabs}
</div >
</div>

Some files were not shown because too many files have changed in this diff Show more