diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 11f397a3d..727d0de0e 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -28,12 +28,8 @@ jobs: steps: - uses: actions/checkout@v4 - if: ${{ github.event_name == 'schedule' }} with: - ref: dev - - - uses: actions/checkout@v4 - if: ${{ github.event_name == 'workflow_dispatch' }} + ref: ${{ github.event_name == 'schedule' && 'dev' || github.ref }} - uses: oven-sh/setup-bun@v2 @@ -44,19 +40,13 @@ jobs: run: | bun install - - name: Install Google Chrome - id: setup-chrome - uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2 - with: - chrome-version: stable - - name: Build Equicord Reporter Version run: bun run buildReporter - name: Run Reporter timeout-minutes: 10 run: | - export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }} + export CHROMIUM_BIN=/usr/bin/google-chrome bun build scripts/generateReport.ts --outfile dist/report.mjs --target bun diff --git a/package.json b/package.json index 80b859ddd..a4ce6d373 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.14.3.1", + "version": "1.14.4.0", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { @@ -47,7 +47,7 @@ "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", "@intrnl/xxhash64": "^0.1.2", - "@song-spotlight/api": "^2.0.0", + "@song-spotlight/api": "^2.0.1", "@streamparser/json": "^0.0.22", "@types/less": "^3.0.8", "@types/stylus": "^0.48.42", diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 208c37698..f0db98933 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -49,6 +49,7 @@ export interface Settings { themeNames: Record; enableReactDevtools: boolean; themeLinks: string[]; + mainWindowFrameless: boolean; frameless: boolean; transparent: boolean; winCtrlQ: boolean; @@ -116,6 +117,7 @@ const DefaultSettings: Settings = { pinnedThemes: [], themeNames: {}, enableReactDevtools: false, + mainWindowFrameless: false, frameless: false, transparent: false, winCtrlQ: false, diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 332cd3ecd..835ab7373 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -749,3 +749,23 @@ export const QrCodeIcon = (props?: any) => { ); }; + +export const ComponentsIcon = (props?: any) => { + return ( + + + + + ); +}; diff --git a/src/components/settings/DonateButton.tsx b/src/components/settings/DonateButton.tsx index 87e69669b..0465d3a26 100644 --- a/src/components/settings/DonateButton.tsx +++ b/src/components/settings/DonateButton.tsx @@ -67,3 +67,22 @@ export function InviteButton({ ); } + +export function TranslateButton({ + className, + ...props +}: Partial) { + const link = "https://weblate.equicord.org/projects/equicord/"; + return ( + + ); +} diff --git a/src/components/settings/SpecialCard.css b/src/components/settings/SpecialCard.css index f037aadbd..af663475d 100644 --- a/src/components/settings/SpecialCard.css +++ b/src/components/settings/SpecialCard.css @@ -26,6 +26,18 @@ background-color: color-mix(in srgb, #c3a3ce 50%, rgb(240 178 240)); } +.vc-translate-button { + overflow: visible !important; + color: black; + background-color: color-mix(in srgb, #575757 50%, rgb(255 255 255)); +} + +.vc-translate-button:hover { + overflow: visible !important; + color: black; + background-color: color-mix(in srgb, #575757 50%, rgb(201 201 201)); +} + .vc-invite-link { width: 16px; height: 16px; @@ -142,4 +154,14 @@ .vc-special-image { width: 65%; +} + +.vc-translate-modal-icon { + margin: auto; + width: 60px; +} + +.vc-translate-modal-paragraph { + padding: 12px; + text-align: center; } \ No newline at end of file diff --git a/src/components/settings/tabs/plugins/ContributorModal.css b/src/components/settings/tabs/plugins/ContributorModal.css index f35d49f57..9cfcb4b5a 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.css +++ b/src/components/settings/tabs/plugins/ContributorModal.css @@ -5,17 +5,19 @@ .vc-author-modal-header { display: flex; align-items: center; + gap: 8px; margin-bottom: 1em; } .vc-author-modal-name { text-transform: none; + margin: 0; flex-grow: 0; background: var(--background-base-lowest); border-radius: 0 9999px 9999px 0; padding: 6px 0.8em 6px 0.5em; font-size: 20px; - height: 20px; + line-height: 1; position: relative; text-wrap: nowrap; } @@ -35,9 +37,17 @@ } .vc-author-modal-avatar { + flex-shrink: 0; + transform: translateX(4px); + position: relative; + z-index: 2; height: 32px; width: 32px; border-radius: 50%; + border: 2px solid var(--background-primary); + box-shadow: + 0 0 0 1px rgb(255 255 255 / 18%), + 0 2px 6px rgb(0 0 0 / 35%); } .vc-author-modal-root+[class*="footer"] { diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx index 4c660cb3a..e848106c8 100644 --- a/src/components/settings/tabs/sync/CloudTab.tsx +++ b/src/components/settings/tabs/sync/CloudTab.tsx @@ -123,14 +123,15 @@ function CloudTab() { Choose which cloud backend to use for storing your settings. You can switch between Equicord's and Vencord's cloud services, or use a self-hosted instance. - o.value === cloud.url)} - onChange={v => changeUrl(v)} - className={Margins.bottom16} - closeOnSelect={true} - renderOptionPrefix={o => o?.value?.includes("equicord") ? : } - /> +
+ o.value === cloud.url)?.value} + onChange={v => changeUrl(v)} + closeOnSelect={true} + renderOptionPrefix={o => o?.value?.includes("equicord") ? : } + /> +
diff --git a/src/components/settings/tabs/vencord/index.tsx b/src/components/settings/tabs/vencord/index.tsx index ee1d5eadf..37901be6d 100644 --- a/src/components/settings/tabs/vencord/index.tsx +++ b/src/components/settings/tabs/vencord/index.tsx @@ -81,11 +81,18 @@ function EquicordSettings() { restartRequired: true, warning: { enabled: false }, }, + (!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && { + key: "mainWindowFrameless", + title: "Disable the Main Window Frame", + description: "Remove the native window frame for a cleaner look. You can still move the window by dragging the title bar area.", + restartRequired: true, + warning: { enabled: false }, + }, !IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS ? { key: "frameless", - title: "Disable the Window Frame", + title: "Disable All Window Frames", description: "Remove the native window frame for a cleaner look. You can still move the window by dragging the title bar area.", restartRequired: true, warning: { enabled: false }, @@ -96,7 +103,8 @@ function EquicordSettings() { description: "Replace Discord's custom title bar with the standard Windows title bar. This may improve compatibility with some window management tools.", restartRequired: true, warning: { enabled: false }, - }), + } + ), !IS_WEB && { key: "transparent", title: "Enable Window Transparency", diff --git a/src/equicordplugins/_api/userArea.ts b/src/equicordplugins/_api/userArea.ts index 4f0d731c6..53cf52cde 100644 --- a/src/equicordplugins/_api/userArea.ts +++ b/src/equicordplugins/_api/userArea.ts @@ -19,7 +19,7 @@ export default definePlugin({ patches: [ { - find: ".WIDGETS_RTC_UPSELL_COACHMARK),", + find: ".DISPLAY_NAME_STYLES_COACHMARK),", replacement: [ { match: /(?<=className:(\i)\.\i,style:\i,)children:\[/, diff --git a/src/equicordplugins/clickableRoles/index.tsx b/src/equicordplugins/clickableRoles/index.tsx new file mode 100644 index 000000000..6f4005e58 --- /dev/null +++ b/src/equicordplugins/clickableRoles/index.tsx @@ -0,0 +1,187 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { openUserProfile } from "@utils/discord"; +import { isTruthy } from "@utils/guards"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import type { Role } from "@vencord/discord-types"; +import { findByPropsLazy } from "@webpack"; +import { Constants, GuildRoleStore, IconUtils, Popout, RestAPI, ScrollerThin, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common"; + +const logger = new Logger("ClickableRoles"); + +const cl = classNameFactory("vc-clickableroles-"); + +const GuildActions = findByPropsLazy("requestMembersById", "banUser"); + +const MAX_VISIBLE_MEMBERS = 20; +const CACHE_TTL = 60_000; + +const memberCache = new Map(); + +function getCachedMemberIds(guildId: string, roleId: string) { + const key = `${guildId}-${roleId}`; + const cached = memberCache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) return cached.ids; + return null; +} + +function setCachedMemberIds(guildId: string, roleId: string, ids: string[]) { + memberCache.set(`${guildId}-${roleId}`, { ids, timestamp: Date.now() }); +} + +function RoleMembersList({ roleId, guildId, closePopout, setPopoutRef }: { roleId: string; guildId: string; closePopout(): void; setPopoutRef(ref: HTMLDivElement | null): void; }) { + const [memberIds, setMemberIds] = useState([]); + const [loading, setLoading] = useState(true); + + const role = useStateFromStores([GuildRoleStore], () => GuildRoleStore.getRole(guildId, roleId)); + + const [totalCount, setTotalCount] = useState(0); + + useEffect(() => { + let cancelled = false; + + const cached = getCachedMemberIds(guildId, roleId); + if (cached) { + setTotalCount(cached.length); + const visible = cached.slice(0, MAX_VISIBLE_MEMBERS); + setMemberIds(visible); + if (visible.length) GuildActions.requestMembersById(guildId, visible, false); + setLoading(false); + } else { + RestAPI.get({ + url: Constants.Endpoints.GUILD_ROLE_MEMBER_IDS(guildId, roleId), + }).then(res => { + if (cancelled) return; + const ids = res.body as string[]; + setCachedMemberIds(guildId, roleId, ids); + setTotalCount(ids.length); + const visible = ids.slice(0, MAX_VISIBLE_MEMBERS); + setMemberIds(visible); + if (visible.length) GuildActions.requestMembersById(guildId, visible, false); + setLoading(false); + }).catch(e => { + if (cancelled) return; + logger.error("Failed to fetch role members", e); + setLoading(false); + }); + } + + return () => { cancelled = true; }; + }, [guildId, roleId]); + + const users = useStateFromStores( + [UserStore], + () => memberIds.map(id => UserStore.getUser(id)).filter(isTruthy), + [memberIds] + ); + + return ( +
+
+ + {role?.name ?? "Unknown Role"} + {totalCount} +
+ + {loading ? ( +
Loading members...
+ ) : users.length === 0 ? ( +
No members found.
+ ) : users.map(user => ( +
{ + closePopout(); + openUserProfile(user.id); + }} + > + + {user.globalName ?? user.username} +
+ ))} + {totalCount > MAX_VISIBLE_MEMBERS && ( +
and {totalCount - MAX_VISIBLE_MEMBERS} more
+ )} +
+
+ ); +} + +function ClickableRole({ roleId, guildId, children }: { roleId: string; guildId: string; children: React.ReactNode; }) { + const ref = useRef(null); + + return ( + ( + + )} + > + {popoutProps => ( +
+ {children} +
+ )} +
+ ); +} + +const WrappedClickableRole = ErrorBoundary.wrap(ClickableRole, { noop: true }); + +export default definePlugin({ + name: "ClickableRoles", + description: "Click on roles in user profiles and the member list to see which members have them.", + authors: [Devs.prism], + + patches: [ + { + find: "#{intl::T+3Adg::raw}", + replacement: { + match: /(colors:\i\}\);)return(.*?enableTooltip:!1\}\):null,\i\]\}\))/, + replace: "$1return $self.wrapRolePill(arguments[0],()=>$2)", + }, + }, + { + find: 'tutorialId:"whos-online"', + replacement: { + match: /\((function\(\i\)\{let\{id:.*?#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.*?\}\))\}\);/, + replace: "($self.wrapRoleGroup($1}));", + }, + }, + ], + + wrapRoleGroup(originalFn: (props: { id: string; guildId: string; }) => React.ReactNode) { + return (props: { id: string; guildId: string; }) => { + const result = originalFn(props); + if (!GuildRoleStore.getRole(props.guildId, props.id)) return result; + return ( + + {result} + + ); + }; + }, + + wrapRolePill(props: { role: Role; guildId: string; }, renderOriginal: () => React.ReactNode) { + return ( + + {renderOriginal()} + + ); + }, +}); diff --git a/src/equicordplugins/clickableRoles/style.css b/src/equicordplugins/clickableRoles/style.css new file mode 100644 index 000000000..59a1ce523 --- /dev/null +++ b/src/equicordplugins/clickableRoles/style.css @@ -0,0 +1,80 @@ +.vc-clickableroles-trigger { + cursor: pointer; +} + +.vc-clickableroles-popout { + background-color: var(--background-surface-highest); + border-radius: 8px; + padding: 12px; + width: 250px; + max-height: var(--reference-position-layer-max-height, 350px); + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.vc-clickableroles-list { + max-height: 260px; + min-height: 0; + flex: 1; +} + +.vc-clickableroles-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; +} + +.vc-clickableroles-color { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.vc-clickableroles-name { + font-weight: 600; + color: var(--interactive-text-default); +} + +.vc-clickableroles-count { + color: var(--text-muted); + margin-left: auto; + font-size: 12px; +} + +.vc-clickableroles-empty { + padding: 8px; + text-align: center; + color: var(--text-muted); + font-size: 13px; +} + +.vc-clickableroles-user { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + cursor: pointer; + border-radius: 4px; + color: var(--interactive-text-default); + font-size: 14px; +} + +.vc-clickableroles-user:hover { + background-color: var(--interactive-background-hover); +} + +.vc-clickableroles-avatar { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.vc-clickableroles-overflow { + padding: 6px; + text-align: center; + color: var(--text-muted); + font-size: 12px; +} diff --git a/src/equicordplugins/components.dev/index.tsx b/src/equicordplugins/components.dev/index.tsx index 68f05fb63..a8269b4bc 100644 --- a/src/equicordplugins/components.dev/index.tsx +++ b/src/equicordplugins/components.dev/index.tsx @@ -6,6 +6,7 @@ import "./styles.css"; +import { ComponentsIcon } from "@components/Icons"; import SettingsPlugin from "@plugins/_core/settings"; import { Devs } from "@utils/constants"; import { removeFromArray } from "@utils/misc"; @@ -14,30 +15,6 @@ import { SettingsRouter } from "@webpack/common"; import ComponentsTab from "./components/ComponentsTab"; -function ComponentsIcon(props: React.SVGProps) { - return ( - - - - - ); -} - export default definePlugin({ name: "Components", description: "Adds a new tab to settings to browse Discord components.", diff --git a/src/equicordplugins/declutter/index.tsx b/src/equicordplugins/declutter/index.tsx index cf4c44dc0..0eb0d2a88 100644 --- a/src/equicordplugins/declutter/index.tsx +++ b/src/equicordplugins/declutter/index.tsx @@ -4,8 +4,13 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import "./style.css"; + import { definePluginSettings, migratePluginSetting, migratePluginSettings } from "@api/Settings"; +import { Divider } from "@components/Divider"; import { HeadingSecondary } from "@components/Heading"; +import { Notice } from "@components/Notice"; +import { classNameFactory } from "@utils/css"; import { Devs, EquicordDevs } from "@utils/index"; import definePlugin, { OptionType } from "@utils/types"; @@ -24,17 +29,13 @@ for (const [oldKey, newKey] of migrationsAnammox) { migratePluginSetting("Declutter", newKey, oldKey); } +const cl = classNameFactory("vc-declutter-"); + export const settings = definePluginSettings({ userProfileHeader: { type: OptionType.COMPONENT, component: () => SectionSeparator("User Profile"), }, - removeAvatarDecoration: { - type: OptionType.BOOLEAN, - description: "Remove avatar decorations.", - default: true, - restartNeeded: true, - }, removeNameplate: { type: OptionType.BOOLEAN, description: "Remove nameplates.", @@ -59,11 +60,13 @@ export const settings = definePluginSettings({ default: true, restartNeeded: true }, - removeUsernameStyles: { - type: OptionType.BOOLEAN, - description: "Remove username colors and effects.", - default: true, - restartNeeded: true + accessibilityNotice: { + type: OptionType.COMPONENT, + component: () => ( + + Discord already has a built-in username style option in Accessibility settings. + + ) }, friendsListHeader: { type: OptionType.COMPONENT, @@ -72,7 +75,7 @@ export const settings = definePluginSettings({ removeShopAboveDM: { type: OptionType.BOOLEAN, description: "Remove shops above DMs list.", - default: true, + default: false, restartNeeded: true, }, removeQuestsAboveDM: { @@ -125,29 +128,21 @@ export const settings = definePluginSettings({ function SectionSeparator(title: string) { return ( - <> -
- {title} -
- +
+ + + {title} + +
); } export default definePlugin({ name: "Declutter", - description: "Declutter discord ui by removing unwanted elements such as profile customizations, shops, quests and more.", + description: "Cleans up Discord by removing non-essential UI elements like profile effects, shop tabs, boosts, and more.", authors: [EquicordDevs.Leon135, Devs.prism, Devs.Kyuuhachi], settings, patches: [ - { - // Avatar decoration - find: "getAvatarDecorationURL:", - replacement: { - match: /(?<=function \i\((\i)\){)(?=.{0,150}let{avatarDecoration)/, - replace: "$&return null;" - }, - predicate: () => settings.store.removeAvatarDecoration, - }, { // Nameplate find: "#{intl::AVATAR_MALLOW}", @@ -176,20 +171,13 @@ export default definePlugin({ predicate: () => settings.store.removeClanTag, }, { - // Username styles and allways show username - find: ".NITRO_PRIVACY_PERK_BETA_COACHMARK));", - replacement: [ - { - match: /displayNameStyles:(\i),/, - replace: "displayNameStyles:void 0,", - predicate: () => settings.store.removeUsernameStyles - }, - { - match: /hoverText:(\i),forceHover:\i,children:/g, - replace: "hoverText:$1,forceHover:!0,children:", - predicate: () => settings.store.alwaysShowUsername - }, - ], + // Always show username + find: ".DISPLAY_NAME_STYLES_COACHMARK),", + replacement: { + match: /hoverText:(\i),forceHover:\i,children:/g, + replace: "hoverText:$1,forceHover:!0,children:" + }, + predicate: () => settings.store.alwaysShowUsername }, { // Button tooltips in user area diff --git a/src/equicordplugins/declutter/style.css b/src/equicordplugins/declutter/style.css new file mode 100644 index 000000000..f979e2235 --- /dev/null +++ b/src/equicordplugins/declutter/style.css @@ -0,0 +1,19 @@ +.vc-declutter-accessibility-notice { + margin-top: 8px; + margin-bottom: 8px; +} + +.vc-declutter-section-separator { + margin-top: 12px; + margin-bottom: 6px; +} + +.vc-declutter-section-title { + margin-top: 14px; + margin-bottom: 0; + font-size: 16px; + line-height: 20px; + text-transform: uppercase; + letter-spacing: 0.02em; + color: var(--text-muted); +} diff --git a/src/equicordplugins/equicordHelper/index.tsx b/src/equicordplugins/equicordHelper/index.tsx index 59cce166f..3c6bb883f 100644 --- a/src/equicordplugins/equicordHelper/index.tsx +++ b/src/equicordplugins/equicordHelper/index.tsx @@ -188,16 +188,6 @@ export default definePlugin({ replace: "{_enableHangStatus:$1=false," } }, - // Always show open legacy settings - { - find: ".DEVELOPER_SECTION,", - replacement: [ - { - match: /\i\.\i\.isDeveloper/, - replace: "true" - }, - ] - }, // Force Role Icon { find: "Message Username", diff --git a/src/equicordplugins/exitSounds/index.tsx b/src/equicordplugins/exitSounds/index.tsx index 3f161fbb6..db67fe6ed 100644 --- a/src/equicordplugins/exitSounds/index.tsx +++ b/src/equicordplugins/exitSounds/index.tsx @@ -30,7 +30,7 @@ function GuildSelector() { return ( o.value === soundGuildId)} + value={options.find(o => o.value === soundGuildId)?.value} placeholder="Select a server..." maxVisibleItems={6} closeOnSelect={true} diff --git a/src/equicordplugins/favouriteAnything/components.tsx b/src/equicordplugins/favouriteAnything/components.tsx index ac38c411e..0f6db374f 100644 --- a/src/equicordplugins/favouriteAnything/components.tsx +++ b/src/equicordplugins/favouriteAnything/components.tsx @@ -7,9 +7,9 @@ import { BaseText } from "@components/BaseText"; import { Button } from "@components/Button"; import { LazyComponentWrapper } from "@utils/lazyReact"; -import { Channel, Message, MessageAttachment, ScrollerBaseRef } from "@vencord/discord-types"; +import { Message, MessageAttachment, ScrollerBaseRef } from "@vencord/discord-types"; import { ChannelType } from "@vencord/discord-types/enums"; -import { findByCodeLazy, findByPropsLazy, findComponentByCode, findComponentByCodeLazy, findCssClassesLazy, proxyLazyWebpack } from "@webpack"; +import { findByCodeLazy, findComponentByCode, findComponentByCodeLazy, findCssClassesLazy, proxyLazyWebpack } from "@webpack"; import { ChannelStore, ExpressionPickerStore, ListScrollerThin, lodash, PermissionsBits, PermissionStore, React, useCallback, useEffect, useMemo, useRef, useState, useStateFromStores } from "@webpack/common"; import { ReactNode } from "react"; @@ -22,11 +22,24 @@ const ManaSearchBar = findComponentByCodeLazy("#{intl::SEARC const FavoriteButton = findComponentByCodeLazy("#{intl::GIF_TOOLTIP_ADD_TO_FAVORITES}"); const SendIcon = findComponentByCodeLazy("M6.6 10.02 14 11.4a.6.6"); -const DmChannel: Channel & { new(base?: Partial): Channel; } = findByPropsLazy("fromServer", "sortRecipients"); -const MessageClass: Message & { new(base?: Partial): Message; } = findByCodeLazy(";mentionChannels;mentionGames;"); +const createChannelRecordFromServer = findByCodeLazy(".GUILD_TEXT]", "fromServer)"); +const createMessageRecord = findByCodeLazy(".createFromServer(", ".isBlockedForMessage", "messageReference:"); const Classes = findCssClassesLazy("gifFavoriteButton", "ctaButtonContainer"); +function createPreviewMessage(attachment: MessageAttachment, channelId: string) { + const previewMessage = { + id: `favourite-anything-preview-${attachment.id}`, + attachments: [attachment], + channel_id: channelId, + content: "", + type: 0, + timestamp: new Date().toISOString() + }; + + return createMessageRecord(previewMessage) as Message; +} + export const AttachmentPreview = proxyLazyWebpack(() => { // findComponentByCodeLazy doesn't work properly with component classes, this must be kept within the lazy scope const MessageComponent = findComponentByCode("this.renderAttachments") as LazyComponentWrapper; @@ -37,12 +50,12 @@ export const AttachmentPreview = proxyLazyWebpack(() => { } } - const channel = Object.freeze(new DmChannel({ id: "0", type: ChannelType.GUILD_TEXT })); + const channel = Object.freeze(createChannelRecordFromServer({ id: "0", type: ChannelType.GUILD_TEXT })); return function AttachmentPreview({ attachment }: AttachmentsComponentProps) { const message = useMemo( - () => new MessageClass({ attachments: [attachment], channel_id: channel.id }), - [attachment] + () => createPreviewMessage(attachment, channel.id), + [attachment, channel.id] ); return ( diff --git a/src/equicordplugins/hopOn/index.tsx b/src/equicordplugins/hopOn/index.tsx index 46e5326c9..cd0de30ed 100644 --- a/src/equicordplugins/hopOn/index.tsx +++ b/src/equicordplugins/hopOn/index.tsx @@ -31,7 +31,7 @@ const settings = definePluginSettings({ }); export default definePlugin({ name: "HopOn", - description: "Hop on Fortnite or Hop on bloons :3", + description: "Hop on! Opens a configurable URL when a message matches a custom regex in the current channel.", authors: [Devs.ImLvna], settings, flux: { diff --git a/src/equicordplugins/instantScreenshare/index.tsx b/src/equicordplugins/instantScreenshare/index.tsx index 1a29d719f..1d0160048 100644 --- a/src/equicordplugins/instantScreenshare/index.tsx +++ b/src/equicordplugins/instantScreenshare/index.tsx @@ -15,8 +15,9 @@ import { ChannelStore, MediaEngineStore, PermissionsBits, PermissionStore, Selec import { getCurrentMedia, settings } from "./utils"; -let hasStreamed; +let hasStreamed, isStreaming, streamKey; const startStream = findByCodeLazy('type:"STREAM_START"'); +const stopStream = findByCodeLazy('type:"STREAM_STOP"'); const StreamPreviewSettings = getUserSettingLazy("voiceAndVideo", "disableStreamPreviews")!; const ApplicationStreamingSettingsStore = findStoreLazy("ApplicationStreamingSettingsStore"); @@ -44,14 +45,18 @@ async function autoStartStream(instant = true) { let sourceId = streamMedia.id; if (streamMedia.type === "video_device") sourceId = `camera:${streamMedia.id}`; - startStream(channel.guild_id ?? null, selected, { - "pid": null, - "sourceId": sourceId, - "sourceName": streamMedia.name, - "audioSourceId": streamMedia.name, - "sound": soundshareEnabled, - "previewDisabled": preview - }); + if (isStreaming && streamKey.endsWith(UserStore.getCurrentUser().id)) { + stopStream(streamKey); + } else { + startStream(channel.guild_id ?? null, selected, { + "pid": null, + "sourceId": sourceId, + "sourceName": streamMedia.name, + "audioSourceId": streamMedia.name, + "sound": soundshareEnabled, + "previewDisabled": preview + }); + } } export default definePlugin({ @@ -122,6 +127,14 @@ export default definePlugin({ break; } + }, + STREAM_CREATE: d => { + streamKey = d; + isStreaming = true; + }, + STREAM_DELETE: d => { + streamKey = d; + isStreaming = false; } }, diff --git a/src/equicordplugins/instantScreenshare/utils.tsx b/src/equicordplugins/instantScreenshare/utils.tsx index b6dbbece8..22ac85cea 100644 --- a/src/equicordplugins/instantScreenshare/utils.tsx +++ b/src/equicordplugins/instantScreenshare/utils.tsx @@ -105,7 +105,7 @@ function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps) placeholder="Select a media source to stream " maxVisibleItems={5} options={options} - value={options.find(o => o.value === streamMedia)} + value={options.find(o => o.value === streamMedia)?.value} onChange={v => settings.store.streamMedia = v} closeOnSelect /> diff --git a/src/equicordplugins/jumpTo/index.tsx b/src/equicordplugins/jumpTo/index.tsx index 36bfb3b7a..537d9c19f 100644 --- a/src/equicordplugins/jumpTo/index.tsx +++ b/src/equicordplugins/jumpTo/index.tsx @@ -129,4 +129,4 @@ export default definePlugin({ "user-context": UserMenuPatch, "message": MessageMenuPatch } -}); \ No newline at end of file +}); diff --git a/src/equicordplugins/messagePeek/index.tsx b/src/equicordplugins/messagePeek/index.tsx index 8d07c8492..866e704e9 100644 --- a/src/equicordplugins/messagePeek/index.tsx +++ b/src/equicordplugins/messagePeek/index.tsx @@ -224,7 +224,7 @@ function Timestamp({ channel }: { channel: Channel; }) { if (!lastMessage) return null; const timestamp = SnowflakeUtils.extractTimestamp(lastMessage.id); - const className = ExperimentStore.getUserExperimentBucket("2026-01-favorites-server") >= 1 ? cl("timestamp-favorites") : cl("timestamp"); + const className = ExperimentStore.getUserExperimentBucket("2026-01-favorites-server") > 0 ? cl("timestamp-favorites") : cl("timestamp"); return {formatRelativeTime(timestamp)}; } diff --git a/src/equicordplugins/moreCommands/utils.ts b/src/equicordplugins/moreCommands/utils.ts index 38a53a517..a5a0f7815 100644 --- a/src/equicordplugins/moreCommands/utils.ts +++ b/src/equicordplugins/moreCommands/utils.ts @@ -11,7 +11,7 @@ export const settings = definePluginSettings({ addFreakyEnding: { type: OptionType.BOOLEAN, description: "Add ๐Ÿ‘… or โค๏ธ at the end", - default: true + default: false }, uwuEveryMessage: { description: "Make every single message uwuified", diff --git a/src/equicordplugins/moyai/index.ts b/src/equicordplugins/moyai/index.ts index 2a5ad52d1..4c6d79391 100644 --- a/src/equicordplugins/moyai/index.ts +++ b/src/equicordplugins/moyai/index.ts @@ -90,7 +90,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "Moyai", authors: [Devs.Megu, Devs.Nuckyz], - description: "๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ๐Ÿ—ฟ", + description: "Plays a ๐Ÿ—ฟ sound effect whenever a moyai emoji is sent, reacted, or used as a voice effect in your current channel.", settings, flux: { diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 0054b321b..dcf547e71 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -52,7 +52,7 @@ export default definePlugin({ patches: [ { - find: ".WIDGETS_RTC_UPSELL_COACHMARK),", + find: ".DISPLAY_NAME_STYLES_COACHMARK),", replacement: { // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,occluded:)/, diff --git a/src/equicordplugins/platformSpoofer/index.tsx b/src/equicordplugins/platformSpoofer/index.tsx index d6366bbf8..aab45545a 100644 --- a/src/equicordplugins/platformSpoofer/index.tsx +++ b/src/equicordplugins/platformSpoofer/index.tsx @@ -67,13 +67,6 @@ export default definePlugin({ replace: "$1{...$2,...$self.getPlatform(true)},presence" } }, - { - find: "#{intl::POPOUT_STAY_ON_TOP}),icon:", - replacement: { - match: /(?<=CallTile.{0,15}\.memo\((\i)=>\{)/, - replace: "$1.platform = $self.getPlatform(false, $1?.participantUserId)?.vcIcon || $1?.platform;" - } - }, { find: '("AppSkeleton");', replacement: { @@ -88,19 +81,19 @@ export default definePlugin({ if (bypass || userId === UserStore.getCurrentUser().id) { switch (platform) { case "desktop": - return { browser: "Discord Client", vcIcon: 0 }; + return { browser: "Discord Client" }; case "web": - return { browser: "Discord Web", vcIcon: 0 }; + return { browser: "Discord Web" }; case "ios": - return { browser: "Discord iOS", vcIcon: 1 }; + return { browser: "Discord iOS" }; case "android": - return { browser: "Discord Android", vcIcon: 1 }; + return { browser: "Discord Android" }; case "xbox": - return { browser: "Discord Embedded", vcIcon: 2 }; + return { browser: "Discord Embedded" }; case "playstation": - return { browser: "Discord Embedded", vcIcon: 3 }; + return { browser: "Discord Embedded" }; case "vr": - return { browser: "Discord VR", vcIcon: 4 }; + return { browser: "Discord VR" }; default: return null; } diff --git a/src/equicordplugins/songSpotlight.desktop/index.tsx b/src/equicordplugins/songSpotlight.desktop/index.tsx index 83ecdf203..ed8d84c12 100644 --- a/src/equicordplugins/songSpotlight.desktop/index.tsx +++ b/src/equicordplugins/songSpotlight.desktop/index.tsx @@ -6,15 +6,14 @@ import "./style.css"; -import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin from "@utils/types"; import { useAuthorizationStore } from "./lib/stores/AuthorizationStore"; import { useSongStore } from "./lib/stores/SongStore"; import { Native } from "./service"; -import Settings from "./ui/settings"; +import settings from "./settings"; import ProfileSongs from "./ui/songs/ProfileSongs"; import WidgetSongs from "./ui/songs/WidgetSongs"; @@ -22,12 +21,7 @@ export default definePlugin({ name: "SongSpotlight", description: "Show off songs on your profile", authors: [EquicordDevs.nexpid], - settings: definePluginSettings({ - manager: { - type: OptionType.COMPONENT, - component: () => , - }, - }), + settings, patches: [ // Personal profile popout { @@ -50,7 +44,7 @@ export default definePlugin({ find: ".SIDEBAR}),nicknameIcons:", replacement: { match: /{userId:(\i)\.id}\)}\).{0,100}]}\)(?=\]\}\))/, - replace: "$&,$self.renderProfileSongs({userId:$1.id})", + replace: "$&,$self.renderProfileSongs({userId:$1.id,isSidebar:true})", }, }, // Full profile modal sections (lazy loaded) @@ -58,14 +52,14 @@ export default definePlugin({ find: ".MUTUAL_GUILDS})),", replacement: { match: /(\i).push\({text.{0,50}}\);/, - replace: "$&$1.push({text:\"Song Spotlight\",section:\"SONG_SPOTLIGHT\"});", + replace: '$&$1.push({text:"Song Spotlight",section:"SONG_SPOTLIGHT"});', }, }, { find: ".hasUnsavedChanges()&&", replacement: { match: /({user:(\i),.{0,80}return (\i===))/, - replace: "$1\"SONG_SPOTLIGHT\"?$self.renderWidgetSongs({user:$2}):$3", + replace: '$1"SONG_SPOTLIGHT"?$self.renderWidgetSongs({user:$2}):$3', }, }, ], diff --git a/src/equicordplugins/songSpotlight.desktop/lib/api.ts b/src/equicordplugins/songSpotlight.desktop/lib/api.ts index 868f79132..9bad09bca 100644 --- a/src/equicordplugins/songSpotlight.desktop/lib/api.ts +++ b/src/equicordplugins/songSpotlight.desktop/lib/api.ts @@ -9,7 +9,6 @@ import { showToast, Toasts, UserStore } from "@webpack/common"; import { useAuthorizationStore } from "./stores/AuthorizationStore"; import { useSongStore } from "./stores/SongStore"; -import { logger } from "./utils"; export const apiConstants = { api: "https://dc.songspotlight.nexpid.xyz/", @@ -20,16 +19,36 @@ export const apiConstants = { songLimit: 6, }; -export async function authFetch(_url: string | URL, options?: RequestInit) { - const url = new URL(_url); +let refreshPromise: Promise | undefined; +async function refreshAccessToken() { + const token = useAuthorizationStore.getState().getToken(); + if (!token) return false; + return refreshPromise ??= fetch(new URL("api/auth/refresh", apiConstants.api), { + method: "POST", + headers: { + "X-Refresh-Token": token.refresh, + }, + body: token.access, + }).then(async res => { + if (!res.ok) return false; + + const access = await res.text(); + useAuthorizationStore.getState().setToken(access, token.refresh); + return true; + }).finally(() => refreshPromise = undefined); +} + +export async function authFetch(url: string | URL, options?: RequestInit, retried = false) { + url = new URL(url); try { + const token = useAuthorizationStore.getState().getToken(); const res = await fetch(url, { ...options, headers: { ...options?.headers, - Authorization: useAuthorizationStore.getState().getToken()!, - }, + Authorization: token?.access, + } as HeadersInit, }); if (res.ok) return res; @@ -37,36 +56,29 @@ export async function authFetch(_url: string | URL, options?: RequestInit) { // not modified if (res.status === 304) return null; + const text = await res.text(); + // unauthorized if (res.status === 401) { - useAuthorizationStore.getState().deleteToken(); + const retry = !retried && await refreshAccessToken(); + if (retry) return await authFetch(url, options, true); + else { + useAuthorizationStore.getState().deleteTokens(); + showToast("You have been signed out from Song Spotlight. Please sign in again.", Toasts.Type.FAILURE); + } + } else { + showToast( + !text.includes("") && res.status >= 400 && res.status <= 599 + ? `Song Spotlight: ${text}` + : `Song Spotlight fetch error at ${url.pathname}`, + Toasts.Type.FAILURE, + ); } - const text = await res.text(); - showToast( - !text.includes("") && res.status >= 400 && res.status <= 599 - ? `Song Spotlight: ${text}` - : `Song Spotlight fetch error at ${url.pathname}`, - Toasts.Type.FAILURE, - ); - - logger.error( - "Got an authFetch response error", - options?.method ?? "GET", - url.toString(), - res.status, - text, - ); throw new Error(text); } catch (error) { showToast(`Song Spotlight: ${error}`, Toasts.Type.FAILURE); - logger.error( - "Got an authFetch request error", - options?.method ?? "GET", - url.toString(), - error, - ); throw error; } } diff --git a/src/equicordplugins/songSpotlight.desktop/lib/oauth2.tsx b/src/equicordplugins/songSpotlight.desktop/lib/oauth2.tsx index 3ff3bcdf9..7bf8e2847 100644 --- a/src/equicordplugins/songSpotlight.desktop/lib/oauth2.tsx +++ b/src/equicordplugins/songSpotlight.desktop/lib/oauth2.tsx @@ -30,15 +30,22 @@ export function presentOAuth2Modal() { const url = new URL(location); url.searchParams.append("whois", "equicord"); - const token = await (await authFetch(url))?.text(); - if (!token) return; + const res = await authFetch(url); + if (!res) throw "Response wasn't ok"; - useAuthorizationStore.getState().setToken(token); + const access = await res.text(); + if (!access) throw "Access token is missing"; + + const refresh = res.headers.get("X-Refresh-Token"); + if (!refresh) throw "Refresh token is missing"; + + useAuthorizationStore.getState().setToken(access, refresh); getData(); showToast("Successfully authorized!", Toasts.Type.SUCCESS); } catch (error) { logger.error("Got an error during OAuth2", error); + if (typeof error === "string") showToast(error, Toasts.Type.FAILURE); } }} /> diff --git a/src/equicordplugins/songSpotlight.desktop/lib/stores/AuthorizationStore.ts b/src/equicordplugins/songSpotlight.desktop/lib/stores/AuthorizationStore.ts index 48e1469d5..368166764 100644 --- a/src/equicordplugins/songSpotlight.desktop/lib/stores/AuthorizationStore.ts +++ b/src/equicordplugins/songSpotlight.desktop/lib/stores/AuthorizationStore.ts @@ -10,11 +10,16 @@ import { UserStore, zustandCreate, zustandPersist } from "@webpack/common"; import { PersistedZustandStore, ZustandDefinition } from "./zustand"; +export interface Token { + access: string; + refresh: string; +} + interface AuthorizationState { - tokens: Record; - getToken(): string | undefined; - setToken(token: string): void; - deleteToken(): void; + tokens: Record; + getToken(): Token | undefined; + setToken(access: string, refresh: string): void; + deleteTokens(): void; isAuthorized(): boolean; } @@ -26,17 +31,18 @@ export const useAuthorizationStore: PersistedZustandStore = getToken() { return get().tokens[UserStore.getCurrentUser()?.id]; }, - setToken(token) { + setToken(access, refresh) { const userId = UserStore.getCurrentUser()?.id; if (userId) { set({ - tokens: Object.assign(get().tokens, { - [userId]: token, - }), + tokens: { + ...get().tokens, + [userId]: { access, refresh }, + }, }); } }, - deleteToken() { + deleteTokens() { set({ tokens: {} }); }, isAuthorized() { @@ -45,11 +51,24 @@ export const useAuthorizationStore: PersistedZustandStore = })) as ZustandDefinition, { name: "songspotlight-auth", + version: 1, + migrate(persisted: any, version: number) { + if (version === 0) { + persisted.tokens = Object.fromEntries( + Object.entries(persisted.tokens).map(([userId, access]) => [userId, { + access, + refresh: "", + }]), + ); + } + + return persisted; + }, storage: { async getItem(name: string) { return (await DataStore.get(name)) ?? null; }, - async setItem(name: string, value: string) { + async setItem(name: string, value: unknown) { return await DataStore.set(name, value); }, async removeItem(name: string) { diff --git a/src/equicordplugins/songSpotlight.desktop/settings.tsx b/src/equicordplugins/songSpotlight.desktop/settings.tsx new file mode 100644 index 000000000..97af8cc3f --- /dev/null +++ b/src/equicordplugins/songSpotlight.desktop/settings.tsx @@ -0,0 +1,35 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { makeRange, OptionType } from "@utils/types"; + +import { apiConstants } from "./lib/api"; +import Settings from "./ui/settings"; + +export default definePluginSettings({ + collapseSongList: { + type: OptionType.BOOLEAN, + description: "Collapses the song list on user profiles to a button which opens a separate menu", + default: false, + }, + profileSongsLimit: { + type: OptionType.SLIDER, + description: "How many songs are shown when initially clicking on a user", + default: apiConstants.songLimit, + markers: makeRange(1, apiConstants.songLimit), + }, + manager: { + type: OptionType.COMPONENT, + component: () => , + }, +}, { + profileSongsLimit: { + disabled() { + return this.store.collapseSongList; + }, + }, +}); diff --git a/src/equicordplugins/songSpotlight.desktop/style.css b/src/equicordplugins/songSpotlight.desktop/style.css index 42f8cb293..bb44f9aaa 100644 --- a/src/equicordplugins/songSpotlight.desktop/style.css +++ b/src/equicordplugins/songSpotlight.desktop/style.css @@ -23,7 +23,10 @@ -webkit-line-clamp: 1; line-clamp: 1; -webkit-box-orient: vertical; + min-width: 0; + max-width: 100%; overflow: hidden; + text-overflow: ellipsis; } /* components */ @@ -137,13 +140,13 @@ --list-height: calc(var(--base-height) + 80px); --number-size: 32px; - padding: 8px; - gap: 12px; - overflow: visible; -} + display: flex; + flex-direction: column; + gap: 6px; -.user-profile-sidebar .vc-songspotlight-songs-container { - padding: 12px; + &.vc-songspotlight-songs-container-sidebar { + padding: 12px; + } } .vc-songspotlight-songs-more { @@ -194,6 +197,10 @@ } } +.user-profile-sidebar .vc-songspotlight-song-container { + padding: 8px; +} + .user-profile-popout:not(.custom-user-profile-theme) .vc-songspotlight-song-container { background: var(--opacity-16); } @@ -211,7 +218,6 @@ grid-template-columns: 1fr auto; grid-template-rows: 1fr auto; gap: 8px; - overflow: visible; border-radius: inherit; } @@ -220,7 +226,7 @@ grid-column: 1; grid-row: 1 / 3; border-radius: inherit; - overflow: visible; + min-width: 0; } .vc-songspotlight-song-cover { @@ -232,7 +238,6 @@ flex-shrink: 0; box-shadow: 0 3px 12px 0 rgb(0 0 0 / 16%); transition: 0.18s transform; - overflow: visible; &:hover { transform: scale(1.03); diff --git a/src/equicordplugins/songSpotlight.desktop/ui/common.tsx b/src/equicordplugins/songSpotlight.desktop/ui/common.tsx index 0e5454fad..eda340ce4 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/common.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/common.tsx @@ -12,6 +12,14 @@ import { Tooltip } from "@webpack/common"; import { ComponentType, HTMLAttributes, JSX } from "react"; export const OverlayClasses = findCssClassesLazy("overlay", "inner", "outer"); +export const ContainerClasses = findCssClassesLazy( + "breadcrumb", + "innerContainer", + "icons", + "icon", + "displayCount", + "displayCountText", +); export const CardClasses = findCssClassesLazy("card", "headerText", "headerContextMenu"); export const WidgetClasses = findCssClassesLazy("tabPanelScroller", "tabListScroller"); diff --git a/src/equicordplugins/songSpotlight.desktop/ui/settings/index.tsx b/src/equicordplugins/songSpotlight.desktop/ui/settings/index.tsx index ef42841de..7a36d6f73 100644 --- a/src/equicordplugins/songSpotlight.desktop/ui/settings/index.tsx +++ b/src/equicordplugins/songSpotlight.desktop/ui/settings/index.tsx @@ -85,7 +85,7 @@ interface SettingsProps { } export default function Settings({ templateData }: SettingsProps) { - const { isAuthorized, deleteToken } = useAuthorizationStore(); + const { isAuthorized, deleteTokens } = useAuthorizationStore(); const { self } = useSongStore(); const ticked = useRef(false); @@ -172,7 +172,7 @@ export default function Settings({ templateData }: SettingsProps) {
diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 60cc515f4..911a40473 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -72,6 +72,7 @@ if (!IS_VANILLA) { constructor(options: BrowserWindowConstructorOptions) { if (options?.webPreferences?.preload && options.title) { const original = options.webPreferences.preload; + const isMainWindow = options.title === "Discord"; options.webPreferences.preload = join(__dirname, "preload.js"); options.webPreferences.sandbox = false; // work around discord unloading when in background @@ -79,6 +80,8 @@ if (!IS_VANILLA) { if (settings.frameless) { options.frame = false; + } else if (settings.mainWindowFrameless && isMainWindow) { + options.frame = false; } else if (process.platform === "win32" && settings.winNativeTitleBar) { delete options.frame; } diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 6125e7667..cd366010c 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -30,7 +30,7 @@ import { ContextMenuApi, Menu, Toasts, UserStore } from "@webpack/common"; import Plugins, { PluginMeta } from "~plugins"; -import { EquicordDonorModal, VencordDonorModal } from "./modals"; +import { EquicordDonorModal, EquicordTranslatorModal, VencordDonorModal } from "./modals"; const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; const EQUICORD_CONTRIBUTOR_BADGE = "https://equicord.org/assets/favicon.png"; @@ -253,7 +253,7 @@ export default definePlugin({ ContextMenuApi.openContextMenu(event, () => ); }, onClick() { - return EquicordDonorModal(); + return badge.tooltip === "Equicord Translator" ? EquicordTranslatorModal() : EquicordDonorModal(); }, } satisfies ProfileBadge)); } diff --git a/src/plugins/_api/badges/modals.tsx b/src/plugins/_api/badges/modals.tsx index 275023666..135bb54ac 100644 --- a/src/plugins/_api/badges/modals.tsx +++ b/src/plugins/_api/badges/modals.tsx @@ -9,7 +9,7 @@ import { Flex } from "@components/Flex"; import { HeadingPrimary } from "@components/Heading"; import { Heart } from "@components/Heart"; import { Paragraph } from "@components/Paragraph"; -import { DonateButton } from "@components/settings"; +import { DonateButton, TranslateButton } from "@components/settings"; import { Margins } from "@utils/margins"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal"; @@ -72,7 +72,6 @@ export function EquicordDonorModal() { const modalKey = openModal(props => ( { closeModal(modalKey); - // Will get my own in the future VencordNative.native.openExternal("https://github.com/sponsors/thororen1234"); }}> @@ -123,3 +122,47 @@ export function EquicordDonorModal() { )); } + +export function EquicordTranslatorModal() { + const modalKey = openModal(props => ( + { + closeModal(modalKey); + }}> + + + + + Equicord Translator + + + + + + + +
+ + Awarded to contributors who expand Equicordโ€™s language support by translating content for the community. + +
+
+ + + + + +
+
+ )); +} diff --git a/src/plugins/_api/nicknameIcons.ts b/src/plugins/_api/nicknameIcons.ts index 47c7b8905..6d0c220b6 100644 --- a/src/plugins/_api/nicknameIcons.ts +++ b/src/plugins/_api/nicknameIcons.ts @@ -14,10 +14,16 @@ export default definePlugin({ patches: [ { find: "#{intl::USER_PROFILE_PRONOUNS}", - replacement: { - match: /(?<=shouldUnderlineOnHover:null.{0,150})children:(\i)/, - replace: "children:[...Vencord.Api.NicknameIcons._renderIcons({userId:arguments[0].user?.id}),$1]" - } + replacement: [ + { + match: /(?<=,\i\]\}\)\),)null!=\i/, + replace: "($&||Vencord.Api.NicknameIcons._renderIcons({userId:arguments[0].user?.id})?.length)" + }, + { + match: /(?<=shouldUnderlineOnHover:null.{0,300})children:(\i)/, + replace: "children:[...Vencord.Api.NicknameIcons._renderIcons({userId:arguments[0].user?.id}),$1]" + } + ] } ] }); diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index d5a57d933..eb87cb137 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -152,14 +152,6 @@ export default definePlugin({ } ] }, - { - find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}", - replacement: { - // Skip the check Discord performs to make sure the section being selected in the user settings context menu is valid - match: /null!=\(\i=Object.values\(\i\.\i\).{0,50}?&&(?=\(0,\i\.openUserSettings\)\(\i,\{section:\i)/, - replace: "" - } - }, { find: ".buildLayout().map", replacement: { diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts index 2052998ad..3c26219de 100644 --- a/src/plugins/alwaysAnimate/index.ts +++ b/src/plugins/alwaysAnimate/index.ts @@ -92,7 +92,7 @@ export default definePlugin({ find: ".MINI_PREVIEW,[", predicate: () => settings.store.nameplates, replacement: { - match: /animate:\i,loop:.{0,15}===\i/, + match: /animate:\i,loop:(?:!0===\i(?:&&)?){2}/, replace: "animate:true,loop:true" }, }, diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index fa07d51a5..d2566ce36 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -265,7 +265,7 @@ export default definePlugin({ // Create the isBetterFolders and betterFoldersExpandedIds variables in the GuildsBar component // Needed because we access this from a non-arrow closure so we can't use arguments[0] { - match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/, + match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i)/, replace: "$&,isBetterFolders,betterFoldersExpandedIds" }, // Export the isBetterFolders and betterFoldersExpandedIds variable to the Guild List component diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 7b0b3517e..7360c32a3 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -7,29 +7,16 @@ import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { buildPluginMenuEntries, buildThemeMenuEntries } from "@equicordplugins/equicordToolbox/menu"; -import SettingsPlugin from "@plugins/_core/settings"; import { Devs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { findCssClassesLazy } from "@webpack"; import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common"; -import type { HTMLAttributes, ReactElement } from "react"; +import type { HTMLAttributes, ReactNode } from "react"; import fullHeightStyle from "./fullHeightContext.css?managed"; -type SettingsEntry = { - ariaLabel?: string; - element?: any; - label?: string; - newIndicator?: any; - newIndicatorDismissibleContentTypes?: number[]; - predicate?: () => boolean; - searchableTitles?: string[]; - section: string, - tabPredicate?: () => boolean; -}; - const cl = classNameFactory(""); const Classes = findCssClassesLazy("animating", "baseLayer", "bg", "layer", "layers"); @@ -54,11 +41,6 @@ const settings = definePluginSettings({ } }); -interface TransformedSettingsEntry { - section: string; - items: SettingsEntry[]; -} - interface LayerProps extends HTMLAttributes { mode: "SHOWN" | "HIDDEN"; baseLayer?: boolean; @@ -169,15 +151,12 @@ export default definePlugin({ }, { // Settings cog context menu find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}", + predicate: () => settings.store.organizeMenu, replacement: [ { - match: /=\[\];(\i)(?=\.forEach.{0,200}?"logout"===\i.{0,100}?(\i)\.get\(\i\))/, - replace: "=$self.wrapMap([]);$self.transformSettingsEntries($1,$2)", + match: /children:\[(\i),(?<=\1=.{0,30}\.openUserSettings.+?)/, + replace: "children:[$self.transformSettingsEntries($1),", }, - { - match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/, - replace: "$&case 'EquicordPlugins':return $self.buildPluginMenuEntries(true);case 'EquicordThemes':return $self.buildThemeMenuEntries();" - } ] }, ], @@ -200,72 +179,34 @@ export default definePlugin({ return ; }, - transformSettingsEntries(list: SettingsEntry[], keyMap: Map) { - const items: any[] = []; - const equicordItems: SettingsEntry[] = [ - { section: "EquicordSettings", label: "Settings" }, - { section: "EquicordPlugins", label: "Plugins" }, - { section: "EquicordThemes", label: "Themes" }, - { section: "EquicordUpdater", label: "Updater" }, - { section: "EquicordChangelog", label: "Changelog" }, - { section: "EquicordCloud", label: "Cloud" }, - { section: "EquicordBackupAndRestore", label: "Backup & Restore" }, - { section: "EquicordPatchHelper", label: "Patch Helper" }, - ]; + transformSettingsEntries(list) { + const items: ReactNode[] = []; - for (const [section, key] of SettingsPlugin.settingsSectionMap) { - const entry = SettingsPlugin.customEntries.find(e => key.endsWith(e.key)); - if (entry) equicordItems.push({ section, label: entry.title }); - } + for (const item of list) { + const { key, props } = item; + if (!props) continue; - if (settings.store.organizeMenu) { - for (const item of list) { - if (!item.label || item.predicate != null && !item.predicate()) continue; + if (key === "equicord_plugins" || key === "equicord_themes") { + const children = key === "equicord_plugins" + ? buildPluginMenuEntries() + : buildThemeMenuEntries(); - if (item.section === "HEADER") { - keyMap.set(item.label, item.label); - items.push({ section: item.label, items: [] }); - } else if (item.section !== "DIVIDER" && keyMap.has(item.section)) { - items.at(-1)?.items.push(item); - } + items.push( + + {children} + + ); + } else if (key.endsWith("_section") && props.label) { + items.push( + + {this.transformSettingsEntries(props.children)} + + ); + } else { + items.push(item); } - - keyMap.set("Equicord", "Equicord"); - items.push({ - section: "Equicord", - items: equicordItems - }); - } else { - items.push(...list, ...equicordItems); } return items; - }, - - wrapMap(toWrap: TransformedSettingsEntry[]) { - if (!settings.store.organizeMenu) return toWrap; - // @ts-expect-error - toWrap.map = function (render: (item: SettingsEntry) => ReactElement) { - return this - .filter(a => a.items.length > 0) - .map(({ section, items }) => { - const children = items.map(render); - if (section) { - return ( - - {children} - - ); - } else { - return children; - } - }); - }; - - return toWrap; } }); diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 7eb415ca6..ea6a1e610 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -87,7 +87,7 @@ export default definePlugin({ }, // Current user area, at bottom of channels/dm list { - find: ".WIDGETS_RTC_UPSELL_COACHMARK),", + find: ".DISPLAY_NAME_STYLES_COACHMARK),", replacement: [ // Use Decor avatar decoration hook { diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx index 2ebb81f2d..6035f0507 100644 --- a/src/plugins/experiments/index.tsx +++ b/src/plugins/experiments/index.tsx @@ -87,14 +87,6 @@ export default definePlugin({ replace: (_, rest) => `${rest}onClick:()=>{}` } }, - // Make the Favourites Server experiment allow favouriting DMs and threads - { - find: "useCanFavoriteChannel", - replacement: { - match: /(?<=isFavorite\(\i\.id\).{0,5})\i\.isThread\(\)/, - replace: "false", - } - }, // Enable experiment embed on sent experiment links { find: "Clear Treatment ", diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 5a776bd6d..10559321d 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -141,7 +141,7 @@ const settings = definePluginSettings({ description: "Whether to disable the embed permission check when sending fake emojis and stickers", type: OptionType.BOOLEAN, default: false - } + }, }); function hasPermission(channelId: string, permission: bigint) { @@ -376,7 +376,7 @@ export default definePlugin({ match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g, replace: "true" } - } + }, ], get guildId() { diff --git a/src/plugins/noDevtoolsWarning/index.ts b/src/plugins/noDevtoolsWarning/index.ts index e897e5793..5c35fa67b 100644 --- a/src/plugins/noDevtoolsWarning/index.ts +++ b/src/plugins/noDevtoolsWarning/index.ts @@ -26,7 +26,7 @@ export default definePlugin({ patches: [{ find: "setDevtoolsCallbacks", replacement: { - match: /if\(null!=\i&&"0.0.0"===\i\.remoteApp\.getVersion\(\)\)/, + match: /if\(null!=\i&&"0.0.0"===\i\.app\.getVersion\(\)\)/, replace: "if(true)" } }] diff --git a/src/plugins/noMosaic/index.ts b/src/plugins/noMosaic/index.ts index 6ff34a350..4ec64cc39 100644 --- a/src/plugins/noMosaic/index.ts +++ b/src/plugins/noMosaic/index.ts @@ -27,7 +27,7 @@ export default definePlugin({ patches: [ { - find: '=>"IMAGE"===', + find: '"PLAINTEXT_PREVIEW":"OTHER"', replacement: { match: /=>"IMAGE"===\i\|\|"VIDEO"===\i(?:\|\|("VISUAL_PLACEHOLDER"===\i))?;/, replace: (_, visualPlaceholderPred) => visualPlaceholderPred != null ? `=>${visualPlaceholderPred};` : "=>false;" diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e0fef9853..34f87b66b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1263,7 +1263,15 @@ export const EquicordDevs = Object.freeze({ Leon135: { name: "Leon135", id: 309275452231385088n, - } + }, + bbpltergiest: { + name: "bbpltergiest", + id: 279448683672502274n, + }, + mshl: { + name: "m.shl", + id: 1025245410224263258n, + }, } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index e830e52ef..2a1ca4efb 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -112,7 +112,7 @@ waitFor(m => { export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const Timestamp = waitForComponent("Timestamp", filters.componentByCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}")); -export const OAuth2AuthorizeModal = waitForComponent("OAuth2AuthorizeModal", filters.componentByCode("hasContentBackground", "oauth2_authorize")); +export const OAuth2AuthorizeModal = waitForComponent("OAuth2AuthorizeModal", filters.componentByCode("hasContentBackground", "nextStep", "onClose?.()")); export const Animations = mapMangledModuleLazy(".assign({colorNames:", { Transition: filters.componentByCode('["items","children"]', ",null,"),