From 81c71eb06a72ba9b4dbd38d7780524a79a94007d Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 2 Apr 2025 07:23:12 -0400 Subject: [PATCH] Update BetterActivities --- .../components/ActivityTooltip.tsx | 28 ++ .../components/CarouselControls.tsx | 77 +++ .../betterActivities/index.tsx | 460 +----------------- .../patch-helpers/activityList.tsx | 94 ++++ .../betterActivities/patch-helpers/popout.tsx | 111 +++++ .../betterActivities/settings.tsx | 71 +++ .../betterActivities/styles.css | 6 +- src/equicordplugins/betterActivities/types.ts | 36 +- .../betterActivities/utils.tsx | 127 +++++ 9 files changed, 556 insertions(+), 454 deletions(-) create mode 100644 src/equicordplugins/betterActivities/components/ActivityTooltip.tsx create mode 100644 src/equicordplugins/betterActivities/components/CarouselControls.tsx create mode 100644 src/equicordplugins/betterActivities/patch-helpers/activityList.tsx create mode 100644 src/equicordplugins/betterActivities/patch-helpers/popout.tsx create mode 100644 src/equicordplugins/betterActivities/settings.tsx create mode 100644 src/equicordplugins/betterActivities/utils.tsx diff --git a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx new file mode 100644 index 00000000..002f7e31 --- /dev/null +++ b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx @@ -0,0 +1,28 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { UserStore } from "@webpack/common"; + +import { ActivityTooltipProps } from "../types"; +import { ActivityView, cl } from "../utils"; + +export function ActivityTooltip({ activity, application, user }: Readonly) { + const currentUser = UserStore.getCurrentUser(); + if (!currentUser) return null; + return ( + +
+ +
+
+ ); +} diff --git a/src/equicordplugins/betterActivities/components/CarouselControls.tsx b/src/equicordplugins/betterActivities/components/CarouselControls.tsx new file mode 100644 index 00000000..eb7d43ae --- /dev/null +++ b/src/equicordplugins/betterActivities/components/CarouselControls.tsx @@ -0,0 +1,77 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { React, Tooltip } from "@webpack/common"; + +import { CarouselControlsProps } from "../types"; +import { cl } from "../utils"; +import { Caret } from "./Caret"; + +export function CarouselControls({ activities, currentActivity, onActivityChange }: CarouselControlsProps) { + const currentIndex = activities.indexOf(currentActivity); + + return ( +
+ {({ + onMouseEnter, + onMouseLeave + }) => { + return { + if (currentIndex - 1 >= 0) { + onActivityChange(activities[currentIndex - 1]); + } else { + onActivityChange(activities[activities.length - 1]); + } + }} + > + + ; + }} + +
+ {activities.map((activity, index) => ( +
onActivityChange(activity)} + className={`dot ${currentActivity === activity ? "selected" : ""}`} /> + ))} +
+ + {({ + onMouseEnter, + onMouseLeave + }) => { + return { + if (currentIndex + 1 < activities.length) { + onActivityChange(activities[currentIndex + 1]); + } else { + onActivityChange(activities[0]); + } + }} + > + = activities.length - 1} + direction="right" /> + ; + }} +
+ ); +} diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 1e6524ae..b87f8ff3 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -6,467 +6,31 @@ import "./styles.css"; -import { definePluginSettings, migratePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; +import { migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { PresenceStore, React, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common"; -import { User } from "discord-types/general"; -import { JSX } from "react"; +import definePlugin from "@utils/types"; -import { Caret } from "./components/Caret"; -import { SpotifyIcon } from "./components/SpotifyIcon"; -import { TwitchIcon } from "./components/TwitchIcon"; -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 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; - currentUser: User; -}>('location:"UserProfileActivityCard",'); - -// 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; }>) => { - const currentUser = UserStore.getCurrentUser(); - if (!currentUser) return null; - return ( - -
- -
-
- ); -}; - -function getActivityApplication(activity: Activity | null) { - if (!activity) return undefined; - const { 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; -} +import { patchActivityList } from "./patch-helpers/activityList"; +import { showAllActivitiesComponent } from "./patch-helpers/popout"; +import { settings } from "./settings"; migratePluginSettings("BetterActivities", "MemberListActivities"); export default definePlugin({ name: "BetterActivities", description: "Shows activity icons in the member list and allows showing all activities", - authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN], + authors: [ + Devs.D3SOX, + Devs.Arjix, + Devs.AutumnVN + ], tags: ["activity"], settings, - patchActivityList: ({ activities, user, hideTooltip }: { activities: Activity[], user: User, hideTooltip: boolean; }): JSX.Element | null => { - const icons: ActivityListIcon[] = []; + patchActivityList, - if (user.bot || hideTooltip) return null; - - const applicationIcons = getApplicationIcons(activities); - if (applicationIcons.length) { - const compareImageSource = (a: ApplicationIcon, b: ApplicationIcon) => { - return a.image.src === b.image.src; - }; - const uniqueIcons = applicationIcons.filter((element, index, array) => { - return array.findIndex(el => compareImageSource(el, element)) === index; - }); - for (const appIcon of uniqueIcons) { - icons.push({ - iconElement: , - tooltip: - }); - } - } - - const addActivityIcon = (activityName: string, IconComponent: React.ComponentType) => { - const activityIndex = activities.findIndex(({ name }) => name === activityName); - if (activityIndex !== -1) { - const activity = activities[activityIndex]; - const iconObject: ActivityListIcon = { - iconElement: , - tooltip: - }; - - if (settings.store.specialFirst) { - icons.unshift(iconObject); - } else { - icons.splice(activityIndex, 0, iconObject); - } - } - }; - addActivityIcon("Twitch", TwitchIcon); - addActivityIcon("Spotify", SpotifyIcon); - - if (icons.length) { - const iconStyle: IconCSSProperties = { - "--icon-size": `${settings.store.iconSize}px`, - }; - - return -
- {icons.map(({ iconElement, tooltip }, i) => ( -
- {tooltip ? - {({ onMouseEnter, onMouseLeave }) => ( -
- {iconElement} -
- )} -
: iconElement} -
- ))} -
-
; - } else { - // Show default icon when there are no custom icons - // We need to filter out custom statuses - const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length; - if (shouldShow) { - return ; - } - } - - return null; - }, - - showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) { - const currentUser = UserStore.getCurrentUser(); - if (!currentUser) return null; - - const [currentActivity, setCurrentActivity] = useState( - activity?.type !== 4 ? activity! : null - ); - - const activities = useStateFromStores( - [PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4) - ) ?? []; - - useEffect(() => { - if (!activities.length) { - setCurrentActivity(null); - return; - } - - 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 ( - { }}> -
- {activity && currentActivity?.id === activity.id ? ( - - ) : ( - - )} - {activities.length > 1 && -
- {({ - onMouseEnter, - onMouseLeave - }) => { - return { - const index = activities.indexOf(currentActivity!); - if (index - 1 >= 0) { - setCurrentActivity(activities[index - 1]); - } else { - setCurrentActivity(activities[activities.length - 1]); - } - }} - > - - ; - }} - -
- {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]); - } else { - setCurrentActivity(activities[0]); - } - }} - > - = activities.length - 1} - direction="right" /> - ; - }} -
- } -
- - ); - } else { - return ( - { }}> -
- {activities.map((activity, index) => - index === 0 ? ( - ) : ( - - ))} -
-
- ); - } - }, + showAllActivitiesComponent, patches: [ { diff --git a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx new file mode 100644 index 00000000..9e4b7893 --- /dev/null +++ b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx @@ -0,0 +1,94 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { findComponentByCodeLazy } from "@webpack"; +import { React, Tooltip } from "@webpack/common"; +import { JSX } from "react"; + +import { ActivityTooltip } from "../components/ActivityTooltip"; +import { SpotifyIcon } from "../components/SpotifyIcon"; +import { TwitchIcon } from "../components/TwitchIcon"; +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"); + +export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null { + const icons: ActivityListIcon[] = []; + + if (user.bot || hideTooltip) return null; + + const applicationIcons = getApplicationIcons(activities); + if (applicationIcons.length) { + const compareImageSource = (a: ApplicationIcon, b: ApplicationIcon) => { + return a.image.src === b.image.src; + }; + const uniqueIcons = applicationIcons.filter((element, index, array) => { + return array.findIndex(el => compareImageSource(el, element)) === index; + }); + for (const appIcon of uniqueIcons) { + icons.push({ + iconElement: , + tooltip: + }); + } + } + + const addActivityIcon = (activityName: string, IconComponent: React.ComponentType) => { + const activityIndex = activities.findIndex(({ name }) => name === activityName); + if (activityIndex !== -1) { + const activity = activities[activityIndex]; + const iconObject: ActivityListIcon = { + iconElement: , + tooltip: + }; + + if (settings.store.specialFirst) { + icons.unshift(iconObject); + } else { + icons.splice(activityIndex, 0, iconObject); + } + } + }; + addActivityIcon("Twitch", TwitchIcon); + addActivityIcon("Spotify", SpotifyIcon); + + if (icons.length) { + const iconStyle: IconCSSProperties = { + "--icon-size": `${settings.store.iconSize}px`, + }; + + return +
+ {icons.map(({ iconElement, tooltip }, i) => ( +
+ {tooltip ? + {({ onMouseEnter, onMouseLeave }) => ( +
+ {iconElement} +
+ )} +
: iconElement} +
+ ))} +
+
; + } else { + // Show default icon when there are no custom icons + // We need to filter out custom statuses + const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length; + if (shouldShow) { + return ; + } + } + + return null; +} diff --git a/src/equicordplugins/betterActivities/patch-helpers/popout.tsx b/src/equicordplugins/betterActivities/patch-helpers/popout.tsx new file mode 100644 index 00000000..e009cc9e --- /dev/null +++ b/src/equicordplugins/betterActivities/patch-helpers/popout.tsx @@ -0,0 +1,111 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { PresenceStore, React, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common"; +import { JSX } from "react"; + +import { CarouselControls } from "../components/CarouselControls"; +import { settings } from "../settings"; +import { Activity, AllActivitiesProps } from "../types"; +import { ActivityView, getActivityApplication } from "../utils"; + +export function showAllActivitiesComponent({ activity, user, ...props }: Readonly): JSX.Element | null { + const currentUser = UserStore.getCurrentUser(); + if (!currentUser) return null; + + const [currentActivity, setCurrentActivity] = useState( + activity?.type !== 4 ? activity! : null + ); + + const activities = useStateFromStores( + [PresenceStore], + () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4) + ); + + useEffect(() => { + if (!activities.length) { + setCurrentActivity(null); + return; + } + + 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 ( + +
+ {activity && currentActivity?.id === activity.id ? ( + + ) : ( + + )} + {activities.length > 1 && currentActivity && ( + + )} +
+
+ ); + } else { + return ( + +
+ {activities.map((activity, index) => + index === 0 ? ( + ) : ( + + ))} +
+
+ ); + } +} diff --git a/src/equicordplugins/betterActivities/settings.tsx b/src/equicordplugins/betterActivities/settings.tsx new file mode 100644 index 00000000..b27d6e4a --- /dev/null +++ b/src/equicordplugins/betterActivities/settings.tsx @@ -0,0 +1,71 @@ +/* + * 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 { OptionType } from "@utils/types"; +import { React } from "@webpack/common"; + +export 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, + }, + 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", + }, + ] + } +}); diff --git a/src/equicordplugins/betterActivities/styles.css b/src/equicordplugins/betterActivities/styles.css index 32c80f55..1ab6d3ad 100644 --- a/src/equicordplugins/betterActivities/styles.css +++ b/src/equicordplugins/betterActivities/styles.css @@ -19,12 +19,8 @@ border-radius: 50%; } -[class*="tooltip_"]:has(.vc-bactivities-activity-tooltip) { - max-width: 280px; -} - .vc-bactivities-activity-tooltip { - margin: -5px; + padding: 1px; } .vc-bactivities-caret-left, diff --git a/src/equicordplugins/betterActivities/types.ts b/src/equicordplugins/betterActivities/types.ts index 8f24dae5..78d21bfd 100644 --- a/src/equicordplugins/betterActivities/types.ts +++ b/src/equicordplugins/betterActivities/types.ts @@ -1,9 +1,10 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ +import { User } from "discord-types/general"; import { CSSProperties, ImgHTMLAttributes, JSX } from "react"; export interface Timestamp { @@ -80,3 +81,36 @@ export interface ActivityListIcon { export interface IconCSSProperties extends CSSProperties { "--icon-size": string; } + +export interface ActivityListProps { + activities: Activity[]; + user: User; + hideTooltip: boolean; +} + +export interface ActivityTooltipProps { + activity: Activity; + application?: Application; + user: User; +} + +export interface AllActivitiesProps { + activity: Activity; + user: User; + application: Application; + type: string; + [key: string]: any; +} + +export interface CarouselControlsProps { + activities: Activity[]; + currentActivity: Activity; + onActivityChange: (activity: Activity) => void; +} + +export interface ActivityViewProps { + activity: Activity | null; + user: User; + application?: Application; + currentUser: User; +} diff --git a/src/equicordplugins/betterActivities/utils.tsx b/src/equicordplugins/betterActivities/utils.tsx new file mode 100644 index 00000000..4798ad44 --- /dev/null +++ b/src/equicordplugins/betterActivities/utils.tsx @@ -0,0 +1,127 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; + +import { settings } from "./settings"; +import { Activity, ActivityViewProps, Application, ApplicationIcon } from "./types"; + +const ApplicationStore: { + getApplication: (id: string) => Application | null; +} = findStoreLazy("ApplicationStore"); + +const { fetchApplication }: { + fetchApplication: (id: string) => Promise; +} = findByPropsLazy("fetchApplication"); + +const fetchedApplications = new Map(); + +const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"? + +export const ActivityView = findComponentByCodeLazy('location:"UserProfileActivityCard",'); + +export const cl = classNameFactory("vc-bactivities-"); + +export function getActivityApplication(activity: Activity | null) { + if (!activity) return undefined; + const { 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; +} + +export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] { + 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 { small_image, small_text, large_image, large_text } = assets; + const smallText = small_text ?? "Small Text"; + const largeText = large_text ?? "Large Text"; + + 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 + }); + } + }; + + if (preferSmall) { + if (small_image) { + addImage(small_image, smallText); + } else if (large_image) { + addImage(large_image, largeText); + } + } else { + if (large_image) { + addImage(large_image, largeText); + } else if (small_image) { + addImage(small_image, 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; +}