From b6cb11578cbe17c9eb923b759e68f4ad17b23585 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 11 Aug 2024 01:12:38 -0400 Subject: [PATCH] Update D3SOX Plugins Remove NotifyUserChanges as it enables a stalking like behavior --- README.md | 10 +- .../components/ActivityTooltip.tsx | 73 --- .../betterActivities/index.tsx | 434 +++++++++++++----- .../betterActivities/settings.tsx | 77 ---- .../betterActivities/styles.css | 48 +- src/equicordplugins/betterActivities/types.ts | 9 - src/equicordplugins/betterActivities/utils.ts | 158 ------- .../components/SpeedIcon.tsx | 21 + .../mediaPlaybackSpeed/index.tsx | 151 ++++++ .../components/NotificationsOffIcon.tsx | 11 - .../components/NotificationsOnIcon.tsx | 11 - .../notifyUserChanges/index.tsx | 344 -------------- .../serverProfilesToolbox/index.tsx | 190 ++++++++ .../silentTypingEnhanced/index.tsx | 177 +++++++ src/equicordplugins/voiceChatUtils/index.tsx | 6 +- 15 files changed, 868 insertions(+), 852 deletions(-) delete mode 100644 src/equicordplugins/betterActivities/components/ActivityTooltip.tsx delete mode 100644 src/equicordplugins/betterActivities/settings.tsx delete mode 100644 src/equicordplugins/betterActivities/utils.ts create mode 100644 src/equicordplugins/mediaPlaybackSpeed/components/SpeedIcon.tsx create mode 100644 src/equicordplugins/mediaPlaybackSpeed/index.tsx delete mode 100644 src/equicordplugins/notifyUserChanges/components/NotificationsOffIcon.tsx delete mode 100644 src/equicordplugins/notifyUserChanges/components/NotificationsOnIcon.tsx delete mode 100644 src/equicordplugins/notifyUserChanges/index.tsx create mode 100644 src/equicordplugins/serverProfilesToolbox/index.tsx create mode 100644 src/equicordplugins/silentTypingEnhanced/index.tsx diff --git a/README.md b/README.md index b0a6c57b..d951b030 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - Request for plugins from Discord.
-Extra included plugins (122 additional plugins) +Extra included plugins (124 additional plugins) - AllCallTimers by MaxHerbold and D3SOX - AltKrispSwitch by newwares @@ -86,6 +86,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - KeywordNotify by camila314 (maintained by thororen) - LoginWithQR by nexpid - MediaDownloader by Colorman +- MediaPlaybackSpeed by D3SOX - Meow by Samwich - MessageColors by Hen - MessageLinkTooltip by Kyuuhachi @@ -102,7 +103,6 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - NoNitroUpsell by thororen - NoRoleHeaders by Samwich - NotificationTitle by Kyuuhachi -- NotifyUserChanges by D3SOX - OnePingPerDM by ProffDea - PlatformSpoofer by Drag - PurgeMessages by bhop and nyx @@ -116,9 +116,11 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - ScreenRecorder by AutumnVN - SearchFix by Jaxx - SekaiStickers by MaiKokain +- ServerProfilesToolbox by D3SOX - ServerSearch by camila314 -- Shakespearean by vmohammad +- Shakespearean by vmohammad (Dev build only) - ShowBadgesInChat by Inbestigator and KrystalSkull +- SilentTypingEnhanced by Ven, Rini, D3SOX - Slap by Korbo - SoundBoardLogger by Moxxie, fres, echo (maintained by thororen) - SteamStatusSync by niko @@ -139,7 +141,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - VencordRPC by AutumnVN - VideoSpeed by Samwich - ViewRaw2 by Kyuuhachi -- VoiceChatUtilities by Dams and D3SOX +- VoiceChatUtilities by D3SOX - WebpackTarball by Kyuuhachi - WhosWatching by fres - WigglyText by nexpid diff --git a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx deleted file mode 100644 index cc5a03ea..00000000 --- a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import ErrorBoundary from "@components/ErrorBoundary"; -import { findComponentByCodeLazy } from "@webpack"; -import { moment, React, useMemo } from "@webpack/common"; -import { User } from "discord-types/general"; - -import { Activity, Application } from "../types"; -import { - formatElapsedTime, - getActivityImage, - getApplicationIcons, - getValidStartTimeStamp, - getValidTimestamps -} from "../utils"; - -const TimeBar = findComponentByCodeLazy<{ - start: number; - end: number; - themed: boolean; - className: string; -}>("isSingleLine"); - -interface ActivityTooltipProps { - activity: Activity; - application?: Application; - user: User; - cl: ReturnType; -} - -export default function ActivityTooltip({ activity, application, user, cl }: Readonly) { - const image = useMemo(() => { - const activityImage = getActivityImage(activity, application); - if (activityImage) { - return activityImage; - } - const icon = getApplicationIcons([activity], true)[0]; - return icon?.image.src; - }, [activity]); - const timestamps = useMemo(() => getValidTimestamps(activity), [activity]); - const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]); - - const hasDetails = activity.details ?? activity.state; - return ( - -
- {image && Activity logo} -
{activity.name}
- {hasDetails &&
} -
-
{activity.details}
-
{activity.state}
- {!timestamps && startTime && -
- {formatElapsedTime(moment(startTime / 1000), moment())} -
- } -
- {timestamps && ( - - )} -
- - ); -} diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 4aaa5a65..813e2fa9 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -1,51 +1,234 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ import "./styles.css"; -import { migratePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { PresenceStore, React, Tooltip, useEffect, useMemo, useState, useStateFromStores } from "@webpack/common"; import { User } from "discord-types/general"; -import ActivityTooltip from "./components/ActivityTooltip"; import { Caret } from "./components/Caret"; import { SpotifyIcon } from "./components/SpotifyIcon"; import { TwitchIcon } from "./components/TwitchIcon"; -import settings from "./settings"; -import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types"; -import { - getApplicationIcons -} from "./utils"; +import { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties } from "./types"; + +const settings = definePluginSettings({ + memberList: { + type: OptionType.BOOLEAN, + description: "Show activity icons in the member list", + default: true, + restartNeeded: true, + }, + iconSize: { + type: OptionType.SLIDER, + description: "Size of the activity icons", + markers: [10, 15, 20], + default: 15, + stickToMarkers: false, + }, + specialFirst: { + type: OptionType.BOOLEAN, + description: "Show special activities first (Currently Spotify and Twitch)", + default: true, + restartNeeded: false, + }, + renderGifs: { + type: OptionType.BOOLEAN, + description: "Allow rendering GIFs", + default: true, + restartNeeded: false, + }, + showAppDescriptions: { + type: OptionType.BOOLEAN, + description: "Show application descriptions in the activity tooltip", + default: true, + restartNeeded: false, + }, + divider: { + type: OptionType.COMPONENT, + description: "", + component: () => ( +
+ ), + }, + userPopout: { + type: OptionType.BOOLEAN, + description: "Show all activities in the profile popout/sidebar", + default: true, + restartNeeded: true, + }, + allActivitiesStyle: { + type: OptionType.SELECT, + description: "Style for showing all activities", + options: [ + { + default: true, + label: "Carousel", + value: "carousel", + }, + { + label: "List", + value: "list", + }, + ] + } +}); const cl = classNameFactory("vc-bactivities-"); -const ActivityView = findComponentByCodeLazy(",onOpenGameProfileModal:"); +const ApplicationStore: { + getApplication: (id: string) => Application | null; +} = findStoreLazy("ApplicationStore"); + +const { fetchApplication }: { + fetchApplication: (id: string) => Promise; +} = findByPropsLazy("fetchApplication"); + +const ActivityView = findComponentByCodeLazy<{ + activity: Activity | null; + user: User; + application?: Application; + type?: string; +}>(",onOpenGameProfileModal:"); // 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"); +const fetchedApplications = new Map(); + +const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"? + +const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => { + return ( + +
+ +
+
+ ); +}; + +function getActivityApplication({ application_id }: Activity) { + if (!application_id) return undefined; + let application = ApplicationStore.getApplication(application_id); + if (!application && fetchedApplications.has(application_id)) { + application = fetchedApplications.get(application_id) ?? null; + } + return application ?? undefined; +} + +function getApplicationIcons(activities: Activity[], preferSmall = false) { + const applicationIcons: ApplicationIcon[] = []; + const applications = activities.filter(activity => activity.application_id || activity.platform); + + for (const activity of applications) { + const { assets, application_id, platform } = activity; + if (!application_id && !platform) { + continue; + } + + if (assets) { + const addImage = (image: string, alt: string) => { + if (image.startsWith("mp:")) { + const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`; + if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) { + applicationIcons.push({ + image: { src: discordMediaLink, alt }, + activity + }); + } + } else { + const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`; + applicationIcons.push({ + image: { src, alt }, + activity + }); + } + }; + + const smallImage = assets.small_image; + const smallText = assets.small_text ?? "Small Text"; + const largeImage = assets.large_image; + const largeText = assets.large_text ?? "Large Text"; + if (preferSmall) { + if (smallImage) { + addImage(smallImage, smallText); + } else if (largeImage) { + addImage(largeImage, largeText); + } + } else { + if (largeImage) { + addImage(largeImage, largeText); + } else if (smallImage) { + addImage(smallImage, smallText); + } + } + } else if (application_id) { + let application = ApplicationStore.getApplication(application_id); + if (!application) { + if (fetchedApplications.has(application_id)) { + application = fetchedApplications.get(application_id) as Application | null; + } else { + fetchedApplications.set(application_id, null); + fetchApplication(application_id).then(app => { + fetchedApplications.set(application_id, app); + }).catch(console.error); + } + } + + if (application) { + if (application.icon) { + const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`; + applicationIcons.push({ + image: { src, alt: application.name }, + activity, + application + }); + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity, + application + }); + } + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity + }); + } + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity + }); + } + } + + return applicationIcons; +} migratePluginSettings("BetterActivities", "MemberListActivities"); + export default definePlugin({ name: "BetterActivities", description: "Shows activity icons in the member list and allows showing all activities", @@ -68,12 +251,7 @@ export default definePlugin({ for (const appIcon of uniqueIcons) { icons.push({ iconElement: , - tooltip: + tooltip: }); } } @@ -84,7 +262,7 @@ export default definePlugin({ const activity = activities[activityIndex]; const iconObject: ActivityListIcon = { iconElement: , - tooltip: + tooltip: }; if (settings.store.specialFirst) { @@ -131,8 +309,8 @@ export default definePlugin({ return null; }, - showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) { - const [currentActivity, setCurrentActivity] = React.useState( + showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) { + const [currentActivity, setCurrentActivity] = useState( activity?.type !== 4 ? activity! : null ); @@ -140,7 +318,7 @@ export default definePlugin({ [PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4) ) ?? []; - React.useEffect(() => { + useEffect(() => { if (!activities.length) { setCurrentActivity(null); return; @@ -148,75 +326,92 @@ export default definePlugin({ if (!currentActivity || !activities.includes(currentActivity)) setCurrentActivity(activities[0]); - }, [activities]); + // we use these for other activities, it would be better to somehow get the corresponding activity props + const generalProps = useMemo(() => Object.keys(props).reduce((acc, key) => { + // exclude activity specific props to prevent copying them to all activities (e.g. buttons) + if (key !== "renderActions" && key !== "application") acc[key] = props[key]; + return acc; + }, {}), [props]); + if (!activities.length) return null; if (settings.store.allActivitiesStyle === "carousel") { return (
- -
- {({ - onMouseEnter, - onMouseLeave - }) => { - return { - const index = activities.indexOf(currentActivity!); - if (index - 1 >= 0) - setCurrentActivity(activities[index - 1]); - }} - > - - ; - }} + {currentActivity?.id === activity.id ? ( + + ) : ( + + )} + {activities.length > 1 && +
+ {({ + onMouseEnter, + onMouseLeave + }) => { + return { + const index = activities.indexOf(currentActivity!); + if (index - 1 >= 0) + setCurrentActivity(activities[index - 1]); + }} + > + + ; + }} -
- {activities.map((activity, index) => ( -
setCurrentActivity(activity)} - className={`dot ${currentActivity === activity ? "selected" : ""}`} /> - ))} +
+ {activities.map((activity, index) => ( +
setCurrentActivity(activity)} + className={`dot ${currentActivity === activity ? "selected" : ""}`} /> + ))} +
+ + {({ + onMouseEnter, + onMouseLeave + }) => { + return { + const index = activities.indexOf(currentActivity!); + if (index + 1 < activities.length) + setCurrentActivity(activities[index + 1]); + }} + > + = activities.length - 1} + direction="right" /> + ; + }}
- - {({ - onMouseEnter, - onMouseLeave - }) => { - return { - const index = activities.indexOf(currentActivity!); - if (index + 1 < activities.length) - setCurrentActivity(activities[index + 1]); - }} - > - = activities.length - 1} - direction="right" /> - ; - }} -
+ }
); } else { @@ -228,16 +423,22 @@ export default definePlugin({ gap: "5px", }} > - {activities.map((activity, index) => ( - - ))} + {activities.map((activity, index) => + index === 0 ? ( + ) : ( + + ))}
); } @@ -254,22 +455,13 @@ export default definePlugin({ predicate: () => settings.store.memberList, }, { - // Show all activities in the profile panel - find: "{layout:\"DM_PANEL\",", + // Show all activities in the user popout/sidebar + find: '"UserActivityContainer"', replacement: { - match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:.+?,user:\i,channelId:\i.id,)/, - replace: "$self.showAllActivitiesComponent" - }, - predicate: () => settings.store.profileSidebar, - }, - { - // Show all activities in the user popout - find: "customStatusSection,", - replacement: { - match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:\i,user:\i,guild:\i,channelId:\i,onClose:\i,)/, - replace: "$self.showAllActivitiesComponent" + match: /(?<=\(0,\i\.jsx\)\()(\i\.\i)(?=,{...(\i),activity:\i,user:\i,application:\i)/, + replace: "$2.type==='BiteSizePopout'?$self.showAllActivitiesComponent:$1" }, predicate: () => settings.store.userPopout - } + }, ], }); diff --git a/src/equicordplugins/betterActivities/settings.tsx b/src/equicordplugins/betterActivities/settings.tsx deleted file mode 100644 index 98f1b06e..00000000 --- a/src/equicordplugins/betterActivities/settings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; - -const settings = definePluginSettings({ - memberList: { - type: OptionType.BOOLEAN, - description: "Show activity icons in the member list", - default: true, - restartNeeded: true, - }, - iconSize: { - type: OptionType.SLIDER, - description: "Size of the activity icons", - markers: [10, 15, 20], - default: 15, - stickToMarkers: false, - }, - specialFirst: { - type: OptionType.BOOLEAN, - description: "Show special activities first (Currently Spotify and Twitch)", - default: true, - }, - renderGifs: { - type: OptionType.BOOLEAN, - description: "Allow rendering GIFs", - default: true, - }, - divider: { - type: OptionType.COMPONENT, - description: "", - component: () => ( -
- ), - }, - profileSidebar: { - type: OptionType.BOOLEAN, - description: "Show all activities in the profile sidebar", - default: true, - restartNeeded: true, - }, - userPopout: { - type: OptionType.BOOLEAN, - description: "Show all activities in the user popout", - default: true, - restartNeeded: true, - }, - allActivitiesStyle: { - type: OptionType.SELECT, - description: "Style for showing all activities", - options: [ - { - default: true, - label: "Carousel", - value: "carousel", - }, - { - label: "List", - value: "list", - }, - ] - } -}); - -export default settings; diff --git a/src/equicordplugins/betterActivities/styles.css b/src/equicordplugins/betterActivities/styles.css index 1990c173..1ab6d3ad 100644 --- a/src/equicordplugins/betterActivities/styles.css +++ b/src/equicordplugins/betterActivities/styles.css @@ -19,44 +19,8 @@ border-radius: 50%; } -.vc-bactivities-activity { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 5px; -} - -.vc-bactivities-activity-title { - font-weight: bold; - text-align: center; -} - -.vc-bactivities-activity-image { - height: 20px; - width: 20px; - border-radius: 50%; - object-fit: cover; -} - -.vc-bactivities-activity-divider { - width: 100%; - border-top: 1px dotted rgb(255 255 255 / 20%); - margin-top: 3px; - margin-bottom: 3px; -} - -.vc-bactivities-activity-details { - display: flex; - flex-direction: column; - color: var(--text-muted); - word-break: break-word; -} - -.vc-bactivities-activity-time-bar { - width: 100%; - margin-top: 3px; - margin-bottom: 3px; +.vc-bactivities-activity-tooltip { + padding: 1px; } .vc-bactivities-caret-left, @@ -101,12 +65,12 @@ background: var(--background-modifier-accent); } -.vc-bactivities-controls .carousell { +.vc-bactivities-controls .carousel { display: flex; align-items: center; } -.vc-bactivities-controls .carousell .dot { +.vc-bactivities-controls .carousel .dot { margin: 0 4px; width: 10px; cursor: pointer; @@ -117,11 +81,11 @@ opacity: 0.6; } -.vc-bactivities-controls .carousell .dot:hover:not(.selected) { +.vc-bactivities-controls .carousel .dot:hover:not(.selected) { opacity: 1; } -.vc-bactivities-controls .carousell .dot.selected { +.vc-bactivities-controls .carousel .dot.selected { opacity: 1; background: var(--dot-color, var(--brand-500)); } diff --git a/src/equicordplugins/betterActivities/types.ts b/src/equicordplugins/betterActivities/types.ts index 6ca4b533..e11896c8 100644 --- a/src/equicordplugins/betterActivities/types.ts +++ b/src/equicordplugins/betterActivities/types.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Guild, User } from "discord-types/general"; import { CSSProperties, ImgHTMLAttributes } from "react"; export interface Timestamp { @@ -81,11 +80,3 @@ export interface ActivityListIcon { export interface IconCSSProperties extends CSSProperties { "--icon-size": string; } - -export interface ActivityViewProps { - activity: Activity | null; - user: User; - guild: Guild; - channelId: string; - onClose: () => void; -} diff --git a/src/equicordplugins/betterActivities/utils.ts b/src/equicordplugins/betterActivities/utils.ts deleted file mode 100644 index d1511acc..00000000 --- a/src/equicordplugins/betterActivities/utils.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { moment } from "@webpack/common"; - -import settings from "./settings"; -import { Activity, Application, ApplicationIcon, Timestamp } from "./types"; - -const ApplicationStore: { - getApplication: (id: string) => Application | null; -} = findStoreLazy("ApplicationStore"); - -const { fetchApplication }: { - fetchApplication: (id: string) => Promise; -} = findByPropsLazy("fetchApplication"); - -export function getActivityImage(activity: Activity, application?: Application): string | undefined { - if (activity.type === 2 && activity.name === "Spotify") { - // get either from large or small image - const image = activity.assets?.large_image ?? activity.assets?.small_image; - // image needs to replace 'spotify:' - if (image?.startsWith("spotify:")) { - // spotify cover art is always https://i.scdn.co/image/ID - return image.replace("spotify:", "https://i.scdn.co/image/"); - } - } - if (activity.type === 1 && activity.name === "Twitch") { - const image = activity.assets?.large_image; - // image needs to replace 'twitch:' - if (image?.startsWith("twitch:")) { - // twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg - return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`; - } - } - // TODO: we could support other assets here -} - -const fetchedApplications = new Map(); - -// TODO: replace with "renderXboxImage"? -const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; - -export function getApplicationIcons(activities: Activity[], preferSmall = false) { - const applicationIcons: ApplicationIcon[] = []; - const applications = activities.filter(activity => activity.application_id || activity.platform); - - for (const activity of applications) { - const { assets, application_id, platform } = activity; - if (!application_id && !platform) { - continue; - } - if (assets) { - - const addImage = (image: string, alt: string) => { - if (image.startsWith("mp:")) { - const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`; - if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) { - applicationIcons.push({ - image: { src: discordMediaLink, alt }, - activity - }); - } - } else { - const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`; - applicationIcons.push({ - image: { src, alt }, - activity - }); - } - }; - - const smallImage = assets.small_image; - const smallText = assets.small_text ?? "Small Text"; - const largeImage = assets.large_image; - const largeText = assets.large_text ?? "Large Text"; - if (preferSmall) { - if (smallImage) { - addImage(smallImage, smallText); - } else if (largeImage) { - addImage(largeImage, largeText); - } - } else { - if (largeImage) { - addImage(largeImage, largeText); - } else if (smallImage) { - addImage(smallImage, smallText); - } - } - } else if (application_id) { - let application = ApplicationStore.getApplication(application_id); - if (!application) { - if (fetchedApplications.has(application_id)) { - application = fetchedApplications.get(application_id) as Application | null; - } else { - fetchedApplications.set(application_id, null); - fetchApplication(application_id).then(app => { - fetchedApplications.set(application_id, app); - }); - } - } - - if (application) { - if (application.icon) { - const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`; - applicationIcons.push({ - image: { src, alt: application.name }, - activity, - application - }); - } else if (platform === "xbox") { - applicationIcons.push({ - image: { src: xboxUrl, alt: "Xbox" }, - activity, - application - }); - } - } - } else { - if (platform === "xbox") { - applicationIcons.push({ - image: { src: xboxUrl, alt: "Xbox" }, - activity - }); - } - } - } - - return applicationIcons; -} - -export function getValidTimestamps(activity: Activity): Required | null { - if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) { - return activity.timestamps as Required; - } - return null; -} - -export function getValidStartTimeStamp(activity: Activity): number | null { - if (activity.timestamps?.start !== undefined) { - return activity.timestamps.start; - } - return null; -} - -const customFormat = (momentObj: moment.Moment): string => { - const hours = momentObj.hours(); - const formattedTime = momentObj.format("mm:ss"); - return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime; -}; - -export function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string { - const duration = moment.duration(endTime.diff(startTime)); - return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`; -} diff --git a/src/equicordplugins/mediaPlaybackSpeed/components/SpeedIcon.tsx b/src/equicordplugins/mediaPlaybackSpeed/components/SpeedIcon.tsx new file mode 100644 index 00000000..c6d4a907 --- /dev/null +++ b/src/equicordplugins/mediaPlaybackSpeed/components/SpeedIcon.tsx @@ -0,0 +1,21 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +function SpeedIcon() { + return ( + + + + ); +} + +export default SpeedIcon; diff --git a/src/equicordplugins/mediaPlaybackSpeed/index.tsx b/src/equicordplugins/mediaPlaybackSpeed/index.tsx new file mode 100644 index 00000000..d7001eff --- /dev/null +++ b/src/equicordplugins/mediaPlaybackSpeed/index.tsx @@ -0,0 +1,151 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { makeRange } from "@components/PluginSettings/components"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ContextMenuApi, FluxDispatcher, Heading, Menu, React, Tooltip, useEffect } from "@webpack/common"; +import { RefObject } from "react"; + +import SpeedIcon from "./components/SpeedIcon"; + +const cl = classNameFactory("vc-media-playback-speed-"); + +const min = 0.25; +const max = 3.5; +const speeds = makeRange(min, max, 0.25); + +const settings = definePluginSettings({ + test: { + type: OptionType.COMPONENT, + description: "", + component() { + return + Default playback speeds + ; + } + }, + defaultVoiceMessageSpeed: { + type: OptionType.SLIDER, + default: 1, + description: "Voice messages", + markers: speeds, + }, + defaultVideoSpeed: { + type: OptionType.SLIDER, + default: 1, + description: "Videos", + markers: speeds, + }, + defaultAudioSpeed: { + type: OptionType.SLIDER, + default: 1, + description: "Audios", + markers: speeds, + }, +}); + +type MediaRef = RefObject | undefined; + +export default definePlugin({ + name: "MediaPlaybackSpeed", + description: "Allows changing the (default) playback speed of media embeds", + authors: [Devs.D3SOX], + + settings, + + PlaybackSpeedComponent({ mediaRef }: { mediaRef: MediaRef; }) { + const changeSpeed = (speed: number) => { + const media = mediaRef?.current; + if (media) { + media.playbackRate = speed; + } + }; + + useEffect(() => { + if (!mediaRef?.current) return; + const media = mediaRef.current; + if (media.tagName === "AUDIO") { + const isVoiceMessage = media.className.includes("audioElement_"); + changeSpeed(isVoiceMessage ? settings.store.defaultVoiceMessageSpeed : settings.store.defaultAudioSpeed); + } else if (media.tagName === "VIDEO") { + changeSpeed(settings.store.defaultVideoSpeed); + } + }, [mediaRef]); + + return ( + + {tooltipProps => ( + + )} + + ); + }, + + renderComponent(mediaRef: MediaRef) { + return + + ; + }, + + patches: [ + // voice message embeds + { + find: "\"--:--\"", + replacement: { + match: /onVolumeShow:\i,onVolumeHide:\i\}\)(?<=useCallback\(\(\)=>\{let \i=(\i).current;.+?)/, + replace: "$&,$self.renderComponent($1)" + } + }, + // audio & video embeds + { + // need to pass media ref via props to make it easily accessible from inside controls + find: "renderControls(){", + replacement: { + match: /onToggleMuted:this.toggleMuted,/, + replace: "$&mediaRef:this.mediaRef," + } + }, + { + find: "AUDIO:\"AUDIO\"", + replacement: { + match: /onVolumeHide:\i,iconClassName:\i.controlIcon,iconColor:"currentColor",sliderWrapperClassName:\i.volumeSliderWrapper\}\)\}\),/, + replace: "$&$self.renderComponent(this.props.mediaRef)," + } + } + ] +}); diff --git a/src/equicordplugins/notifyUserChanges/components/NotificationsOffIcon.tsx b/src/equicordplugins/notifyUserChanges/components/NotificationsOffIcon.tsx deleted file mode 100644 index a2261bee..00000000 --- a/src/equicordplugins/notifyUserChanges/components/NotificationsOffIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import type { SVGProps } from "react"; - -export function NotificationsOffIcon(props: SVGProps) { - return (); -} diff --git a/src/equicordplugins/notifyUserChanges/components/NotificationsOnIcon.tsx b/src/equicordplugins/notifyUserChanges/components/NotificationsOnIcon.tsx deleted file mode 100644 index 8ce2818f..00000000 --- a/src/equicordplugins/notifyUserChanges/components/NotificationsOnIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import type { SVGProps } from "react"; - -export function NotificationsOnIcon(props: SVGProps) { - return (); -} diff --git a/src/equicordplugins/notifyUserChanges/index.tsx b/src/equicordplugins/notifyUserChanges/index.tsx deleted file mode 100644 index 170e41bf..00000000 --- a/src/equicordplugins/notifyUserChanges/index.tsx +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { showNotification } from "@api/Notifications"; -import { definePluginSettings, Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Menu, PresenceStore, React, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common"; -import type { Channel, User } from "discord-types/general"; -import { CSSProperties } from "react"; - -import { NotificationsOffIcon } from "./components/NotificationsOffIcon"; -import { NotificationsOnIcon } from "./components/NotificationsOnIcon"; - -interface PresenceUpdate { - user: { - id: string; - username?: string; - global_name?: string; - }; - clientStatus: { - desktop?: string; - web?: string; - mobile?: string; - console?: string; - }; - guildId?: string; - status: string; - broadcast?: any; // what's this? - activities: Array<{ - session_id: string; - created_at: number; - id: string; - name: string; - details?: string; - type: number; - }>; -} - -interface VoiceState { - userId: string; - channelId?: string; - oldChannelId?: string; - deaf: boolean; - mute: boolean; - selfDeaf: boolean; - selfMute: boolean; - selfStream: boolean; - selfVideo: boolean; - sessionId: string; - suppress: boolean; - requestToSpeakTimestamp: string | null; -} - -function shouldBeNative() { - if (typeof Notification === "undefined") return false; - - const { useNative } = Settings.notifications; - if (useNative === "always") return true; - if (useNative === "not-focused") return !document.hasFocus(); - return false; -} - -const SessionsStore = findStoreLazy("SessionsStore"); - -const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes"); - -function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { - return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => ( - - {(tooltipProps: any) => ( - - - - )} - - ); -} - -const Icons = { - desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"), - web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"), - mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }), - console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }), -}; -type Platform = keyof typeof Icons; - -const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => { - const tooltip = platform[0].toUpperCase() + platform.slice(1); - const Icon = Icons[platform] ?? Icons.desktop; - - return ; -}; - -interface PlatformIndicatorProps { - user: User; - wantMargin?: boolean; - wantTopMargin?: boolean; - small?: boolean; - style?: CSSProperties; -} - -const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false, style = {} }: PlatformIndicatorProps) => { - if (!user || user.bot) return null; - - if (user.id === UserStore.getCurrentUser().id) { - const sessions = SessionsStore.getSessions(); - if (typeof sessions !== "object") return null; - const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => { - if (a === b) return 0; - if (a === "online") return 1; - if (b === "online") return -1; - if (a === "idle") return 1; - if (b === "idle") return -1; - return 0; - }); - - const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => { - if (curr.clientInfo.client !== "unknown") - acc[curr.clientInfo.client] = curr.status; - return acc; - }, {}); - - const { clientStatuses } = PresenceStore.getState(); - clientStatuses[UserStore.getCurrentUser().id] = ownStatus; - } - - const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record; - if (!status) return null; - - const icons = Object.entries(status).map(([platform, status]) => ( - - )); - - if (!icons.length) return null; - - return ( - - {icons} - - ); -}; - -export const settings = definePluginSettings({ - notifyStatus: { - type: OptionType.BOOLEAN, - description: "Notify on status changes", - restartNeeded: false, - default: true, - }, - notifyVoice: { - type: OptionType.BOOLEAN, - description: "Notify on voice channel changes", - restartNeeded: false, - default: false, - }, - persistNotifications: { - type: OptionType.BOOLEAN, - description: "Persist notifications", - restartNeeded: false, - default: false, - }, - userIds: { - type: OptionType.STRING, - description: "User IDs (comma separated)", - restartNeeded: false, - default: "", - } -}); - -function getUserIdList() { - try { - return settings.store.userIds.split(",").filter(Boolean); - } catch (e) { - settings.store.userIds = ""; - return []; - } -} - -// show rich body with user avatar -const getRichBody = (user: User, text: string | React.ReactNode) =>
-
- {`${user.username}'s - -
- {text} -
; - -function triggerVoiceNotification(userId: string, userChannelId: string | null) { - const user = UserStore.getUser(userId); - const myChanId = SelectedChannelStore.getVoiceChannelId(); - - const name = user.username; - - const title = shouldBeNative() ? `User ${name} changed voice status` : "User voice status change"; - if (userChannelId) { - if (userChannelId !== myChanId) { - showNotification({ - title, - body: "joined a new voice channel", - noPersist: !settings.store.persistNotifications, - richBody: getRichBody(user, `${name} joined a new voice channel`), - }); - } - } else { - showNotification({ - title, - body: "left their voice channel", - noPersist: !settings.store.persistNotifications, - richBody: getRichBody(user, `${name} left their voice channel`), - }); - } -} - -function toggleUserNotify(userId: string) { - const userIds = getUserIdList(); - if (userIds.includes(userId)) { - userIds.splice(userIds.indexOf(userId), 1); - } else { - userIds.push(userId); - } - settings.store.userIds = userIds.join(","); -} - -interface UserContextProps { - channel?: Channel; - guildId?: string; - user: User; -} - -const UserContext: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { - if (!user || user.id === UserStore.getCurrentUser().id) return; - const isNotifyOn = getUserIdList().includes(user.id); - const label = isNotifyOn ? "Don't notify on changes" : "Notify on changes"; - const icon = isNotifyOn ? NotificationsOffIcon : NotificationsOnIcon; - - children.splice(-1, 0, ( - - toggleUserNotify(user.id)} - icon={icon} - /> - - )); -}; - -const lastStatuses = new Map(); - -export default definePlugin({ - name: "NotifyUserChanges", - description: "Adds a notify option in the user context menu to get notified when a user changes voice channels or online status", - authors: [Devs.D3SOX], - - settings, - - contextMenus: { - "user-context": UserContext - }, - - flux: { - VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - if (!settings.store.notifyVoice || !settings.store.userIds) { - return; - } - for (const { userId, channelId, oldChannelId } of voiceStates) { - if (channelId !== oldChannelId) { - const isFollowed = getUserIdList().includes(userId); - if (!isFollowed) { - continue; - } - - if (channelId) { - // move or join new channel - triggerVoiceNotification(userId, channelId); - } else if (oldChannelId) { - // leave - triggerVoiceNotification(userId, null); - } - } - } - }, - PRESENCE_UPDATES({ updates }: { updates: PresenceUpdate[]; }) { - if (!settings.store.notifyStatus || !settings.store.userIds) { - return; - } - for (const { user: { id: userId, username }, status } of updates) { - const isFollowed = getUserIdList().includes(userId); - if (!isFollowed) { - continue; - } - - // this is also triggered for multiple guilds and when only the activities change, so we have to check if the status actually changed - if (lastStatuses.has(userId) && lastStatuses.get(userId) !== status) { - const user = UserStore.getUser(userId); - const name = username ?? user.username; - - showNotification({ - title: shouldBeNative() ? `User ${name} changed status` : "User status change", - body: `is now ${status}`, - noPersist: !settings.store.persistNotifications, - richBody: getRichBody(user, `${name}'s status is now ${status}`), - }); - } - lastStatuses.set(userId, status); - } - } - }, - -}); diff --git a/src/equicordplugins/serverProfilesToolbox/index.tsx b/src/equicordplugins/serverProfilesToolbox/index.tsx new file mode 100644 index 00000000..1c9c87c3 --- /dev/null +++ b/src/equicordplugins/serverProfilesToolbox/index.tsx @@ -0,0 +1,190 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { + Button, + Clipboard, + GuildMemberStore, + Text, + Toasts, + UserProfileStore, + UserStore +} from "@webpack/common"; +import { GuildMember } from "discord-types/general"; + +const SummaryItem = findComponentByCodeLazy("borderType", "showBorder", "hideDivider"); + +interface SavedProfile { + nick: string | null; + pronouns: string | null; + bio: string | null; + themeColors: number[] | undefined; + banner: string | undefined; + avatar: string | undefined; + profileEffectId: string | undefined; + avatarDecoration: string | undefined; +} + +const savedProfile: SavedProfile = { + nick: null, + pronouns: null, + bio: null, + themeColors: undefined, + banner: undefined, + avatar: undefined, + profileEffectId: undefined, + avatarDecoration: undefined, +}; + +const { + setPendingAvatar, + setPendingBanner, + setPendingBio, + setPendingNickname, + setPendingPronouns, + setPendingThemeColors, + setPendingProfileEffectId, + setPendingAvatarDecoration, +}: { + setPendingAvatar: (a: string | undefined) => void; + setPendingBanner: (a: string | undefined) => void; + setPendingBio: (a: string | null) => void; + setPendingNickname: (a: string | null) => void; + setPendingPronouns: (a: string | null) => void; + setPendingThemeColors: (a: number[] | undefined) => void; + setPendingProfileEffectId: (a: string | undefined) => void; + setPendingAvatarDecoration: (a: string | undefined) => void; +} = findByPropsLazy("setPendingNickname", "setPendingPronouns"); + +export default definePlugin({ + name: "ServerProfilesToolbox", + authors: [Devs.D3SOX], + description: "Adds a copy/paste/reset button to the server profiles editor", + + patchServerProfiles({ guildId }: { guildId: string; }) { + const currentUser = UserStore.getCurrentUser(); + const premiumType = currentUser.premiumType ?? 0; + + const copy = () => { + const profile = UserProfileStore.getGuildMemberProfile(currentUser.id, guildId); + const nick = GuildMemberStore.getNick(guildId, currentUser.id); + const selfMember = GuildMemberStore.getMember(guildId, currentUser.id) as GuildMember & { avatarDecoration: string | undefined; }; + savedProfile.nick = nick ?? ""; + savedProfile.pronouns = profile.pronouns; + savedProfile.bio = profile.bio; + savedProfile.themeColors = profile.themeColors; + savedProfile.banner = profile.banner; + savedProfile.avatar = selfMember.avatar; + savedProfile.profileEffectId = profile.profileEffectId; + savedProfile.avatarDecoration = selfMember.avatarDecoration; + }; + + const paste = () => { + setPendingNickname(savedProfile.nick); + setPendingPronouns(savedProfile.pronouns); + if (premiumType === 2) { + setPendingBio(savedProfile.bio); + setPendingThemeColors(savedProfile.themeColors); + setPendingBanner(savedProfile.banner); + setPendingAvatar(savedProfile.avatar); + setPendingProfileEffectId(savedProfile.profileEffectId); + setPendingAvatarDecoration(savedProfile.avatarDecoration); + } + }; + + const reset = () => { + setPendingNickname(null); + setPendingPronouns(""); + if (premiumType === 2) { + setPendingBio(null); + setPendingThemeColors([]); + setPendingBanner(undefined); + setPendingAvatar(undefined); + setPendingProfileEffectId(undefined); + setPendingAvatarDecoration(undefined); + } + }; + + const copyToClipboard = () => { + copy(); + Clipboard.copy(JSON.stringify(savedProfile)); + }; + + const pasteFromClipboard = async () => { + try { + const clip = await navigator.clipboard.readText(); + if (!clip) { + Toasts.show({ + message: "Clipboard is empty", + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + }); + return; + } + const clipboardProfile: SavedProfile = JSON.parse(clip); + + if (!("nick" in clipboardProfile)) { + Toasts.show({ + message: "Data is not in correct format", + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + }); + return; + } + + Object.assign(savedProfile, JSON.parse(clip)); + paste(); + } catch (e) { + Toasts.show({ + message: `Failed to read clipboard data: ${e}`, + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + }); + } + }; + + return +
+ + Use the following buttons to mange the currently selected server + +
+ + + +
+
+ + +
+
+
; + }, + + patches: [ + { + find: ".PROFILE_CUSTOMIZATION_GUILD_SELECT_TITLE", + replacement: { + match: /return\(0(.{10,350})\}\)\}\)\}/, + replace: "return [(0$1})}),$self.patchServerProfiles(e)]}" + } + } + ], + +}); diff --git a/src/equicordplugins/silentTypingEnhanced/index.tsx b/src/equicordplugins/silentTypingEnhanced/index.tsx new file mode 100644 index 00000000..309aa5cf --- /dev/null +++ b/src/equicordplugins/silentTypingEnhanced/index.tsx @@ -0,0 +1,177 @@ +/* + * 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 { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; +import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, FluxDispatcher, React } from "@webpack/common"; + +const settings = definePluginSettings({ + showIcon: { + type: OptionType.BOOLEAN, + default: true, + description: "Show an icon for toggling the plugin", + restartNeeded: true, + }, + isEnabled: { + type: OptionType.BOOLEAN, + description: "Toggle functionality", + default: true, + }, + specificChats: { + type: OptionType.BOOLEAN, + default: false, + description: "Disable silent typing for specific chats instead (use icon to toggle)", + restartNeeded: false, + }, + disabledFor: { + type: OptionType.STRING, + description: "Disable functionality for these chats (comma separated list of guild or user IDs)", + default: "", + }, +}); + +const SilentTypingToggle: ChatBarButton = ({ isMainChat, channel }) => { + const { isEnabled, showIcon, specificChats, disabledFor } = settings.use(["isEnabled", "showIcon", "specificChats", "disabledFor"]); + const id = channel.guild_id ?? channel.id; + + const toggleGlobal = () => { + settings.store.isEnabled = !settings.store.isEnabled; + }; + const toggle = () => { + if (specificChats) { + if (!settings.store.isEnabled) { + toggleGlobal(); + } else { + const disabledChannels = getDisabledChannelsList(disabledFor); + if (disabledChannels.includes(id)) { + disabledChannels.splice(disabledChannels.indexOf(id), 1); + } else { + disabledChannels.push(id); + } + settings.store.disabledFor = disabledChannels.join(", "); + } + } else { + toggleGlobal(); + } + }; + const shouldEnable = isEnabled && (!specificChats || !getDisabledChannelsList(disabledFor).includes(id)); + + let tooltip = shouldEnable ? "Disable Silent Typing" : "Enable Silent Typing"; + if (specificChats) { + if (!isEnabled) { + tooltip = "Re-enable Silent Typing globally"; + } else { + const chatType = channel.guild_id ? "guild" : "user"; + tooltip = shouldEnable ? `Disable Silent Typing for current ${chatType} (right-click to toggle globally)` + : `Enable Silent Typing for current ${chatType} (right-click to toggle globally)`; + } + } + + if (!isMainChat || !showIcon) return null; + + return ( + + + + {shouldEnable && + } + {specificChats && !settings.store.isEnabled && + + } + + + ); +}; + +function getDisabledChannelsList(list = settings.store.disabledFor) { + try { + return list.split(",").map(x => x.trim()).filter(Boolean); + } catch (e) { + settings.store.disabledFor = ""; + return []; + } +} + +function isEnabled(channelId: string) { + if (!settings.store.isEnabled) return false; + if (settings.store.specificChats) { + // need to resolve guild id for guild channels + const guildId = ChannelStore.getChannel(channelId)?.guild_id; + return !getDisabledChannelsList().includes(guildId ?? channelId); + } + return true; +} + +export default definePlugin({ + name: "SilentTypingEnhanced", + authors: [Devs.Ven, Devs.Rini, Devs.D3SOX], + description: "Hide that you are typing", + dependencies: ["CommandsAPI", "ChatInputButtonAPI"], + settings, + + patches: [ + { + find: '.dispatch({type:"TYPING_START_LOCAL"', + replacement: { + match: /startTyping\(\i\){.+?},stop/, + replace: "startTyping:$self.startTyping,stop" + } + }, + ], + + commands: [{ + name: "silenttype", + description: "Toggle whether you're hiding that you're typing or not.", + inputType: ApplicationCommandInputType.BUILT_IN, + options: [ + { + name: "value", + description: "whether to hide or not that you're typing (default is toggle)", + required: false, + type: ApplicationCommandOptionType.BOOLEAN, + }, + ], + execute: async (args, ctx) => { + settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); + sendBotMessage(ctx.channel.id, { + content: settings.store.isEnabled ? "Silent typing enabled!" : "Silent typing disabled!", + }); + }, + }], + + async startTyping(channelId: string) { + if (isEnabled(channelId)) return; + FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); + }, + + start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + stop: () => removeChatBarButton("SilentTyping"), +}); diff --git a/src/equicordplugins/voiceChatUtils/index.tsx b/src/equicordplugins/voiceChatUtils/index.tsx index e40d79c3..ec232cf6 100644 --- a/src/equicordplugins/voiceChatUtils/index.tsx +++ b/src/equicordplugins/voiceChatUtils/index.tsx @@ -7,7 +7,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; -import { Devs, EquicordDevs } from "@utils/constants"; +import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common"; @@ -159,8 +159,10 @@ const settings = definePluginSettings({ export default definePlugin({ name: "VoiceChatUtilities", description: "This plugin allows you to perform multiple actions on an entire channel (move, mute, disconnect, etc.) (originally by dutake)", - authors: [EquicordDevs.Dams, Devs.D3SOX], + authors: [Devs.D3SOX], + settings, + contextMenus: { "channel-context": VoiceChannelContext },