diff --git a/packages/discord-types/enums/index.ts b/packages/discord-types/enums/index.ts index 0ab498877..3d80895cd 100644 --- a/packages/discord-types/enums/index.ts +++ b/packages/discord-types/enums/index.ts @@ -1,3 +1,4 @@ +export * from "./channel"; export * from "./commands"; export * from "./messages"; -export * from "./channel"; \ No newline at end of file +export * from "./misc"; diff --git a/packages/discord-types/enums/misc.ts b/packages/discord-types/enums/misc.ts new file mode 100644 index 000000000..d1419ba3e --- /dev/null +++ b/packages/discord-types/enums/misc.ts @@ -0,0 +1,4 @@ +export const enum CloudUploadPlatform { + REACT_NATIVE = 0, + WEB = 1, +} diff --git a/packages/discord-types/src/index.d.ts b/packages/discord-types/src/index.d.ts index d302e326f..10a46a020 100644 --- a/packages/discord-types/src/index.d.ts +++ b/packages/discord-types/src/index.d.ts @@ -4,6 +4,7 @@ export * from "./components"; export * from "./flux"; export * from "./fluxEvents"; export * from "./menu"; +export * from "./modules"; export * from "./stores"; export * from "./passiveupdatestate"; export * from "./utils"; diff --git a/packages/discord-types/src/modules/CloudUpload.d.ts b/packages/discord-types/src/modules/CloudUpload.d.ts new file mode 100644 index 000000000..02192ed59 --- /dev/null +++ b/packages/discord-types/src/modules/CloudUpload.d.ts @@ -0,0 +1,74 @@ +import EventEmitter from "events"; +import { CloudUploadPlatform } from "../../enums"; + +interface BaseUploadItem { + platform: CloudUploadPlatform; + id?: string; + origin?: string; + isThumbnail?: boolean; + clip?: unknown; +} + +export interface ReactNativeUploadItem extends BaseUploadItem { + platform: CloudUploadPlatform.REACT_NATIVE; + uri: string; + filename?: string; + mimeType?: string; + durationSecs?: number; + waveform?: string; + isRemix?: boolean; +} + +export interface WebUploadItem extends BaseUploadItem { + platform: CloudUploadPlatform.WEB; + file: File; +} + +export type CloudUploadItem = ReactNativeUploadItem | WebUploadItem; + +export class CloudUpload extends EventEmitter { + constructor(item: CloudUploadItem, channelId: string, showLargeMessageDialog?: boolean, reactNativeFileIndex?: number); + + channelId: string; + classification: string; + clip: unknown; + contentHash: unknown; + currentSize: number; + description: string | null; + durationSecs: number | undefined; + etag: string | undefined; + error: unknown; + filename: string; + id: string; + isImage: boolean; + isRemix: boolean | undefined; + isThumbnail: boolean; + isVideo: boolean; + item: { + file: File; + platform: CloudUploadPlatform; + origin: string; + }; + loaded: number; + mimeType: string; + origin: string; + postCompressionSize: number | undefined; + preCompressionSize: number; + responseUrl: string; + sensitive: boolean; + showLargeMessageDialog: boolean; + spoiler: boolean; + startTime: number; + status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED" | "REMOVED_FROM_MSG_DRAFT"; + uniqueId: string; + uploadedFilename: string; + waveform: string | undefined; + + // there are many more methods than just these but I didn't find them particularly useful + upload(): Promise; + cancel(): void; + delete(): Promise; + getSize(): number; + maybeConvertToWebP(): Promise; + removeFromMsgDraft(): void; +} diff --git a/packages/discord-types/src/modules/index.d.ts b/packages/discord-types/src/modules/index.d.ts new file mode 100644 index 000000000..fe0c1e248 --- /dev/null +++ b/packages/discord-types/src/modules/index.d.ts @@ -0,0 +1 @@ +export * from "./CloudUpload"; diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index b08cb114e..bf4efd2d5 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -17,7 +17,7 @@ */ import { Logger } from "@utils/Logger"; -import type { Channel, CustomEmoji, Message } from "@vencord/discord-types"; +import type { Channel, CloudUpload, CustomEmoji, Message } from "@vencord/discord-types"; import { MessageStore } from "@webpack/common"; import type { Promisable } from "type-fest"; @@ -30,32 +30,6 @@ export interface MessageObject { tts: boolean; } -export interface Upload { - classification: string; - currentSize: number; - description: string | null; - filename: string; - id: string; - isImage: boolean; - isRemix?: boolean; - isThumbnail: boolean; - isVideo: boolean; - item: { - file: File; - platform: number; - }; - loaded: number; - mimeType: string; - preCompressionSize: number; - responseUrl: string; - sensitive: boolean; - showLargeMessageDialog: boolean; - spoiler: boolean; - status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED"; - uniqueId: string; - uploadedFilename: string; -} - export interface MessageReplyOptions { messageReference: Message["messageReference"]; allowedMentions?: { @@ -68,7 +42,7 @@ export interface MessageReplyOptions { export interface MessageOptions { stickers?: string[]; - uploads?: Upload[]; + uploads?: CloudUpload[]; replyOptions: MessageReplyOptions; content: string; channel: Channel; diff --git a/src/api/Notices.ts b/src/api/Notices.tsx similarity index 73% rename from src/api/Notices.ts rename to src/api/Notices.tsx index cd5d58767..5285ca148 100644 --- a/src/api/Notices.ts +++ b/src/api/Notices.tsx @@ -16,7 +16,10 @@ * along with this program. If not, see . */ +import ErrorBoundary from "@components/ErrorBoundary"; +import { isPrimitiveReactNode } from "@utils/react"; import { waitFor } from "@webpack"; +import { ReactNode } from "react"; let NoticesModule: any; waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m); @@ -36,7 +39,11 @@ export function nextNotice() { } } -export function showNotice(message: string, buttonText: string, onOkClick: () => void) { - noticesQueue.push(["GENERIC", message, buttonText, onOkClick]); +export function showNotice(message: ReactNode, buttonText: string, onOkClick: () => void) { + const notice = isPrimitiveReactNode(message) + ? message + : "Error Showing Notice"}>{message}; + + noticesQueue.push(["GENERIC", notice, buttonText, onOkClick]); if (!currentNotice) nextNotice(); } diff --git a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx index 1c4a2d0a9..0b842d990 100644 --- a/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx +++ b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx @@ -21,7 +21,7 @@ import "./spotifyStyles.css"; import { Settings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; -import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; +import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; import { debounce } from "@shared/debounce"; import { openImageModal } from "@utils/discord"; import { classes, copyWithToast } from "@utils/misc"; @@ -76,27 +76,28 @@ function Button(props: React.ButtonHTMLAttributes) { ); } -function CopyContextMenu({ name, path }: { name: string; path: string; }) { - const copyId = `spotify-copy-${name}`; - const openId = `spotify-open-${name}`; - +function CopyContextMenu({ name, type, path }: { type: string; name: string; path: string; }) { return ( FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label={`Spotify ${name} Menu`} + navId="vc-spotify-menu" + onClose={ContextMenuApi.closeContextMenu} + aria-label={`Spotify ${type} Menu`} > copyWithToast(name)} + icon={CopyIcon} + /> + copyWithToast("https://open.spotify.com" + path)} icon={LinkIcon} /> SpotifyStore.openExternal(path)} icon={OpenExternalIcon} /> @@ -104,11 +105,6 @@ function CopyContextMenu({ name, path }: { name: string; path: string; }) { ); } -function makeContextMenu(name: string, path: string) { - return (e: React.MouseEvent) => - ContextMenuApi.openContextMenu(e, () => ); -} - function Controls() { const [isPlaying, shuffle, repeat] = useStateFromStores( [SpotifyStore], @@ -259,13 +255,14 @@ function AlbumContextMenu({ track }: { track: Track; }) { ); } -function makeLinkProps(name: string, condition: unknown, path: string) { +function makeLinkProps(type: "Song" | "Artist" | "Album", condition: unknown, name: string, path: string) { if (!condition) return {}; return { role: "link", onClick: () => SpotifyStore.openExternal(path), - onContextMenu: makeContextMenu(name, path) + onContextMenu: e => + ContextMenuApi.openContextMenu(e, () => ) } satisfies React.HTMLAttributes; } @@ -306,7 +303,7 @@ function Info({ track }: { track: Track; }) { id={cl("song-title")} className={cl("ellipoverflow")} title={track.name} - {...makeLinkProps("Song", track.id, `/track/${track.id}`)} + {...makeLinkProps("Song", track.id, track.name, `/track/${track.id}`)} > {track.name} @@ -319,7 +316,7 @@ function Info({ track }: { track: Track; }) { className={cl("artist")} style={{ fontSize: "inherit" }} title={a.name} - {...makeLinkProps("Artist", a.id, `/artist/${a.id}`)} + {...makeLinkProps("Artist", a.id, a.name, `/artist/${a.id}`)} > {a.name} @@ -336,7 +333,7 @@ function Info({ track }: { track: Track; }) { className={cl("album")} style={{ fontSize: "inherit" }} title={track.album.name} - {...makeLinkProps("Album", track.album.id, `/album/${track.album.id}`)} + {...makeLinkProps("Album", track.album.id, track.album.name, `/album/${track.album.id}`)} > {track.album.name} diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index fae1a2e11..ef69da95a 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -17,12 +17,12 @@ */ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { Upload } from "@api/MessageEvents"; import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { reverseExtensionMap } from "@equicordplugins/fixFileExtensions"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +import { CloudUpload } from "@vencord/discord-types"; import { findByCodeLazy } from "@webpack"; import { useState } from "@webpack/common"; @@ -96,7 +96,7 @@ export default definePlugin({ }, ], - AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: Upload; }) => { + AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: CloudUpload; }) => { const [anonymise, setAnonymise] = useState(upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault); function onToggleAnonymise() { @@ -117,8 +117,7 @@ export default definePlugin({ ); }, { noop: true }), - anonymise(upload: Upload) { - + anonymise(upload: CloudUpload) { const originalFileName = upload.filename; const tarMatch = tarExtMatcher.exec(originalFileName); const extIdx = tarMatch?.index ?? originalFileName.lastIndexOf("."); diff --git a/src/plugins/clearURLs/README.md b/src/plugins/clearURLs/README.md new file mode 100644 index 000000000..43a453d1b --- /dev/null +++ b/src/plugins/clearURLs/README.md @@ -0,0 +1,11 @@ +# ClearURLs + +Automatically removes tracking elements from URLs you send. + +Uses data from the [ClearURLs browser extension](https://clearurls.xyz/). + +## Example + +**Before:** `https://www.amazon.com/dp/exampleProduct/ref=sxin_0_pb?__mk_de_DE=ÅMÅŽÕÑ&keywords=tea&pd_rd_i=exampleProduct&pd_rd_r=8d39e4cd-1e4f-43db-b6e7-72e969a84aa5&pd_rd_w=1pcKM&pd_rd_wg=hYrNl&pf_rd_p=50bbfd25-5ef7-41a2-68d6-74d854b30e30&pf_rd_r=0GMWD0YYKA7XFGX55ADP&qid=1517757263&rnid=2914120011` + +**After:** `https://www.amazon.com/dp/exampleProduct/` diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts deleted file mode 100644 index 7bbb3d685..000000000 --- a/src/plugins/clearURLs/defaultRules.ts +++ /dev/null @@ -1,162 +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 . -*/ - -export const defaultRules = [ - "action_object_map", - "action_type_map", - "action_ref_map", - "spm@*.aliexpress.com", - "scm@*.aliexpress.com", - "aff_platform", - "aff_trace_key", - "algo_expid@*.aliexpress.*", - "algo_pvid@*.aliexpress.*", - "btsid", - "ws_ab_test", - "pd_rd_*@amazon.*", - "_encoding@amazon.*", - "psc@amazon.*", - "tag@amazon.*", - "ref_@amazon.*", - "pf_rd_*@amazon.*", - "pf@amazon.*", - "crid@amazon.*", - "keywords@amazon.*", - "sprefix@amazon.*", - "sr@amazon.*", - "ie@amazon.*", - "node@amazon.*", - "qid@amazon.*", - "callback@bilibili.com", - "cvid@bing.com", - "form@bing.com", - "sk@bing.com", - "sp@bing.com", - "sc@bing.com", - "qs@bing.com", - "pq@bing.com", - "sc_cid", - "mkt_tok", - "trk", - "trkCampaign", - "ga_*", - "gclid", - "gclsrc", - "hmb_campaign", - "hmb_medium", - "hmb_source", - "spReportId", - "spJobID", - "spUserID", - "spMailingID", - "itm_*", - "s_cid", - "elqTrackId", - "elqTrack", - "assetType", - "assetId", - "recipientId", - "campaignId", - "siteId", - "mc_cid", - "mc_eid", - "pk_*", - "sc_campaign", - "sc_channel", - "sc_content", - "sc_medium", - "sc_outcome", - "sc_geo", - "sc_country", - "nr_email_referer", - "vero_conv", - "vero_id", - "yclid", - "_openstat", - "mbid", - "cmpid", - "cid", - "c_id", - "campaign_id", - "Campaign", - "hash@ebay.*", - "fb_action_ids", - "fb_action_types", - "fb_ref", - "fb_source", - "fbclid", - "refsrc@facebook.com", - "hrc@facebook.com", - "gs_l", - "gs_lcp@google.*", - "ved@google.*", - "ei@google.*", - "sei@google.*", - "gws_rd@google.*", - "gs_gbg@google.*", - "gs_mss@google.*", - "gs_rn@google.*", - "_hsenc", - "_hsmi", - "__hssc", - "__hstc", - "hsCtaTracking", - "source@sourceforge.net", - "position@sourceforge.net", - "t@*.twitter.com", - "s@*.twitter.com", - "ref_*@*.twitter.com", - "t@*.x.com", - "s@*.x.com", - "ref_*@*.x.com", - "t@*.fixupx.com", - "s@*.fixupx.com", - "ref_*@*.fixupx.com", - "t@*.fxtwitter.com", - "s@*.fxtwitter.com", - "ref_*@*.fxtwitter.com", - "t@*.vxtwitter.com", - "s@*.vxtwitter.com", - "ref_*@*.vxtwitter.com", - "t@*.twittpr.com", - "s@*.twittpr.com", - "ref_*@*.twittpr.com", - "t@*.fixvx.com", - "s@*.fixvx.com", - "ref_*@*.fixvx.com", - "tt_medium", - "tt_content", - "lr@yandex.*", - "redircnt@yandex.*", - "feature@*.youtube.com", - "kw@*.youtube.com", - "si@*.youtube.com", - "pp@*.youtube.com", - "si@*.youtu.be", - "wt_zmc", - "utm_source", - "utm_content", - "utm_medium", - "utm_campaign", - "utm_term", - "si@open.spotify.com", - "igshid", - "igsh", - "share_id@reddit.com", - "si@soundcloud.com", -]; diff --git a/src/plugins/clearURLs/index.ts b/src/plugins/clearURLs/index.ts index f00c751d4..362706079 100644 --- a/src/plugins/clearURLs/index.ts +++ b/src/plugins/clearURLs/index.ts @@ -22,77 +22,74 @@ import { import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; -import { defaultRules } from "./defaultRules"; +const CLEAR_URLS_JSON_URL = "https://raw.githubusercontent.com/ClearURLs/Rules/master/data.min.json"; -// From lodash -const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; -const reHasRegExpChar = RegExp(reRegExpChar.source); +interface Provider { + urlPattern: string; + completeProvider: boolean; + rules?: string[]; + rawRules?: string[]; + referralMarketing?: string[]; + exceptions?: string[]; + redirections?: string[]; + forceRedirection?: boolean; +} + +interface ClearUrlsData { + providers: Record; +} + +interface RuleSet { + name: string; + urlPattern: RegExp; + rules?: RegExp[]; + rawRules?: RegExp[]; + exceptions?: RegExp[]; +} export default definePlugin({ name: "ClearURLs", - description: "Removes tracking garbage from URLs", - authors: [Devs.adryd], + description: "Automatically removes tracking elements from URLs you send", + authors: [Devs.adryd, Devs.thororen], - start() { - this.createRules(); + rules: [] as RuleSet[], + + async start() { + await this.createRules(); + }, + + stop() { + this.rules = []; }, onBeforeMessageSend(_, msg) { - return this.onSend(msg); + return this.cleanMessage(msg); }, onBeforeMessageEdit(_cid, _mid, msg) { - return this.onSend(msg); + return this.cleanMessage(msg); }, - escapeRegExp(str: string) { - return (str && reHasRegExpChar.test(str)) - ? str.replace(reRegExpChar, "\\$&") - : (str || ""); - }, + async createRules() { + const res = await fetch(CLEAR_URLS_JSON_URL) + .then(res => res.json()) as ClearUrlsData; - createRules() { - // Can be extended upon once user configs are available - // Eg. (useDefaultRules: boolean, customRules: Array[string]) - const rules = defaultRules; + this.rules = []; - this.universalRules = new Set(); - this.rulesByHost = new Map(); - this.hostRules = new Map(); + for (const [name, provider] of Object.entries(res.providers)) { + const urlPattern = new RegExp(provider.urlPattern, "i"); - for (const rule of rules) { - const splitRule = rule.split("@"); - const paramRule = new RegExp( - "^" + - this.escapeRegExp(splitRule[0]).replace(/\\\*/, ".+?") + - "$" - ); + const rules = provider.rules?.map(rule => new RegExp(rule, "i")); + const rawRules = provider.rawRules?.map(rule => new RegExp(rule, "i")); + const exceptions = provider.exceptions?.map(ex => new RegExp(ex, "i")); - if (!splitRule[1]) { - this.universalRules.add(paramRule); - continue; - } - const hostRule = new RegExp( - "^(www\\.)?" + - this.escapeRegExp(splitRule[1]) - .replace(/\\\./, "\\.") - .replace(/^\\\*\\\./, "(.+?\\.)?") - .replace(/\\\*/, ".+?") + - "$" - ); - const hostRuleIndex = hostRule.toString(); - - this.hostRules.set(hostRuleIndex, hostRule); - if (this.rulesByHost.get(hostRuleIndex) == null) { - this.rulesByHost.set(hostRuleIndex, new Set()); - } - this.rulesByHost.get(hostRuleIndex).add(paramRule); - } - }, - - removeParam(rule: string | RegExp, param: string, parent: URLSearchParams) { - if (param === rule || rule instanceof RegExp && rule.test(param)) { - parent.delete(param); + this.rules.push({ + name, + urlPattern, + rules, + rawRules, + exceptions, + }); } }, @@ -106,34 +103,40 @@ export default definePlugin({ } // Cheap way to check if there are any search params - if (url.searchParams.entries().next().done) { - // If there are none, we don't need to modify anything - return match; - } + if (url.searchParams.entries().next().done) return match; - // Check all universal rules - this.universalRules.forEach(rule => { - url.searchParams.forEach((_value, param, parent) => { - this.removeParam(rule, param, parent); - }); - }); + // Check rules for each provider that matches + this.rules.forEach(({ urlPattern, exceptions, rawRules, rules }) => { + if (!urlPattern.test(url.href) || exceptions?.some(ex => ex.test(url.href))) return; - // Check rules for each hosts that match - this.hostRules.forEach((regex, hostRuleName) => { - if (!regex.test(url.hostname)) return; - this.rulesByHost.get(hostRuleName).forEach(rule => { - url.searchParams.forEach((_value, param, parent) => { - this.removeParam(rule, param, parent); + const toDelete: string[] = []; + + if (rules) { + // Add matched params to delete list + url.searchParams.forEach((_, param) => { + if (rules.some(rule => rule.test(param))) { + toDelete.push(param); + } }); + } + + // Delete matched params from list + toDelete.forEach(param => url.searchParams.delete(param)); + + // Match and remove any raw rules + let cleanedUrl = url.href; + rawRules?.forEach(rawRule => { + cleanedUrl = cleanedUrl.replace(rawRule, ""); }); + url = new URL(cleanedUrl); }); return url.toString(); }, - onSend(msg: MessageObject) { + cleanMessage(msg: MessageObject) { // Only run on messages that contain URLs - if (msg.content.match(/http(s)?:\/\//)) { + if (/http(s)?:\/\//.test(msg.content)) { msg.content = msg.content.replace( /(https?:\/\/[^\s<]+[^<.,:;"'>)|\]\s])/g, match => this.replacer(match) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 9d6138994..707cfc1fb 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -182,8 +182,8 @@ function makeBypassPatches(): Omit { export default definePlugin({ name: "FakeNitro", - authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], - description: "Allows you to stream in nitro quality, send fake emojis/stickers, and use client themes.", + authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan], + description: "Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality", dependencies: ["MessageEventsAPI"], settings, @@ -281,6 +281,14 @@ export default definePlugin({ replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});` } }, + // Allow users to use custom client themes + { + find: "customUserThemeSettings:{", + replacement: { + match: /(?<=\i=)\(0,\i\.\i\)\(\i\.\i\.TIER_2\)(?=,|;)/g, + replace: "true" + } + }, { find: '["strong","em","u","text","inlineCode","s","spoiler"]', replacement: [ @@ -392,24 +400,15 @@ export default definePlugin({ if (premiumType !== 2) { proto.appearance ??= AppearanceSettingsActionCreators.create(); - if (UserSettingsProtoStore.settings.appearance?.theme != null) { - const appearanceSettingsDummy = AppearanceSettingsActionCreators.create({ - theme: UserSettingsProtoStore.settings.appearance.theme - }); + const protoStoreAppearenceSettings = UserSettingsProtoStore.settings.appearance; - proto.appearance.theme = appearanceSettingsDummy.theme; - } + const appearanceSettingsOverwrite = AppearanceSettingsActionCreators.create({ + ...proto.appearance, + theme: protoStoreAppearenceSettings?.theme, + clientThemeSettings: protoStoreAppearenceSettings?.clientThemeSettings + }); - if (UserSettingsProtoStore.settings.appearance?.clientThemeSettings?.backgroundGradientPresetId?.value != null) { - const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({ - backgroundGradientPresetId: { - value: UserSettingsProtoStore.settings.appearance.clientThemeSettings.backgroundGradientPresetId.value - } - }); - - proto.appearance.clientThemeSettings ??= clientThemeSettingsDummy; - proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId; - } + proto.appearance = appearanceSettingsOverwrite; } } catch (err) { new Logger("FakeNitro").error(err); diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx index 5afc9598e..ae491beab 100644 --- a/src/plugins/favGifSearch/index.tsx +++ b/src/plugins/favGifSearch/index.tsx @@ -20,13 +20,11 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; import { useCallback, useEffect, useRef, useState } from "@webpack/common"; interface SearchBarComponentProps { - ref?: React.MutableRefObject; + ref?: React.RefObject; autoFocus: boolean; - className: string; size: string; onChange: (query: string) => void; onClear: () => void; @@ -59,9 +57,6 @@ interface Instance { forceUpdate: () => void; } - -const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchBarFullRow"); - export const settings = definePluginSettings({ searchOption: { type: OptionType.SELECT, @@ -136,7 +131,7 @@ export default definePlugin({ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) { const [query, setQuery] = useState(""); - const ref = useRef<{ containerRef?: React.MutableRefObject; } | null>(null); + const ref = useRef<{ containerRef?: React.RefObject; } | null>(null); const onChange = useCallback((searchQuery: string) => { setQuery(searchQuery); @@ -181,7 +176,6 @@ function SearchBar({ instance, SearchBarComponent }: { instance: Instance; Searc { diff --git a/src/plugins/gameActivityToggle/style.css b/src/plugins/gameActivityToggle/style.css index 019ef0ff9..040212ce9 100644 --- a/src/plugins/gameActivityToggle/style.css +++ b/src/plugins/gameActivityToggle/style.css @@ -1,7 +1,4 @@ -[class^="panels"] [class^="avatarWrapper"] { - min-width: 88px; -} - -.vc-game-activity { - margin-right: 4px; -} +/* make gap smaller since we're adding an extra button */ +[class^="panels"] [class^="buttons"] { + gap: 4px; +} \ No newline at end of file diff --git a/src/plugins/lastfmRichPresence/index.tsx b/src/plugins/lastfmRichPresence/index.tsx index 71619d006..a6de8b188 100644 --- a/src/plugins/lastfmRichPresence/index.tsx +++ b/src/plugins/lastfmRichPresence/index.tsx @@ -48,6 +48,7 @@ interface Activity { buttons?: Array; name: string; application_id: string; + status_display_type?: number; metadata?: { button_urls?: Array; }; @@ -137,6 +138,25 @@ const settings = definePluginSettings({ type: OptionType.STRING, default: "some music", }, + statusDisplayType: { + description: "Show the track / artist name in the member list", + type: OptionType.SELECT, + options: [ + { + label: "Don't show (shows generic listening message)", + value: "off", + default: true + }, + { + label: "Show artist name", + value: "artist" + }, + { + label: "Show track name", + value: "track" + } + ] + }, nameFormat: { description: "Show name of song and artist in status name", type: OptionType.SELECT, @@ -354,6 +374,11 @@ export default definePlugin({ details: trackData.name, state: trackData.artist, + status_display_type: { + "off": 0, + "artist": 1, + "track": 2 + }[settings.store.statusDisplayType], assets, buttons: buttons.length ? buttons.map(v => v.label) : undefined, diff --git a/src/plugins/memberCount/CircleIcon.tsx b/src/plugins/memberCount/CircleIcon.tsx new file mode 100644 index 000000000..bf6604a43 --- /dev/null +++ b/src/plugins/memberCount/CircleIcon.tsx @@ -0,0 +1,17 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function CircleIcon({ className }: { className?: string; }) { + return ( + + + + ); +} diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx index 020338861..6887af51a 100644 --- a/src/plugins/memberCount/MemberCount.tsx +++ b/src/plugins/memberCount/MemberCount.tsx @@ -9,6 +9,7 @@ import { isObjectEmpty } from "@utils/misc"; import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common"; import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from "."; +import { CircleIcon } from "./CircleIcon"; import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; import { VoiceIcon } from "./VoiceIcon"; @@ -80,7 +81,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t {props => (
- + {formattedOnlineCount}
)} @@ -88,7 +89,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t {props => (
- + {numberFormat(totalCount)}
)} diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index bb48eedbf..e2d78e2b0 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -36,7 +36,6 @@ export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as F getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; }; }; - export const settings = definePluginSettings({ toolTip: { type: OptionType.BOOLEAN, diff --git a/src/plugins/memberCount/style.css b/src/plugins/memberCount/style.css index 2b0e4400f..009e7d10c 100644 --- a/src/plugins/memberCount/style.css +++ b/src/plugins/memberCount/style.css @@ -18,6 +18,7 @@ flex-wrap: wrap; margin-top: 1em; padding-inline: 1em; + line-height: 1.2em; } .vc-membercount-container { @@ -38,20 +39,18 @@ color: var(--color-voice); } -.vc-membercount-online-dot { - background-color: var(--color-online); - display: inline-block; - width: 12px; - height: 12px; - border-radius: 50%; +.vc-membercount-online-count { + fill: var(--status-online); + width: 18px; + height: 18px; } -.vc-membercount-total-dot { - display: inline-block; - width: 6px; - height: 6px; - border-radius: 50%; - border: 3px solid var(--color-total); +.vc-membercount-total-count { + fill: none; + stroke: var(--status-offline); + stroke-width: 4px; + width: 15px; + height: 15px; } .vc-membercount-voice-icon { diff --git a/src/plugins/noPendingCount/index.ts b/src/plugins/noPendingCount/index.ts index febd63f38..b1a7ffad8 100644 --- a/src/plugins/noPendingCount/index.ts +++ b/src/plugins/noPendingCount/index.ts @@ -87,8 +87,8 @@ export default definePlugin({ replacement: { // The two groups inside the first group grab the minified names of the variables, // they are then referenced later to find unviewedTrialCount + unviewedDiscountCount. - match: /(?<=\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.{0,300}\.NONE\?)\1\+\2/, - replace: "0" + match: /(\{unviewedTrialCount:(\i),unviewedDiscountCount:(\i)\}.+?)\2\+\3/, + replace: (_, rest) => `${rest}0` } } ], diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index 1f64c7149..6bc9d25f0 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -20,7 +20,7 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; import { Devs } from "@utils/constants"; import definePlugin, { StartAt } from "@utils/types"; -import { MessageAttachment } from "@vencord/discord-types"; +import { CloudUpload, MessageAttachment } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common"; @@ -45,7 +45,7 @@ const getImageBox = (url: string): Promise<{ width: number, height: number; } | const getAttachments = async (channelId: string) => await Promise.all( UploadStore.getUploads(channelId, DraftType.ChannelMessage) - .map(async (upload: any) => { + .map(async (upload: CloudUpload) => { const { isImage, filename, spoiler, item: { file } } = upload; const url = URL.createObjectURL(file); const attachment: MessageAttachment = { @@ -53,7 +53,7 @@ const getAttachments = async (channelId: string) => filename: spoiler ? "SPOILER_" + filename : filename, // weird eh? if i give it the normal content type the preview doenst work content_type: undefined, - size: await upload.getSize(), + size: upload.getSize(), spoiler, // discord adds query params to the url, so we need to add a hash to prevent that url: url + "#", diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 48ea1d6ee..3539ac79f 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -27,6 +27,8 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa import { useAwaiter } from "@utils/react"; import definePlugin from "@utils/types"; import { chooseFile } from "@utils/web"; +import { CloudUpload as TCloudUpload } from "@vencord/discord-types"; +import { CloudUploadPlatform } from "@vencord/discord-types/enums"; import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { ComponentType } from "react"; @@ -38,7 +40,7 @@ import { cl } from "./utils"; import { VoicePreview } from "./VoicePreview"; import { VoiceRecorderWeb } from "./WebRecorder"; -const CloudUpload = findLazy(m => m.prototype?.trackUploadFinished); +const CloudUpload: typeof TCloudUpload = findLazy(m => m.prototype?.trackUploadFinished); const PendingReplyStore = findStoreLazy("PendingReplyStore"); const OptionClasses = findByPropsLazy("optionName", "optionIcon", "optionLabel"); @@ -93,8 +95,8 @@ function sendAudio(blob: Blob, meta: AudioMetadata) { const upload = new CloudUpload({ file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }), isThumbnail: false, - platform: 1, - }, channelId, false, 0); + platform: CloudUploadPlatform.WEB, + }, channelId); upload.on("complete", () => { RestAPI.post({ diff --git a/src/plugins/volumeBooster/index.ts b/src/plugins/volumeBooster/index.ts index 664a59ddb..81dcf6633 100644 --- a/src/plugins/volumeBooster/index.ts +++ b/src/plugins/volumeBooster/index.ts @@ -24,7 +24,7 @@ const settings = definePluginSettings({ multiplier: { description: "Volume Multiplier", type: OptionType.SLIDER, - markers: makeRange(1, 5, 1), + markers: makeRange(1, 5, 0.5), default: 2, stickToMarkers: true, } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9cf67baae..81425d7d1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -621,6 +621,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Cootshk", id: 921605971577548820n }, + thororen: { + name: "thororen", + id: 848339671629299742n + }, } satisfies Record); export const EquicordDevs = Object.freeze({ diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index f236943b1..837136256 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -17,7 +17,7 @@ */ import { MessageObject } from "@api/MessageEvents"; -import { Channel, Guild, GuildFeatures, Message, User } from "@vencord/discord-types"; +import { Channel, CloudUpload, Guild, GuildFeatures, Message, User } from "@vencord/discord-types"; import { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, IconUtils, InviteActions, MessageActions, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; import { Except } from "type-fest"; @@ -117,6 +117,20 @@ interface MessageOptions { replied_user: boolean; }; stickerIds: string[]; + attachmentsToUpload: CloudUpload[]; + poll: { + allow_multiselect: boolean; + answers: Array<{ + poll_media: { + text: string; + attachment_ids?: unknown; + emoji?: { name: string; id?: string; }; + }; + }>; + duration: number; + layout_type: number; + question: { text: string; }; + }; } export function sendMessage( diff --git a/src/utils/react.tsx b/src/utils/react.tsx index 04cbd95a4..e88c463fd 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -17,7 +17,7 @@ */ import { React, useEffect, useMemo, useReducer, useState } from "@webpack/common"; -import { ActionDispatch } from "react"; +import { ActionDispatch, ReactNode } from "react"; import { checkIntersecting } from "./misc"; @@ -25,6 +25,14 @@ export * from "./lazyReact"; export const NoopComponent = () => null; +/** + * Check if a React node is a primitive (string, number, bigint, boolean, undefined) + */ +export function isPrimitiveReactNode(node: ReactNode): boolean { + const t = typeof node; + return t === "string" || t === "number" || t === "bigint" || t === "boolean" || t === "undefined"; +} + /** * Check if an element is on screen * @param intersectOnly If `true`, will only update the state when the element comes into view diff --git a/src/utils/text.ts b/src/utils/text.ts index 7f0c8c4b8..0203a85dc 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -21,7 +21,8 @@ import { moment } from "@webpack/common"; // Utils for readable text transformations eg: `toTitle(fromKebab())` // Case style to words -export const wordsFromCamel = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase()); +export const wordsFromCamel = (text: string) => + text.split(/(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])/).map(w => /^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase()); export const wordsFromSnake = (text: string) => text.toLowerCase().split("_"); export const wordsFromKebab = (text: string) => text.toLowerCase().split("-"); export const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase()); diff --git a/src/webpack/common/menu.ts b/src/webpack/common/menu.ts index fc2e12b44..7e5447ce8 100644 --- a/src/webpack/common/menu.ts +++ b/src/webpack/common/menu.ts @@ -42,7 +42,7 @@ waitFor(m => m.name === "MenuCheckboxItem", (_, id) => { waitFor(filters.componentByCode('path:["empty"]'), m => Menu.Menu = m); waitFor(filters.componentByCode("sliderContainer", "slider", "handleSize:16", "=100"), m => Menu.MenuSliderControl = m); -waitFor(filters.componentByCode(".useImperativeHandle(", ".SEARCH)", "query:"), m => Menu.MenuSearchControl = m); +waitFor(filters.componentByCode(".SEARCH)", ".focus()", "query:"), m => Menu.MenuSearchControl = m); export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', { closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),