diff --git a/README.md b/README.md index 87e80cb03..64d05aaab 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch ### Extra included plugins
-175 additional plugins +184 additional plugins ### All Platforms @@ -41,6 +41,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - CommandPalette by Ethan - CopyStickerLinks by Byeoon - CopyUserMention by Cortex & castdrian +- CustomFolderIcons by sadan - CustomSounds by TheKodeToad & SpikeHD - CustomTimestamps by Rini & nvhrr - CustomUserColors by mochienya @@ -105,13 +106,17 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - Meow by Samwich - MessageBurst by port - MessageColors by Hen +- MessageFetchTimer by GroupXyz - MessageLinkTooltip by Kyuuhachi - MessageLoggerEnhanced by Aria - MessageTranslate by Samwich - ModalFade by Kyuuhachi +- MoreCommands by Arjix, echo, Samu +- MoreKaomoji by JacobTm & voidbbg - MoreStickers by Leko & Arjix - MoreUserTags by Cyn, TheSun, RyanCaoDev, LordElias, AutumnVN, hen - Morse by zyqunix +- Moyai by Megu & Nuckyz - NeverPausePreviews by vappstar - NewPluginsManager by Sqaaakoi - NoAppsAllowed by kvba @@ -123,6 +128,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - NoOnboarding by omaw & Glitch - NoRoleHeaders by Samwich - NotificationTitle by Kyuuhachi +- PartyMode by UwUDev - PingNotifications by smuki - PinIcon by iamme - PlatformSpoofer by Drag @@ -154,6 +160,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - StatusPresets by iamme - SteamStatusSync by niko - StickerBlocker by Samwich +- StreamingCodecDisabler by davidkra230 - TalkInReverse by Tolgchu - TeX by Kyuuhachi - TextToSpeech by Samwich @@ -198,6 +205,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - ClipsEnhancements by niko - MediaDownloader by Colorman +- NoRPC by Cyn - StatusWhilePlaying by thororen ### Equicord Devbuilds Only diff --git a/src/api/MessageAccessories.tsx b/src/api/MessageAccessories.tsx index 71664e93a..d2bc081e7 100644 --- a/src/api/MessageAccessories.tsx +++ b/src/api/MessageAccessories.tsx @@ -48,7 +48,7 @@ export function _modifyAccessories( ) { for (const [key, accessory] of accessories.entries()) { const res = ( - + ); diff --git a/src/api/Settings.ts b/src/api/Settings.ts index e4dc21c50..adc154015 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -236,7 +236,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } -export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) { +export function migratePluginSetting(pluginName: string, newSetting: string, oldSetting: string) { const settings = SettingsStore.plain.plugins[pluginName]; if (!settings) return; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 0ca204409..7058b5fd4 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => { logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); } + get isNoop() { + if (IS_DEV) return false; + return this.props.noop; + } + render() { if (this.state.error === NO_ERROR) return this.props.children; - if (this.props.noop) return null; + if (this.isNoop) return null; if (this.props.fallback) return ( diff --git a/src/components/ErrorCard.css b/src/components/ErrorCard.css index 5146aa03f..6401c59c6 100644 --- a/src/components/ErrorCard.css +++ b/src/components/ErrorCard.css @@ -4,4 +4,8 @@ border: 1px solid #e78284; border-radius: 5px; color: var(--text-normal, white); + + & a:hover { + text-decoration: underline; + } } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0f4eb07db..2eb7ab00f 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren) { props.style.pointerEvents = "none"; props["aria-disabled"] = true; } + + props.rel ??= "noreferrer"; + return ( {props.children} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 69a2273f1..39c1f18c5 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti {!!plugin.settingsAboutComponent && (
- + diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 7c2d31074..1bfaee37d 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -244,7 +244,7 @@ export default function PluginSettings() { })); }, []); - const depMap = React.useMemo(() => { + const depMap = useMemo(() => { const o = {} as Record; for (const plugin in Plugins) { const deps = Plugins[plugin].dependencies; diff --git a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx index 9e4b7893e..16a1bc463 100644 --- a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx +++ b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx @@ -16,8 +16,8 @@ import { settings } from "../settings"; import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types"; import { cl, getApplicationIcons } from "../utils"; -// if discord one day decides to change their icon this needs to be updated -const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z"); +// Discord no longer shows an icon here by default but we use the one from the popout now here +const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"); export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null { const icons: ActivityListIcon[] = []; @@ -86,7 +86,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis // We need to filter out custom statuses const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length; if (shouldShow) { - return ; + return ; } } diff --git a/src/equicordplugins/customFolderIcons/components.tsx b/src/equicordplugins/customFolderIcons/components.tsx new file mode 100644 index 000000000..b5feb52a7 --- /dev/null +++ b/src/equicordplugins/customFolderIcons/components.tsx @@ -0,0 +1,124 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 sadan + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { closeModal, ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; +import { Button, Menu, Slider, TextInput, useState } from "@webpack/common"; + +import { folderIconsData, settings } from "./settings"; +import { folderProp, int2rgba, setFolderData } from "./util"; + +export function ImageModal(folderProps: folderProp) { + const [data, setData] = useState(((settings.store.folderIcons ?? {}) as folderIconsData)[folderProps.folderId]?.url ?? ""); + const [size, setSize] = useState(100); + return ( + <> + { + setData(val); + }} + placeholder="https://example.com/image.png" + > + + + {data && <> +
Change the size of the folder icon
+ { + setSize(v); + }} + maxValue={200} + minValue={25} + // [25, 200] + markers={Array.apply(0, Array(176)).map((_, i) => i + 25)} + stickToMarkers={true} + keyboardStep={1} + renderMarker={() => null} /> + } + +
+ +
+ + ); +} +export function RenderPreview({ folderProps, url, size }: { folderProps: folderProp; url: string; size: number; }) { + if (!url) return null; + return ( +
+ +
+ ); +} + +export function makeContextItem(a: folderProp) { + return ( + { + openModalLazy(async () => { + return props => ( + + +
+ Set a New Icon. +
+
+ + + +
+ You might have to hover the folder after setting in order for it to refresh. +
+
+ ); + }, + { + modalKey: "custom-folder-icon" + }); + }} /> + ); +} diff --git a/src/equicordplugins/customFolderIcons/index.tsx b/src/equicordplugins/customFolderIcons/index.tsx new file mode 100644 index 000000000..5da725330 --- /dev/null +++ b/src/equicordplugins/customFolderIcons/index.tsx @@ -0,0 +1,58 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 sadan + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +import { makeContextItem } from "./components"; +import { folderIconsData, settings } from "./settings"; +import { folderProp, int2rgba } from "./util"; + +export default definePlugin({ + name: "CustomFolderIcons", + description: "Customize folder icons with any png", + authors: [EquicordDevs.sadan], + settings, + patches: [ + { + find: ".folderButtonInner", + replacement: { + match: /(\(0,\i\.jsx\)\(\i,\{folderNode:(\i),hovered:\i,sorting:\i\}\))/, + replace: "($self.shouldReplace({folderNode:$2})?$self.replace({folderNode:$2}):$1)" + } + }, + ], + contextMenus: { + "guild-context": (menuItems, props: folderProp) => { + if (!("folderId" in props)) return; + menuItems.push(makeContextItem(props)); + } + }, + shouldReplace(props: any): boolean { + return !!((settings.store.folderIcons as folderIconsData)?.[props.folderNode.id]?.url); + }, + replace(props: any) { + const folderSettings = (settings.store.folderIcons as folderIconsData); + if (folderSettings && folderSettings[props.folderNode.id]) { + const data = folderSettings[props.folderNode.id]; + return ( +
+ +
+ ); + } + } +}); diff --git a/src/equicordplugins/customFolderIcons/settings.tsx b/src/equicordplugins/customFolderIcons/settings.tsx new file mode 100644 index 000000000..83c98e76d --- /dev/null +++ b/src/equicordplugins/customFolderIcons/settings.tsx @@ -0,0 +1,28 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 sadan + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +export interface folderIcon { + url: string, + size: number, +} +export type folderIconsData = Record; + +export const settings = definePluginSettings({ + solidIcon: { + type: OptionType.BOOLEAN, + default: false, + description: "Use a solid background on the background of the image" + }, + folderIcons: { + type: OptionType.COMPONENT, + hidden: true, + description: "folder icon settings", + component: () => <> + } +}); diff --git a/src/equicordplugins/customFolderIcons/util.tsx b/src/equicordplugins/customFolderIcons/util.tsx new file mode 100644 index 000000000..959b35fbb --- /dev/null +++ b/src/equicordplugins/customFolderIcons/util.tsx @@ -0,0 +1,31 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 sadan + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { folderIcon, folderIconsData, settings } from "./settings"; + +export async function setFolderData(props: folderProp, newData: folderIcon) { + if (!settings.store.folderIcons) { + settings.store.folderIcons = {}; + } + const folderSettings = (settings.store.folderIcons as folderIconsData); + folderSettings[props.folderId] = newData; +} + +export interface folderProp { + folderId: string; + folderColor: number; +} + +/** + * @param rgbVal RGB value + * @param alpha alpha bewteen zero and 1 +*/ +export function int2rgba(rgbVal: number, alpha: number = 1) { + const b = rgbVal & 0xFF, + g = (rgbVal & 0xFF00) >>> 8, + r = (rgbVal & 0xFF0000) >>> 16; + return `rgba(${[r, g, b].join(",")},${alpha})`; +} diff --git a/src/equicordplugins/equicordToolbox/index.tsx b/src/equicordplugins/equicordToolbox/index.tsx index 9326c1897..598764928 100644 --- a/src/equicordplugins/equicordToolbox/index.tsx +++ b/src/equicordplugins/equicordToolbox/index.tsx @@ -147,7 +147,7 @@ function VencordPopoutButton() { function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) { children.splice( children.length - 1, 0, - + ); diff --git a/src/equicordplugins/moreUserTags/consts.ts b/src/equicordplugins/expandedUserTags/consts.ts similarity index 89% rename from src/equicordplugins/moreUserTags/consts.ts rename to src/equicordplugins/expandedUserTags/consts.ts index 14ea3dd9e..8d820637a 100644 --- a/src/equicordplugins/moreUserTags/consts.ts +++ b/src/equicordplugins/expandedUserTags/consts.ts @@ -9,9 +9,14 @@ import { GuildStore } from "@webpack/common"; import { RC } from "@webpack/types"; import { Channel, Guild, Message, User } from "discord-types/general"; +import { settings } from "./settings"; import type { ITag } from "./types"; -export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot(); +export const isWebhook = (message: Message, user: User) => { + const isFollowed = message?.type === 0 && !!message?.messageReference && !settings.store.showWebhookTagFully; + return !!message?.webhookId && user.isNonUserBot() && !isFollowed; +}; + export const tags = [ { name: "WEBHOOK", diff --git a/src/equicordplugins/moreUserTags/index.tsx b/src/equicordplugins/expandedUserTags/index.tsx similarity index 91% rename from src/equicordplugins/moreUserTags/index.tsx rename to src/equicordplugins/expandedUserTags/index.tsx index b45dac049..3948b4a1a 100644 --- a/src/equicordplugins/moreUserTags/index.tsx +++ b/src/equicordplugins/expandedUserTags/index.tsx @@ -6,6 +6,7 @@ import "./styles.css"; +import { migratePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs, EquicordDevs } from "@utils/constants"; import { getCurrentChannel, getIntlMessage } from "@utils/discord"; @@ -31,11 +32,13 @@ const genTagTypes = () => { return obj; }; +migratePluginSettings("ExpandedUserTags", "MoreUserTags"); export default definePlugin({ - name: "MoreUserTags", + name: "ExpandedUserTags", description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen], dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"], + tags: ["MoreUserTags"], settings, patches: [ // Make discord actually use our tags @@ -73,7 +76,7 @@ export default definePlugin({ renderNicknameIcon(props) { const tagId = this.getTag({ user: UserStore.getUser(props.userId), - channel: ChannelStore.getChannel(this.getChannelId()), + channel: getCurrentChannel(), channelId: this.getChannelId(), isChat: false }); @@ -89,7 +92,7 @@ export default definePlugin({ message: props.message, user: UserStore.getUser(props.message.author.id), channelId: props.message.channel_id, - isChat: false + isChat: true }); return tagId && @@ -96,9 +96,14 @@ export const settings = definePluginSettings({ default: false, restartNeeded: true }, + showWebhookTagFully: { + description: "Show Webhook tag in followed channels like announcements", + type: OptionType.BOOLEAN, + default: false + }, tagSettings: { type: OptionType.COMPONENT, component: SettingsComponent, description: "fill me" - } + }, }); diff --git a/src/equicordplugins/moreUserTags/styles.css b/src/equicordplugins/expandedUserTags/styles.css similarity index 100% rename from src/equicordplugins/moreUserTags/styles.css rename to src/equicordplugins/expandedUserTags/styles.css diff --git a/src/equicordplugins/moreUserTags/types.ts b/src/equicordplugins/expandedUserTags/types.ts similarity index 100% rename from src/equicordplugins/moreUserTags/types.ts rename to src/equicordplugins/expandedUserTags/types.ts diff --git a/src/equicordplugins/fixFileExtensions/index.tsx b/src/equicordplugins/fixFileExtensions/index.tsx index 9e3fd34d7..e4664a7e3 100644 --- a/src/equicordplugins/fixFileExtensions/index.tsx +++ b/src/equicordplugins/fixFileExtensions/index.tsx @@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta return acc; }, {} as Record); -type ExtUpload = Upload & { fixExtension?: boolean; }; - export default definePlugin({ name: "FixFileExtensions", authors: [EquicordDevs.thororen], @@ -34,24 +32,21 @@ export default definePlugin({ patches: [ // Taken from AnonymiseFileNames { - find: 'type:"UPLOAD_START"', - replacement: { - match: /await \i\.uploadFiles\((\i),/, - replace: "$1.forEach($self.fixExt),$&" - }, + find: "async uploadFiles(", + replacement: [ + { + match: /async uploadFiles\((\i),\i\){/, + replace: "$&$1.forEach($self.fixExt);" + }, + { + match: /async uploadFilesSimple\((\i)\){/, + replace: "$&$1.forEach($self.fixExt);" + } + ], predicate: () => !Settings.plugins.AnonymiseFileNames.enabled, }, - // Also taken from AnonymiseFileNames - { - find: 'addFilesTo:"message.attachments"', - replacement: { - match: /(\i.uploadFiles\((\i),)/, - replace: "$2.forEach(f=>f.filename=$self.fixExt(f)),$1", - }, - predicate: () => !Settings.plugins.AnonymiseFileNames.enabled, - } ], - fixExt(upload: ExtUpload) { + fixExt(upload: Upload) { const file = upload.filename; const tarMatch = tarExtMatcher.exec(file); const extIdx = tarMatch?.index ?? file.lastIndexOf("."); diff --git a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts index 32d836b47..e36d40de4 100644 --- a/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts +++ b/src/equicordplugins/furudoSpeak.dev/providers/Ollama.ts @@ -24,7 +24,7 @@ export default async ( }: FurudoSettings, repliedMessage?: Message ): Promise => { - const completion = await fetch("http://localhost:11434/api/chat", { + const completion = await fetch("http://127.0.0.1:11434/api/chat", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/equicordplugins/githubRepos/index.tsx b/src/equicordplugins/githubRepos/index.tsx index 65eccb641..9d8595c28 100644 --- a/src/equicordplugins/githubRepos/index.tsx +++ b/src/equicordplugins/githubRepos/index.tsx @@ -30,7 +30,7 @@ export const settings = definePluginSettings({ }, showInMiniProfile: { type: OptionType.BOOLEAN, - description: "Only show a button in the mini profile", + description: "Show full ui in the mini profile instead of just a button", default: true }, }); diff --git a/src/equicordplugins/instantScreenshare/index.tsx b/src/equicordplugins/instantScreenshare/index.tsx index 4a97276c5..931d62ae6 100644 --- a/src/equicordplugins/instantScreenshare/index.tsx +++ b/src/equicordplugins/instantScreenshare/index.tsx @@ -4,60 +4,31 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByCode, findByProps } from "@webpack"; +import definePlugin from "@utils/types"; +import { findByCodeLazy } from "@webpack"; import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common"; import { VoiceState } from "@webpack/types"; -const settings = definePluginSettings({ - streamType: { - description: "Stream screen or window", - type: OptionType.SELECT, - options: [ - { label: "Screen", value: "screen" }, - { label: "Window", value: "window" } - ], - default: "screen" - }, - streamWindowKeyword: { - description: "Keyword to search for in window title", - type: OptionType.STRING, - default: "", - placeholder: "Enter keyword" - } -}); +import { getCurrentMedia, settings } from "./utils"; + +const startStream = findByCodeLazy('type:"STREAM_START"'); let hasStreamed; -let sources; -let source; -async function startStream() { - const startStream = findByCode('type:"STREAM_START"'); - const mediaEngine = findByProps("getMediaEngine").getMediaEngine(); - const getDesktopSources = findByCode("desktop sources"); +async function autoStartStream() { const selected = SelectedChannelStore.getVoiceChannelId(); if (!selected) return; const channel = ChannelStore.getChannel(selected); if (channel.type === 13 || !PermissionStore.can(PermissionsBits.STREAM, channel)) return; - if (settings.store.streamType === "screen") { - sources = await getDesktopSources(mediaEngine, ["screen"], null); - source = sources[0]; - } else if (settings.store.streamType === "window") { - const keyword = settings.store.streamWindowKeyword?.toLowerCase(); - sources = await getDesktopSources(mediaEngine, ["window", "application"], null); - source = sources.find(s => s.name?.toLowerCase().includes(keyword)); - } - - if (!source) return; + const streamMedia = await getCurrentMedia(); startStream(channel.guild_id, selected, { "pid": null, - "sourceId": source.id, - "sourceName": source.name, + "sourceId": streamMedia.id, + "sourceName": streamMedia.name, "audioSourceId": null, "sound": true, "previewDisabled": false @@ -78,7 +49,7 @@ export default definePlugin({ if (channelId && !hasStreamed) { hasStreamed = true; - await startStream(); + await autoStartStream(); } if (!channelId) { diff --git a/src/equicordplugins/instantScreenshare/utils.tsx b/src/equicordplugins/instantScreenshare/utils.tsx new file mode 100644 index 000000000..7f85ab4a6 --- /dev/null +++ b/src/equicordplugins/instantScreenshare/utils.tsx @@ -0,0 +1,102 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Logger } from "@utils/Logger"; +import { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { Forms, SearchableSelect, useEffect, useState } from "@webpack/common"; + +interface PickerProps { + streamMediaSelection: any[]; + streamMedia: any[]; +} + +const mediaEngine = findByPropsLazy("getMediaEngine"); +const getDesktopSources = findByCodeLazy("desktop sources"); + +export const settings = definePluginSettings({ + streamMedia: { + type: OptionType.COMPONENT, + component: SettingSection, + }, +}); + +export async function getCurrentMedia() { + const media = mediaEngine.getMediaEngine(); + const sources = [ + ...(await getDesktopSources(media, ["screen"], null) ?? []), + ...(await getDesktopSources(media, ["window", "application"], null) ?? []) + ]; + const streamMedia = sources.find(screen => screen.id === settings.store.streamMedia); + console.log(sources); + + if (streamMedia) return streamMedia; + + new Logger("InstantScreenShare").error(`Stream Media "${settings.store.streamMedia}" not found. Resetting to default.`); + + settings.store.streamMedia = sources[0]; + return sources[0]; +} + +function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps) { + const options = streamMediaSelection.map(screen => ({ + label: screen.name, + value: screen.id, + default: streamMediaSelection[0], + })); + + return ( + o.value === streamMedia)} + onChange={v => settings.store.streamMedia = v} + closeOnSelect + /> + ); +} + +function ScreenSetting() { + const { streamMedia } = settings.use(["streamMedia"]); + const media = mediaEngine.getMediaEngine(); + const [streamMediaSelection, setStreamMediaSelection] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + async function fetchMedia() { + setLoading(true); + const sources = [ + ...(await getDesktopSources(media, ["screen"], null) ?? []), + ...(await getDesktopSources(media, ["window", "application"], null) ?? []) + ]; + + if (active) { + setStreamMediaSelection(sources); + setLoading(false); + } + } + fetchMedia(); + return () => { active = false; }; + }, []); + + if (loading) return Loading media sources...; + if (!streamMediaSelection.length) return No Media found.; + + return ; +} + +function SettingSection() { + return ( + + Media source to stream + Resets to main screen if not found + + + ); +} diff --git a/src/equicordplugins/mediaDownloader.desktop/native.ts b/src/equicordplugins/mediaDownloader.desktop/native.ts index d35d3abd7..896f84578 100644 --- a/src/equicordplugins/mediaDownloader.desktop/native.ts +++ b/src/equicordplugins/mediaDownloader.desktop/native.ts @@ -103,12 +103,21 @@ export async function stop(_: IpcMainInvokeEvent) { } async function metadata(options: DownloadOptions) { - stdout_global = ""; - const metadata = JSON.parse(await ytdlp(["-J", options.url, "--no-warnings"])); - if (metadata.is_live) throw "Live streams are not supported."; - stdout_global = ""; - return { videoTitle: `${metadata.title || "video"} (${metadata.id})` }; + try { + stdout_global = ""; + const output = await ytdlp(["-J", options.url, "--no-warnings"]); + const metadata = JSON.parse(output); + + if (metadata.is_live) throw new Error("Live streams are not supported."); + + stdout_global = ""; + return { videoTitle: `${metadata.title || "video"} (${metadata.id})` }; + + } catch (err) { + throw err; + } } + function genFormat({ videoTitle }: { videoTitle: string; }, { maxFileSize, format }: DownloadOptions) { const HAS_LIMIT = !!maxFileSize; const MAX_VIDEO_SIZE = HAS_LIMIT ? maxFileSize * 0.8 : 0; @@ -161,8 +170,11 @@ async function download({ format, videoTitle }: { format: string; videoTitle: st : [] : []; const customArgs = ytdlpArgs?.filter(Boolean) || []; - - await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]); + try { + await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]); + } catch (err) { + console.error("Error during yt-dlp execution:", err); + } const file = fs.readdirSync(getdir()).find(f => f.startsWith("download.")); if (!file) throw "No video file was found!"; return { file, videoTitle }; diff --git a/src/equicordplugins/messageFetchTimer/index.tsx b/src/equicordplugins/messageFetchTimer/index.tsx new file mode 100644 index 000000000..40a6a7bff --- /dev/null +++ b/src/equicordplugins/messageFetchTimer/index.tsx @@ -0,0 +1,170 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { getCurrentChannel } from "@utils/discord"; +import definePlugin, { OptionType } from "@utils/types"; +import { FluxDispatcher, React } from "@webpack/common"; + +interface FetchTiming { + channelId: string; + startTime: number; + endTime?: number; + duration?: number; + timestamp?: Date; +} + +let currentFetch: FetchTiming | null = null; +let currentChannelId: string | null = null; +const channelTimings: Map = new Map(); + +const settings = definePluginSettings({ + showIcon: { + type: OptionType.BOOLEAN, + description: "Show fetch time icon in message bar", + default: true, + }, + showMs: { + type: OptionType.BOOLEAN, + description: "Show milliseconds in timing", + default: true, + }, + iconColor: { + type: OptionType.STRING, + description: "Icon color (CSS color value)", + default: "#00d166", + } +}); + +const FetchTimeButton: ChatBarButtonFactory = ({ isMainChat }) => { + const { showMs, iconColor } = settings.use(["showMs", "iconColor"]); + + if (!isMainChat || !settings.store.showIcon || !currentChannelId) { + return null; + } + + const channelData = channelTimings.get(currentChannelId); + if (!channelData) { + return null; + } + + const { time, timestamp } = channelData; + const displayTime = showMs ? `${Math.round(time)}ms` : `${Math.round(time / 1000)}s`; + + if (!showMs && Math.round(time / 1000) === 0) { + return null; + } + + const timeAgo = formatTimeAgo(timestamp); + + return ( + { }} + > +
+ + + + + {displayTime} + +
+
+ ); +}; + +function formatTimeAgo(timestamp: Date): string { + const now = new Date(); + const diff = now.getTime() - timestamp.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days} day${days > 1 ? "s" : ""} ago`; + } else if (hours > 0) { + return `${hours} hour${hours > 1 ? "s" : ""} ago`; + } else if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; + } else { + return "just now"; + } +} + +function handleChannelSelect(data: any) { + if (data.channelId && data.channelId !== currentChannelId) { + currentChannelId = data.channelId; + currentFetch = { + channelId: data.channelId, + startTime: performance.now() + }; + } +} + +function handleMessageLoad(data: any) { + if (!currentFetch || data.channelId !== currentFetch.channelId) return; + + const existing = channelTimings.get(currentFetch.channelId); + if (existing) return; + + const endTime = performance.now(); + const duration = endTime - currentFetch.startTime; + + channelTimings.set(currentFetch.channelId, { + time: duration, + timestamp: new Date() + }); + + currentFetch = null; +} + + +export default definePlugin({ + name: "MessageFetchTimer", + description: "Shows how long it took to fetch messages for the current channel", + authors: [EquicordDevs.GroupXyz], + settings, + + start() { + FluxDispatcher.subscribe("CHANNEL_SELECT", handleChannelSelect); + FluxDispatcher.subscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad); + FluxDispatcher.subscribe("MESSAGE_CREATE", handleMessageLoad); + + const currentChannel = getCurrentChannel(); + if (currentChannel) { + currentChannelId = currentChannel.id; + } + }, + + stop() { + FluxDispatcher.unsubscribe("CHANNEL_SELECT", handleChannelSelect); + FluxDispatcher.unsubscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad); + FluxDispatcher.unsubscribe("MESSAGE_CREATE", handleMessageLoad); + + currentFetch = null; + channelTimings.clear(); + currentChannelId = null; + }, + + renderChatBarButton: FetchTimeButton, +}); diff --git a/src/equicordplugins/messageLoggerEnhanced/index.tsx b/src/equicordplugins/messageLoggerEnhanced/index.tsx index bbf4df3ac..e4c560d8c 100644 --- a/src/equicordplugins/messageLoggerEnhanced/index.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/index.tsx @@ -78,7 +78,8 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo bot: message?.bot || message?.author?.bot, flags: message?.flags, ghostPinged, - isCachedByUs: (message as LoggedMessageJSON).ourCache + isCachedByUs: (message as LoggedMessageJSON).ourCache, + webhookId: message?.webhookId }) ) { // Flogger.log("IGNORING", message, payload); diff --git a/src/equicordplugins/messageLoggerEnhanced/settings.tsx b/src/equicordplugins/messageLoggerEnhanced/settings.tsx index 00f2bee59..d21ea4b0c 100644 --- a/src/equicordplugins/messageLoggerEnhanced/settings.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/settings.tsx @@ -52,6 +52,12 @@ export const settings = definePluginSettings({ } }, + ignoreWebhooks: { + type: OptionType.BOOLEAN, + description: "Whether to ignore messages by webhooks", + default: false, + }, + ignoreSelf: { type: OptionType.BOOLEAN, description: "Whether to ignore messages by yourself", diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts index cff08e2ce..082e2c190 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts @@ -74,6 +74,7 @@ interface ShouldIgnoreArguments { bot?: boolean; ghostPinged?: boolean; isCachedByUs?: boolean; + webhookId?: string; } const EPHEMERAL = 64; @@ -87,7 +88,7 @@ const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); * @param {ShouldIgnoreArguments} args - An object containing the message details. * @returns {boolean} - True if the message should be ignored, false if it should be kept. */ -export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs }: ShouldIgnoreArguments): boolean { +export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs, webhookId }: ShouldIgnoreArguments): boolean { const isEphemeral = ((flags ?? 0) & EPHEMERAL) === EPHEMERAL; if (isEphemeral) return true; // ignore @@ -96,7 +97,7 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi const myId = UserStore.getCurrentUser().id; const { ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger; - const { ignoreBots, ignoreSelf } = settings.store; + const { ignoreBots, ignoreSelf, ignoreWebhooks } = settings.store; if (ignoreSelf && authorId === myId) return true; // ignore @@ -132,6 +133,8 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi if ((ignoreBots && bot) && !isAuthorWhitelisted) return true; // ignore + if ((ignoreWebhooks && webhookId) && !isAuthorWhitelisted) return true; + if (ghostPinged) return false; // keep // author has highest priority diff --git a/src/equicordplugins/moreCommands/index.ts b/src/equicordplugins/moreCommands/index.ts new file mode 100644 index 000000000..02f3c3738 --- /dev/null +++ b/src/equicordplugins/moreCommands/index.ts @@ -0,0 +1,65 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated, Samu and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + + +function mock(input: string): string { + let output = ""; + for (let i = 0; i < input.length; i++) { + output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase(); + } + return output; +} + +export default definePlugin({ + name: "MoreCommands", + description: "echo, lenny, mock", + authors: [Devs.Arjix, Devs.echo, Devs.Samu], + commands: [ + { + name: "echo", + description: "Sends a message as Clyde (locally)", + options: [OptionalMessageOption], + inputType: ApplicationCommandInputType.BOT, + execute: (opts, ctx) => { + const content = findOption(opts, "message", ""); + + sendBotMessage(ctx.channel.id, { content }); + }, + }, + { + name: "lenny", + description: "Sends a lenny face", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" + }), + }, + { + name: "mock", + description: "mOcK PeOpLe", + options: [RequiredMessageOption], + execute: opts => ({ + content: mock(findOption(opts, "message", "")) + }), + }, + ] +}); diff --git a/src/plugins/moreKaomoji/index.ts b/src/equicordplugins/moreKaomoji/index.ts similarity index 100% rename from src/plugins/moreKaomoji/index.ts rename to src/equicordplugins/moreKaomoji/index.ts diff --git a/src/plugins/moyai/index.ts b/src/equicordplugins/moyai/index.ts similarity index 96% rename from src/plugins/moyai/index.ts rename to src/equicordplugins/moyai/index.ts index 9d095c478..a0f76b014 100644 --- a/src/plugins/moyai/index.ts +++ b/src/equicordplugins/moyai/index.ts @@ -52,10 +52,8 @@ interface IVoiceChannelEffectSendEvent { } const MOYAI = "🗿"; -const MOYAI_URL = - "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3"; -const MOYAI_URL_HD = - "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav"; +const MOYAI_URL = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3"; +const MOYAI_URL_HD = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav"; const settings = definePluginSettings({ volume: { diff --git a/src/equicordplugins/noRPC.discordDesktop/index.ts b/src/equicordplugins/noRPC.discordDesktop/index.ts new file mode 100644 index 000000000..51ed1f962 --- /dev/null +++ b/src/equicordplugins/noRPC.discordDesktop/index.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NoRPC", + description: "Disables Discord's RPC server.", + authors: [Devs.Cyn], + patches: [ + { + find: '.ensureModule("discord_rpc")', + replacement: { + match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/, + replace: '.ensureModule("discord_rpc")}', + }, + }, + ], +}); diff --git a/src/plugins/partyMode/index.ts b/src/equicordplugins/partyMode/index.ts similarity index 100% rename from src/plugins/partyMode/index.ts rename to src/equicordplugins/partyMode/index.ts diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx index 19a135338..3580051ac 100644 --- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx +++ b/src/equicordplugins/questCompleter.discordDesktop/index.tsx @@ -51,26 +51,27 @@ async function openCompleteQuestUI() { const applicationId = quest.config.application.id; const applicationName = quest.config.application.name; const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null); + const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; // @ts-ignore const secondsNeeded = quest.config.taskConfig.tasks[taskName].target; // @ts-ignore - const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; - const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; + let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; if (taskName === "WATCH_VIDEO") { - const tolerance = 2, speed = 10; - const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000); - const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded); + const maxFuture = 10, speed = 7, interval = 1; + const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime(); const fn = async () => { - for (let i = startingPoint; i <= secondsNeeded; i += speed) { - try { - await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } }); - } catch (ex) { - console.log("Failed to send increment of", i, ex); + while (true) { + const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture; + const diff = maxAllowed - secondsDone; + const timestamp = secondsDone + speed; + if (diff >= speed) { + await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } }); + secondsDone = Math.min(secondsNeeded, timestamp); } - await new Promise(resolve => setTimeout(resolve, tolerance * 1000)); - } - if ((secondsNeeded - secondsDone) % speed !== 0) { - await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } }); + if (timestamp >= secondsNeeded) { + break; + } + await new Promise(resolve => setTimeout(resolve, interval * 1000)); showNotification({ title: `${applicationName} - Quest Completer`, body: "Quest Completed.", @@ -81,10 +82,9 @@ async function openCompleteQuestUI() { fn(); showNotification({ title: `${applicationName} - Quest Completer`, - body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`, + body: `Spoofing video for ${applicationName}.`, icon: icon, }); - console.log(`Spoofing video for ${applicationName}.`); } else if (taskName === "PLAY_ON_DESKTOP") { RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => { const appData = res.body[0]; diff --git a/src/equicordplugins/splitLargeMessages/index.ts b/src/equicordplugins/splitLargeMessages/index.ts index cb19fe702..50dadff56 100644 --- a/src/equicordplugins/splitLargeMessages/index.ts +++ b/src/equicordplugins/splitLargeMessages/index.ts @@ -61,8 +61,7 @@ const listener: MessageSendListener = async (channelId, msg) => { if (hardSplit || splitIndex === -1) { chunks.push(msg.content.slice(0, maxLength)); msg.content = msg.content.slice(maxLength); - } - else { + } else { chunks.push(msg.content.slice(0, splitIndex)); msg.content = msg.content.slice(splitIndex); } diff --git a/src/equicordplugins/streamingCodecDisabler/index.ts b/src/equicordplugins/streamingCodecDisabler/index.ts new file mode 100644 index 000000000..f26113e9c --- /dev/null +++ b/src/equicordplugins/streamingCodecDisabler/index.ts @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings, Settings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findStoreLazy } from "@webpack"; + +let mediaEngine = findStoreLazy("MediaEngineStore"); + +const originalCodecStatuses: { + AV1: boolean, + H265: boolean, + H264: boolean; +} = { + AV1: true, + H265: true, + H264: true +}; + +const settings = definePluginSettings({ + disableAv1Codec: { + description: "Make Discord not consider using AV1 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, + disableH265Codec: { + description: "Make Discord not consider using H265 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, + disableH264Codec: { + description: "Make Discord not consider using H264 for streaming.", + type: OptionType.BOOLEAN, + default: false + }, +}); + +export default definePlugin({ + name: "StreamingCodecDisabler", + description: "Disable codecs for streaming of your choice", + authors: [EquicordDevs.davidkra230], + settings, + + patches: [ + { + find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo", + replacement: { + match: /setGoLiveSource\(.,.\)\{/, + replace: "$&$self.updateDisabledCodecs();" + }, + } + ], + + async updateDisabledCodecs() { + mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec); + mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec); + mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec); + }, + + async start() { + mediaEngine = mediaEngine.getMediaEngine(); + const options = Object.keys(originalCodecStatuses); + // [{"codec":"","decode":false,"encode":false}] + const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res))); + CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => { + if (options.includes(codec.codec)) { + originalCodecStatuses[codec.codec] = codec.encode; + } + }); + }, + + async stop() { + mediaEngine.setAv1Enabled(originalCodecStatuses.AV1); + mediaEngine.setH265Enabled(originalCodecStatuses.H265); + mediaEngine.setH264Enabled(originalCodecStatuses.H264); + } +}); diff --git a/src/equicordplugins/timezones/TimezoneModal.tsx b/src/equicordplugins/timezones/TimezoneModal.tsx index 1f01922d8..bd90d378a 100644 --- a/src/equicordplugins/timezones/TimezoneModal.tsx +++ b/src/equicordplugins/timezones/TimezoneModal.tsx @@ -8,9 +8,10 @@ import * as DataStore from "@api/DataStore"; import { classNameFactory } from "@api/Styles"; import { Margins } from "@utils/margins"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; -import { Button, Forms, SearchableSelect, useMemo, useState } from "@webpack/common"; +import { Button, Forms, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common"; -import { DATASTORE_KEY, timezones } from "."; +import { DATASTORE_KEY, settings, timezones } from "."; +import { getTimezone, setTimezone, setUserDatabaseTimezone } from "./database"; export async function setUserTimezone(userId: string, timezone: string | null) { timezones[userId] = timezone; @@ -19,9 +20,22 @@ export async function setUserTimezone(userId: string, timezone: string | null) { const cl = classNameFactory("vc-timezone-"); -export function SetTimezoneModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) { +export function SetTimezoneModal({ userId, modalProps, database }: { userId: string, modalProps: ModalProps; database?: boolean; }) { const [currentValue, setCurrentValue] = useState(timezones[userId] ?? null); + useEffect(() => { + const localTimezone = timezones[userId]; + const shouldUseDatabase = + settings.store.useDatabase && + (settings.store.preferDatabaseOverLocal || !localTimezone); + + const value = shouldUseDatabase + ? getTimezone(userId) ?? localTimezone + : localTimezone; + + setCurrentValue(value ?? Intl.DateTimeFormat().resolvedOptions().timeZone); + }, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]); + const options = useMemo(() => { return Intl.supportedValuesOf("timeZone").map(timezone => { const offset = new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: "short" }) @@ -59,20 +73,28 @@ export function SetTimezoneModal({ userId, modalProps }: { userId: string, modal - + {!database && ( + + )} + ) + }, + + resetDatabaseTimezone: { + description: "Reset your timezone on the database", + type: OptionType.COMPONENT, + component: () => ( + + ) + }, + + askedTimezone: { + type: OptionType.BOOLEAN, + description: "Whether the user has been asked to set their timezone", + hidden: true, + default: false } }); @@ -64,25 +118,36 @@ interface Props { timestamp?: string; type: "message" | "profile"; } + const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Props) => { const [currentTime, setCurrentTime] = useState(timestamp || Date.now()); - const timezone = timezones[userId]; + const [timezone, setTimezone] = useState(null); useEffect(() => { - let timer: NodeJS.Timeout; + const localTimezone = timezones[userId]; + const shouldUseDatabase = + settings.store.useDatabase && + (settings.store.preferDatabaseOverLocal || !localTimezone); - if (type === "profile") { - setCurrentTime(Date.now()); - - const now = new Date(); - const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds(); - - timer = setTimeout(() => { - setCurrentTime(Date.now()); - }, delay); + if (shouldUseDatabase) { + setTimezone(getTimezone(userId) ?? localTimezone); + } else { + setTimezone(localTimezone); } + }, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]); - return () => timer && clearTimeout(timer); + useEffect(() => { + if (type !== "profile") return; + + setCurrentTime(Date.now()); + + const now = new Date(); + const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds(); + const timer = setTimeout(() => { + setCurrentTime(Date.now()); + }, delay); + + return () => clearTimeout(timer); }, [type, currentTime]); if (!timezone) return null; @@ -94,8 +159,9 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop month: "long", day: "numeric", hour: "numeric", - minute: "numeric", + minute: "numeric" }); + return ( - {toolTipProps => { - return ( - - { - type === "message" ? `(${shortTime})` : shortTime - } - - ); - }} + {toolTipProps => ( + + {type === "message" ? `(${shortTime})` : shortTime} + + )} ); }, { noop: true }); @@ -128,19 +190,18 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: { const setTimezoneItem = ( openModal(modalProps => )} /> ); children.push(, setTimezoneItem); - }; export default definePlugin({ name: "Timezones", - authors: [Devs.Aria], + authors: [Devs.Aria, EquicordDevs.creations], description: "Shows the local time of users in profiles and message headers", contextMenus: { "user-context": userContextMenuPatch @@ -164,26 +225,76 @@ export default definePlugin({ } } ], + + toolboxActions: { + "Set Database Timezone": () => { + authModal(async () => { + openModal(modalProps => ); + }); + }, + "Refresh Database Timezones": async () => { + try { + const good = await loadDatabaseTimezones(); + + if (good) { + showToast("Timezones refreshed successfully!", Toasts.Type.SUCCESS); + } else { + showToast("Timezones Failed to refresh!", Toasts.Type.FAILURE); + } + } + catch (error) { + console.error("Failed to refresh timezone:", error); + showToast("Failed to refresh timezones.", Toasts.Type.FAILURE); + } + } + }, + + async start() { + timezones = await DataStore.get>(DATASTORE_KEY) || {}; + + if (settings.store.useDatabase) { + await loadDatabaseTimezones(); + + if (!settings.store.askedTimezone) { + showToast( + "", + Toasts.Type.MESSAGE, + { + duration: 10000, + component: ( + + ), + position: Toasts.Position.BOTTOM + } + ); + settings.store.askedTimezone = true; + } + } + }, + settings, getTime, - renderProfileTimezone: (props?: { user?: User; }) => { if (!settings.store.showProfileTime || !props?.user?.id) return null; + if (props.user.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null; - return ; + return ; }, renderMessageTimezone: (props?: { message?: Message; }) => { if (!settings.store.showMessageHeaderTime || !props?.message) return null; + if (props.message.author.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null; - return ; + return ; } }); diff --git a/src/equicordplugins/tosuRPC/index.ts b/src/equicordplugins/tosuRPC/index.ts index b1727773e..05d908b37 100644 --- a/src/equicordplugins/tosuRPC/index.ts +++ b/src/equicordplugins/tosuRPC/index.ts @@ -29,7 +29,7 @@ export default definePlugin({ authors: [Devs.AutumnVN], start() { (function connect() { - ws = new WebSocket("ws://localhost:24050/websocket/v2"); + ws = new WebSocket("ws://127.0.0.1:24050/websocket/v2"); ws.addEventListener("error", () => ws.close()); ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000)); ws.addEventListener("message", ({ data }) => throttledOnMessage(data)); diff --git a/src/main/csp.ts b/src/main/csp.ts index d5027ea0e..b35a11a81 100644 --- a/src/main/csp.ts +++ b/src/main/csp.ts @@ -8,30 +8,30 @@ import { session } from "electron"; type PolicyMap = Record; -const ConnectSrc = ["connect-src"]; -const MediaSrc = [...ConnectSrc, "img-src", "media-src"]; -const CssSrc = ["style-src", "font-src"]; -const MediaAndCssSrc = [...MediaSrc, ...CssSrc]; +export const ConnectSrc = ["connect-src"]; +export const MediaSrc = [...ConnectSrc, "img-src", "media-src"]; +export const CssSrc = ["style-src", "font-src"]; +export const MediaAndCssSrc = [...MediaSrc, ...CssSrc]; export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"]; // Plugins can whitelist their own domains by importing this object in their native.ts // script and just adding to it. But generally, you should just edit this file instead export const CspPolicies: PolicyMap = { - "*.github.io": MediaAndCssSrc, // github pages, used by most themes - "raw.githubusercontent.com": MediaAndCssSrc, // github raw, used by some themes - "*.gitlab.io": MediaAndCssSrc, // gitlab pages, used by some themes - "gitlab.com": MediaAndCssSrc, // gitlab raw, used by some themes - "*.codeberg.page": MediaAndCssSrc, // codeberg pages, used by some themes - "codeberg.org": MediaAndCssSrc, // codeberg raw, used by some themes + "*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes + "raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes + "*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes + "gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes + "*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes + "codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes "*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes - "jsdelivr.net": MediaAndCssSrc, // jsdeliver, used by very few themes + "jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes - "fonts.googleapis.com": CssSrc, // google fonts, used by many themes + "fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes - "i.imgur.com": MediaSrc, // imgur, used by some themes - "i.ibb.co": MediaSrc, // imgbb, used by some themes + "i.imgur.com": MediaSrc, // Imgur, used by some themes + "i.ibb.co": MediaSrc, // ImgBB, used by some themes "cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media "media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN @@ -43,7 +43,7 @@ export const CspPolicies: PolicyMap = { // Function Specific "api.github.com": ConnectSrc, // used for updating Vencord itself - "ws.audioscrobbler.com": ConnectSrc, // last.fm API + "ws.audioscrobbler.com": ConnectSrc, // Last.fm API "translate-pa.googleapis.com": ConnectSrc, // Google Translate API "*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev) "manti.vendicated.dev": MediaSrc, // ReviewDB API @@ -78,7 +78,7 @@ const stringifyPolicy = (policy: PolicyMap): string => .join("; "); -const patchCsp = (headers: Record) => { +const patchCsp = (headers: PolicyMap) => { const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only"); if (reportOnlyHeader) delete headers[reportOnlyHeader]; diff --git a/src/main/index.ts b/src/main/index.ts index 6331b19d8..5c27b1886 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -25,7 +25,7 @@ import { RendererSettings } from "./settings"; import { IS_VANILLA, THEMES_DIR } from "./utils/constants"; import { installExt } from "./utils/extensions"; -if (IS_VESKTOP || IS_EQUIBOP || !IS_VANILLA) { +if (!IS_VANILLA && !IS_EXTENSION) { app.whenReady().then(() => { // Source Maps! Maybe there's a better way but since the renderer is executed // from a string I don't think any other form of sourcemaps would work diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index c3ef05980..2fbb40ab4 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -24,14 +24,14 @@ import { openContributorModal } from "@components/PluginSettings/ContributorModa import { isEquicordDonor } from "@components/VencordSettings/VencordTab"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import { isEquicordPluginDev, isPluginDev } from "@utils/misc"; +import { shouldShowContributorBadge, shouldShowEquicordContributorBadge } from "@utils/misc"; import definePlugin from "@utils/types"; import { Toasts, UserStore } from "@webpack/common"; import { User } from "discord-types/general"; import { EquicordDonorModal, VencordDonorModal } from "./modals"; -const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png"; +const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png"; const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png"; @@ -39,7 +39,7 @@ const ContributorBadge: ProfileBadge = { description: "Vencord Contributor", image: CONTRIBUTOR_BADGE, position: BadgePosition.START, - shouldShow: ({ userId }) => isPluginDev(userId), + shouldShow: ({ userId }) => shouldShowContributorBadge(userId), onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId)) }; @@ -47,7 +47,7 @@ const EquicordContributorBadge: ProfileBadge = { description: "Equicord Contributor", image: EQUICORD_CONTRIBUTOR_BADGE, position: BadgePosition.START, - shouldShow: ({ userId }) => isEquicordPluginDev(userId), + shouldShow: ({ userId }) => shouldShowEquicordContributorBadge(userId), onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId)) }; @@ -77,7 +77,7 @@ async function loadBadges(url: string, noCache = false) { async function loadAllBadges(noCache = false) { const vencordBadges = await loadBadges("https://badges.vencord.dev/badges.json", noCache); - const equicordBadges = await loadBadges("https://equicord.org/badges", noCache); + const equicordBadges = await loadBadges("https://equicord.org/badges.json", noCache); DonorBadges = vencordBadges; EquicordDonorBadges = equicordBadges; diff --git a/src/plugins/_api/userSettings.ts b/src/plugins/_api/userSettings.ts index 3a00bc116..559de3698 100644 --- a/src/plugins/_api/userSettings.ts +++ b/src/plugins/_api/userSettings.ts @@ -30,8 +30,8 @@ export default definePlugin({ replacement: [ // Main setting definition { - match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/, - replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&" + match: /\.updateAsync\(.+?(?=,useSetting:)/, + replace: "$&,userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1]" }, // Selective wrapper { diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx index 84b425182..51e3e5caa 100644 --- a/src/plugins/anonymiseFileNames/index.tsx +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -79,18 +79,17 @@ export default definePlugin({ patches: [ { - find: 'type:"UPLOAD_START"', - replacement: { - match: /await \i\.uploadFiles\((\i),/, - replace: "$1.forEach($self.anonymise),$&" - }, - }, - { - find: 'addFilesTo:"message.attachments"', - replacement: { - match: /\i.uploadFiles\((\i),/, - replace: "$1.forEach($self.anonymise),$&" - } + find: "async uploadFiles(", + replacement: [ + { + match: /async uploadFiles\((\i),\i\){/, + replace: "$&$1.forEach($self.anonymise);" + }, + { + match: /async uploadFilesSimple\((\i)\){/, + replace: "$&$1.forEach($self.anonymise);" + } + ], }, { find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}", diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx index f7dc6ba8c..00fc0f16d 100644 --- a/src/plugins/arRPC.web/index.tsx +++ b/src/plugins/arRPC.web/index.tsx @@ -36,12 +36,21 @@ async function lookupApp(applicationId: string): Promise { return socket.application; } +let hideSetting = false; + +if (IS_VESKTOP || IS_EQUIBOP || "legcord" in window) { + hideSetting = true; +} else if ("goofcord" in window) { + hideSetting = false; +} + let ws: WebSocket; export default definePlugin({ name: "WebRichPresence (arRPC)", description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", authors: [Devs.Ducko], reporterTestable: ReporterTestable.None, + hidden: hideSetting, settingsAboutComponent: () => ( <> @@ -73,9 +82,6 @@ export default definePlugin({ }, async start() { - // Legcord comes with its own arRPC implementation, so this plugin just confuses users - if ("legcord" in window) return; - if (ws) ws.close(); ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket diff --git a/src/plugins/customidle/index.ts b/src/plugins/customIdle/index.ts similarity index 100% rename from src/plugins/customidle/index.ts rename to src/plugins/customIdle/index.ts diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 248c93bc7..05b62667a 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -139,5 +139,5 @@ export default definePlugin({ } }, - DecorSection: ErrorBoundary.wrap(DecorSection) + DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true }) }); diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/expressionCloner/index.tsx similarity index 97% rename from src/plugins/emoteCloner/index.tsx rename to src/plugins/expressionCloner/index.tsx index ffc2307e6..3a73489c5 100644 --- a/src/plugins/emoteCloner/index.tsx +++ b/src/plugins/expressionCloner/index.tsx @@ -17,6 +17,7 @@ */ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { migratePluginSettings } from "@api/Settings"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -165,7 +166,7 @@ async function doClone(guildId: string, data: Sticker | Emoji) { message = JSON.parse(e.text).message; } catch { } - new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e); + new Logger("ExpressionCloner").error("Failed to clone", data.name, "to", guildId, e); Toasts.show({ message: "Failed to clone: " + message, type: Toasts.Type.FAILURE, @@ -364,10 +365,11 @@ const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { t } }; +migratePluginSettings("ExpressionCloner", "EmoteCloner"); export default definePlugin({ - name: "EmoteCloner", + name: "ExpressionCloner", description: "Allows you to clone Emotes & Stickers to your own server (right click them)", - tags: ["StickerCloner"], + tags: ["StickerCloner", "EmoteCloner", "EmojiCloner"], authors: [Devs.Ven, Devs.Nuckyz], contextMenus: { "message": messageContextMenuPatch, diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx index 80f7117ef..be3d587b2 100644 --- a/src/plugins/favGifSearch/index.tsx +++ b/src/plugins/favGifSearch/index.tsx @@ -118,7 +118,7 @@ export default definePlugin({ renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) { this.instance = instance; return ( - + ); diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfmRichPresence/index.tsx similarity index 100% rename from src/plugins/lastfm/index.tsx rename to src/plugins/lastfmRichPresence/index.tsx diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx index 2121d24c3..231253394 100644 --- a/src/plugins/mentionAvatars/index.tsx +++ b/src/plugins/mentionAvatars/index.tsx @@ -98,7 +98,7 @@ export default definePlugin({ src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`} /> ); - }), + }, { noop: true }), }); function getUsernameString(username: string) { diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 14f75a0e8..ea8f3dad9 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso import { updateMessage } from "@api/MessageUpdater"; import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants.js"; import { classes } from "@utils/misc"; import { Queue } from "@utils/Queue"; @@ -373,7 +372,7 @@ export default definePlugin({ settings, start() { - addMessageAccessory("messageLinkEmbed", props => { + addMessageAccessory("MessageLinkEmbeds", props => { if (!messageLinkRegex.test(props.message.content)) return null; @@ -381,15 +380,13 @@ export default definePlugin({ messageLinkRegex.lastIndex = 0; return ( - - - + ); }, 4 /* just above rich embeds */); }, stop() { - removeMessageAccessory("messageLinkEmbed"); + removeMessageAccessory("MessageLinkEmbeds"); } }); diff --git a/src/plugins/moreCommands/index.tsx b/src/plugins/moreCommands/index.tsx deleted file mode 100644 index c167fe2fb..000000000 --- a/src/plugins/moreCommands/index.tsx +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated, Samu and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - - -function mock(input: string): string { - let output = ""; - for (let i = 0; i < input.length; i++) { - output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase(); - } - return output; -} - -export default definePlugin({ - name: "MoreCommands", - description: "Echo, Lenny, Mock, and More", - authors: [Devs.Arjix, Devs.echo, Devs.Samu], - commands: [ - { - name: "echo", - description: "Sends a message as Clyde (locally)", - options: [OptionalMessageOption], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - const content = findOption(opts, "message", ""); - - sendBotMessage(ctx.channel.id, { content }); - }, - }, - { - name: "lenny", - description: "Sends a lenny face", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)" - }), - }, - { - name: "mock", - description: "mOcK PeOpLe", - options: [RequiredMessageOption], - execute: opts => ({ - content: mock(findOption(opts, "message", "")) - }), - }, - { - name: "reverse", - description: "Reverses the input message", - options: [RequiredMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "").split("").reverse().join("") - }), - }, - { - name: "uppercase", - description: "Converts the message to uppercase", - options: [RequiredMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "").toUpperCase() - }), - }, - { - name: "lowercase", - description: "Converts the message to lowercase", - options: [RequiredMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "").toLowerCase() - }), - }, - { - name: "wordcount", - description: "Counts the number of words in a message", - options: [RequiredMessageOption], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - const message = findOption(opts, "message", ""); - const wordCount = message.trim().split(/\s+/).length; - sendBotMessage(ctx.channel.id, { - content: `The message contains ${wordCount} words.` - }); - }, - }, - { - name: "ping", - description: "Pings the bot to check if it's responding", - options: [], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - sendBotMessage(ctx.channel.id, { - content: "Pong!" - }); - }, - }, - { - name: "rolldice", - description: "Roll a die with the specified number of sides", - options: [RequiredMessageOption], - execute: opts => { - const sides = parseInt(findOption(opts, "message", "6")); - const roll = Math.floor(Math.random() * sides) + 1; - return { - content: `You rolled a ${roll}!` - }; - }, - }, - { - name: "flipcoin", - description: "Flips a coin and returns heads or tails", - options: [], - execute: (opts, ctx) => { - const flip = Math.random() < 0.5 ? "Heads" : "Tails"; - return { - content: `The coin landed on: ${flip}` - }; - }, - }, - { - name: "ask", - description: "Ask a yes/no question and get an answer", - options: [RequiredMessageOption], - execute: opts => { - const question = findOption(opts, "message", ""); - const responses = [ - "Yes", "No", "Maybe", "Ask again later", "Definitely not", "It is certain" - ]; - const response = responses[Math.floor(Math.random() * responses.length)]; - return { - content: `${question} - ${response}` - }; - }, - }, - { - name: "randomcat", - description: "Get a random cat picture", - options: [], - execute: (opts, ctx) => { - return (async () => { - try { - const response = await fetch("https://api.thecatapi.com/v1/images/search"); - if (!response.ok) throw new Error("Failed to fetch cat image"); - const data = await response.json(); - return { - content: data[0].url - }; - } catch (err) { - sendBotMessage(ctx.channel.id, { - content: "Sorry, couldn't fetch a cat picture right now 😿" - }); - } - })(); - }, - }, - { - name: "randomdog", - description: "Get a random ddog picture", - options: [], - execute: (opts, ctx) => { - return (async () => { - try { - const response = await fetch("https://api.thedogapi.com/v1/images/search"); - if (!response.ok) throw new Error("Failed to fetch dog image"); - const data = await response.json(); - return { - content: data[0].url - }; - } catch (err) { - sendBotMessage(ctx.channel.id, { - content: "Sorry, couldn't fetch a cat picture right now 🐶" - }); - } - })(); - }, - }, - { - name: "randomnumber", - description: "Generates a random number between two values", - options: [ - { - name: "min", - description: "Minimum value", - type: ApplicationCommandOptionType.INTEGER, - required: true - }, - { - name: "max", - description: "Maximum value", - type: ApplicationCommandOptionType.INTEGER, - required: true - } - ], - execute: opts => { - const min = parseInt(findOption(opts, "min", "0")); - const max = parseInt(findOption(opts, "max", "100")); - const number = Math.floor(Math.random() * (max - min + 1)) + min; - return { - content: `Random number between ${min} and ${max}: ${number}` - }; - } - }, - { - name: "countdown", - description: "Starts a countdown from a specified number", - options: [ - { - name: "number", - description: "Number to countdown from (max 10)", - type: ApplicationCommandOptionType.INTEGER, - required: true - } - ], - inputType: ApplicationCommandInputType.BOT, - execute: async (opts, ctx) => { - const number = Math.min(parseInt(findOption(opts, "number", "5")), 10); - if (isNaN(number) || number < 1) { - sendBotMessage(ctx.channel.id, { - content: "Please provide a valid number between 1 and 10!" - }); - return; - } - sendBotMessage(ctx.channel.id, { - content: `Starting countdown from ${number}...` - }); - for (let i = number; i >= 0; i--) { - await new Promise(resolve => setTimeout(resolve, 1000)); - sendBotMessage(ctx.channel.id, { - content: i === 0 ? "🎉 Go! 🎉" : `${i}...` - }); - } - }, - }, - { - name: "choose", - description: "Randomly chooses from provided options", - options: [ - { - name: "choices", - description: "Comma-separated list of choices", - type: ApplicationCommandOptionType.STRING, - required: true - } - ], - execute: opts => { - const choices = findOption(opts, "choices", "").split(",").map(c => c.trim()); - const choice = choices[Math.floor(Math.random() * choices.length)]; - return { - content: `I choose: ${choice}` - }; - } - }, - { - name: "systeminfo", - description: "Shows system information", - options: [], - execute: async (opts, ctx) => { - try { - const { userAgent, hardwareConcurrency, onLine, languages } = navigator; - const { width, height, colorDepth } = window.screen; - const { deviceMemory, connection }: { deviceMemory: any, connection: any; } = navigator as any; - const platform = userAgent.includes("Windows") ? "Windows" : - userAgent.includes("Mac") ? "MacOS" : - userAgent.includes("Linux") ? "Linux" : "Unknown"; - const isMobile = /Mobile|Android|iPhone/i.test(userAgent); - const deviceType = isMobile ? "Mobile" : "Desktop"; - const browserInfo = userAgent.match(/(?:chrome|firefox|safari|edge|opr)\/?\s*(\d+)/i)?.[0] || "Unknown"; - const networkInfo = connection ? `${connection.effectiveType || "Unknown"}` : "Unknown"; - const info = [ - `> **Platform**: ${platform}`, - `> **Device Type**: ${deviceType}`, - `> **Browser**: ${browserInfo}`, - `> **CPU Cores**: ${hardwareConcurrency || "N/A"}`, - `> **Memory**: ${deviceMemory ? `${deviceMemory}GB` : "N/A"}`, - `> **Screen**: ${width}x${height} (${colorDepth}bit)`, - `> **Languages**: ${languages?.join(", ")}`, - `> **Network**: ${networkInfo} (${onLine ? "Online" : "Offline"})` - ].join("\n"); - return { content: info }; - } catch (err) { - sendBotMessage(ctx.channel.id, { content: "Failed to fetch system information" }); - } - }, - }, - { - name: "getUptime", - description: "Returns the system uptime", - execute: async (opts, ctx) => { - const uptime = performance.now() / 1000; - const uptimeInfo = `> **System Uptime**: ${Math.floor(uptime / 60)} minutes`; - return { content: uptimeInfo }; - }, - }, - { - name: "getTime", - description: "Returns the current server time", - execute: async (opts, ctx) => { - const currentTime = new Date().toLocaleString(); - return { content: `> **Current Time**: ${currentTime}` }; - }, - }, - { - name: "getLocation", - description: "Returns the user's approximate location based on IP", - execute: async (opts, ctx) => { - try { - const response = await fetch("https://ipapi.co/json/"); - const data = await response.json(); - const locationInfo = `> **Country**: ${data.country_name}\n> **Region**: ${data.region}\n> **City**: ${data.city}`; - return { content: locationInfo }; - } catch (err) { - sendBotMessage(ctx.channel.id, { content: "Failed to fetch location information" }); - } - }, - } - ] -}); diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index e46824b44..796a91dbf 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -121,14 +121,9 @@ export default definePlugin({ }, // Make the gap between each item smaller so our tab can fit. { - match: /className:\i\.tabBar/, - replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"' + match: /type:"top",/, + replace: '$&className:"vc-mutual-gdms-modal-v2-tab-bar",' }, - // Make the tab bar item text smaller so our tab can fit. - { - match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/, - replace: '$1"heading-sm/normal"' - } ] }, { @@ -209,5 +204,5 @@ export default definePlugin({ /> ); - }) + }, { noop: true }) }); diff --git a/src/plugins/mutualGroupDMs/style.css b/src/plugins/mutualGroupDMs/style.css index f0ad3c609..b6d992ad0 100644 --- a/src/plugins/mutualGroupDMs/style.css +++ b/src/plugins/mutualGroupDMs/style.css @@ -3,5 +3,5 @@ } .vc-mutual-gdms-modal-v2-tab-bar { - gap: 12px; + --space-xl: 16px; } diff --git a/src/plugins/noBlockedMessages/index.ts b/src/plugins/noBlockedMessages/index.ts index 0d19726cc..d4d7b3f95 100644 --- a/src/plugins/noBlockedMessages/index.ts +++ b/src/plugins/noBlockedMessages/index.ts @@ -33,7 +33,7 @@ interface MessageDeleteProps { } // Remove this migration once enough time has passed -migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages"); +migratePluginSetting("NoBlockedMessages", "ignoreMessages", "ignoreBlockedMessages"); const settings = definePluginSettings({ ignoreMessages: { description: "Completely ignores incoming messages from blocked and ignored (if enabled) users", diff --git a/src/plugins/noRPC.discordDesktop/index.ts b/src/plugins/noRPC.discordDesktop/index.ts deleted file mode 100644 index 4c6319e5e..000000000 --- a/src/plugins/noRPC.discordDesktop/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoRPC", - description: "Disables Discord's RPC server.", - authors: [Devs.Cyn], - patches: [ - { - find: '.ensureModule("discord_rpc")', - replacement: { - match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/, - replace: '.ensureModule("discord_rpc")}', - }, - }, - ], -}); diff --git a/src/plugins/noUnblockToJump/index.ts b/src/plugins/noUnblockToJump/index.ts index cb379bf8c..b36a2e24d 100644 --- a/src/plugins/noUnblockToJump/index.ts +++ b/src/plugins/noUnblockToJump/index.ts @@ -21,7 +21,7 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "NoUnblockToJump", - description: "Allows you to jump to messages of blocked users without unblocking them", + description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them", authors: [Devs.dzshn], patches: [ { diff --git a/src/plugins/pauseInvitesForever/index.tsx b/src/plugins/pauseInvitesForever/index.tsx index b648f92ee..432d1c1cd 100644 --- a/src/plugins/pauseInvitesForever/index.tsx +++ b/src/plugins/pauseInvitesForever/index.tsx @@ -75,5 +75,5 @@ export default definePlugin({ }}> Pause Indefinitely.
}
); - }) + }, { noop: true }) }); diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index 2ecae5171..afe0ad448 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -201,7 +201,7 @@ function toggleMessageDecorators(enabled: boolean) { } } -migratePluginSetting("PlatformIndicators", "badges", "profiles"); +migratePluginSetting("PlatformIndicators", "profiles", "badges"); const settings = definePluginSettings({ list: { type: OptionType.BOOLEAN, diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index f99c0be96..4a3dc0286 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -151,7 +151,13 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect : diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 3f4f524c4..294cdb583 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -299,7 +299,7 @@ export default definePlugin({ { find: '"MessageManager"', replacement: { - match: /"Skipping fetch because channelId is a static route"\);return}(?=.+?getChannel\((\i)\))/, + match: /forceFetch:\i,isPreload:.+?}=\i;(?=.+?getChannel\((\i)\))/, replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;` } }, @@ -526,10 +526,11 @@ export default definePlugin({ isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) { try { - if (!channel) return false; + if (channel == null || Object.hasOwn(channel, "channelId") && channel.channelId == null) return false; - if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId); - if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; + if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId); + if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false; + if (["browse", "customize", "guide"].includes(channel.id)) return false; return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel); } catch (e) { diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 5f45902e8..d8b64cf98 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -86,5 +86,5 @@ export default definePlugin({ )} ; - }) + }, { noop: true }) }); diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index aee652524..8966e8b1c 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -132,10 +132,16 @@ export default definePlugin({ { find: "Copy image not supported", - replacement: { - match: /(?<=(?:canSaveImage|canCopyImage)\(\i?\)\{.{0,50})!\i\.isPlatformEmbedded/g, - replace: "false" - } + replacement: [ + { + match: /(?<=(?:canSaveImage|canCopyImage)\(.{0,120}?)!\i\.isPlatformEmbedded/g, + replace: "false" + }, + { + match: /canCopyImage\(.+?(?=return"function"==typeof \i\.clipboard\.copyImage)/, + replace: "$&return true;" + } + ] }, // Add back Copy & Save Image { @@ -147,7 +153,7 @@ export default definePlugin({ replace: "false" }, { - match: /return\s*?\[.{0,50}?(?=\?.{0,100}?id:"copy-image")/, + match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/, replace: "return [true" }, { diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index e63cf5aae..f9796cd16 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -67,7 +67,7 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`; - const cache = reactions[key] ??= { fetched: false, users: {} }; + const cache = reactions[key] ??= { fetched: false, users: new Map() }; if (!cache.fetched) { queue.unshift(() => fetchReactions(msg, e, type)); cache.fetched = true; @@ -169,7 +169,7 @@ export default definePlugin({ }, [message.id, forceUpdate]); const reactions = getReactionsWithQueue(message, emoji, type); - const users = Object.values(reactions).filter(Boolean) as User[]; + const users = [...reactions.values()].filter(Boolean); return (
; + users: Map; } interface RootObject { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index fa32700bd..da9ac47c7 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -57,7 +57,7 @@ export interface Dev { */ export const Devs = /* #__PURE__*/ Object.freeze({ Ven: { - name: "Vee", + name: "V", id: 343383572805058560n }, Arjix: { @@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, axyie: { name: "'ax", - id: 273562710745284628n + id: 929877747151548487n, }, pointy: { name: "pointy", @@ -604,7 +604,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, samsam: { name: "samsam", - id: 836452332387565589n, + id: 400482410279469056n, + }, + Cootshk: { + name: "Cootshk", + id: 921605971577548820n }, } satisfies Record); @@ -1074,6 +1078,18 @@ export const EquicordDevs = Object.freeze({ name: "sliwka", id: 1165286199628419129n, }, + bbgaming25k: { + name: "bbgaming25k", + id: 851222385528274964n, + }, + davidkra230: { + name: "davidkra230", + id: 652699312631054356n, + }, + GroupXyz: { + name: "GroupXyz", + id: 950033410229944331n + }, } satisfies Record); // iife so #__PURE__ works correctly diff --git a/src/utils/cspViolations.ts b/src/utils/cspViolations.ts new file mode 100644 index 000000000..9477bcaac --- /dev/null +++ b/src/utils/cspViolations.ts @@ -0,0 +1,34 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useLayoutEffect } from "@webpack/common"; + +import { useForceUpdater } from "./react"; + +const cssRelevantDirectives = ["style-src", "img-src", "font-src"] as const; + +export const CspBlockedUrls = new Set(); +const CspErrorListeners = new Set<() => void>(); + +document.addEventListener("securitypolicyviolation", ({ effectiveDirective, blockedURI }) => { + if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return; + + CspBlockedUrls.add(blockedURI); + + CspErrorListeners.forEach(listener => listener()); +}); + +export function useCspErrors() { + const forceUpdate = useForceUpdater(); + + useLayoutEffect(() => { + CspErrorListeners.add(forceUpdate); + + return () => void CspErrorListeners.delete(forceUpdate); + }, [forceUpdate]); + + return [...CspBlockedUrls] as const; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 70ac9d424..3e7aa0e14 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -21,6 +21,7 @@ export * from "../shared/onceDefined"; export * from "./ChangeList"; export * from "./clipboard"; export * from "./constants"; +export * from "./cspViolations"; export * from "./discord"; export * from "./guards"; export * from "./intlHash"; diff --git a/src/utils/misc.ts b/src/utils/misc.ts index e8bfdf2a5..5c5848ccd 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -92,7 +92,10 @@ export function identity(value: T): T { export const isMobile = navigator.userAgent.includes("Mobi"); export const isPluginDev = (id: string) => Object.hasOwn(VencordDevsById, id); +export const shouldShowContributorBadge = (id: string) => isPluginDev(id) && VencordDevsById[id].badge !== false; + export const isEquicordPluginDev = (id: string) => Object.hasOwn(EquicordDevsById, id); +export const shouldShowEquicordContributorBadge = (id: string) => isEquicordPluginDev(id) && EquicordDevsById[id].badge !== false; export function pluralise(amount: number, singular: string, plural = singular + "s") { return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index de0c5ea89..a448f899d 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -141,7 +141,7 @@ export const UserUtils = { export const UploadManager = findByPropsLazy("clearAll", "addFile"); export const UploadHandler = { - promptToUpload: findByCodeLazy("#{intl::ATTACHMENT_TOO_MANY_ERROR_TITLE}") as (files: File[], channel: Channel, draftType: Number) => void + promptToUpload: findByCodeLazy("=!0,showLargeMessageDialog:") as (files: File[], channel: Channel, draftType: Number) => void }; export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", {