From 8278641c9b3756d716a5c4e0e964f2d3aa32d674 Mon Sep 17 00:00:00 2001 From: D3SOX Date: Fri, 17 May 2024 18:05:00 +0200 Subject: [PATCH] refactor(betterSettings): split index into multiple files --- .../components/ActivityTooltip.tsx | 69 +++++ src/plugins/betterActivities/index.tsx | 280 ++---------------- src/plugins/betterActivities/settings.tsx | 79 +++++ src/plugins/betterActivities/types.ts | 9 + src/plugins/betterActivities/utils.ts | 158 ++++++++++ 5 files changed, 332 insertions(+), 263 deletions(-) create mode 100644 src/plugins/betterActivities/components/ActivityTooltip.tsx create mode 100644 src/plugins/betterActivities/settings.tsx create mode 100644 src/plugins/betterActivities/utils.ts diff --git a/src/plugins/betterActivities/components/ActivityTooltip.tsx b/src/plugins/betterActivities/components/ActivityTooltip.tsx new file mode 100644 index 000000000..64839d4a2 --- /dev/null +++ b/src/plugins/betterActivities/components/ActivityTooltip.tsx @@ -0,0 +1,69 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +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"); + +const ActivityTooltip = ({ activity, application, user, cl }: Readonly<{ activity: Activity, application?: Application, user: User; cl: ReturnType }>) => { + 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), moment())} +
+ } +
+ {timestamps && ( + + )} +
+ + ); +}; + +export default ActivityTooltip; diff --git a/src/plugins/betterActivities/index.tsx b/src/plugins/betterActivities/index.tsx index ba6ce3d5e..d51778de1 100644 --- a/src/plugins/betterActivities/index.tsx +++ b/src/plugins/betterActivities/index.tsx @@ -18,105 +18,26 @@ import "./styles.css"; -import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { moment, PresenceStore, React, Tooltip, useMemo, useStateFromStores } from "@webpack/common"; +import definePlugin from "@utils/types"; +import { findComponentByCodeLazy } from "@webpack"; +import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common"; import { Guild, 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 { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties, Timestamp } 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, - }, - 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", - }, - ] - } -}); +import settings from "./settings"; +import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types"; +import { + getApplicationIcons +} from "./utils"; const cl = classNameFactory("vc-bactivities-"); -const ApplicationStore: { - getApplication: (id: string) => Application | null; -} = findStoreLazy("ApplicationStore"); - -const { fetchApplication }: { - fetchApplication: (id: string) => Promise; -} = findByPropsLazy("fetchApplication"); - -const TimeBar = findComponentByCodeLazy<{ - start: number; - end: number; - themed: boolean; - className: string; -}>("isSingleLine"); - const ActivityView = findComponentByCodeLazy<{ activity: Activity | null; user: User; @@ -128,178 +49,6 @@ const ActivityView = findComponentByCodeLazy<{ // 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"? - -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 -} - -function getValidTimestamps(activity: Activity): Required | null { - if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) { - return activity.timestamps as Required; - } - return null; -} - -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; -}; - -function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string { - const duration = moment.duration(endTime.diff(startTime)); - return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`; -} - -const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => { - 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), moment())} -
- } -
- {timestamps && } -
- - ); -}; - -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 default definePlugin({ name: "BetterActivities", description: "Shows activity icons in the member list and allows showing all activities", @@ -322,7 +71,12 @@ export default definePlugin({ for (const appIcon of uniqueIcons) { icons.push({ iconElement: , - tooltip: + tooltip: }); } } @@ -333,7 +87,7 @@ export default definePlugin({ const activity = activities[activityIndex]; const iconObject: ActivityListIcon = { iconElement: , - tooltip: + tooltip: }; if (settings.store.specialFirst) { @@ -380,7 +134,7 @@ export default definePlugin({ return null; }, - showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: { activity: Activity; user: User, guild: Guild, channelId: string, onClose: () => void; }) { + showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) { const [currentActivity, setCurrentActivity] = React.useState( activity?.type !== 4 ? activity! : null ); diff --git a/src/plugins/betterActivities/settings.tsx b/src/plugins/betterActivities/settings.tsx new file mode 100644 index 000000000..415976bb8 --- /dev/null +++ b/src/plugins/betterActivities/settings.tsx @@ -0,0 +1,79 @@ +/* + * 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, + restartNeeded: false, + }, + renderGifs: { + type: OptionType.BOOLEAN, + description: "Allow rendering GIFs", + default: true, + restartNeeded: false, + }, + 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/plugins/betterActivities/types.ts b/src/plugins/betterActivities/types.ts index 7e7421cb7..909435fe2 100644 --- a/src/plugins/betterActivities/types.ts +++ b/src/plugins/betterActivities/types.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { Guild, User } from "discord-types/general"; import { CSSProperties, ImgHTMLAttributes } from "react"; export interface Timestamp { @@ -80,3 +81,11 @@ export interface ActivityListIcon { export interface IconCSSProperties extends CSSProperties { "--icon-size": string; } + +export interface ActivityViewProps { + activity: Activity; + user: User; + guild: Guild; + channelId: string; + onClose: () => void; +} diff --git a/src/plugins/betterActivities/utils.ts b/src/plugins/betterActivities/utils.ts new file mode 100644 index 000000000..d1511acc0 --- /dev/null +++ b/src/plugins/betterActivities/utils.ts @@ -0,0 +1,158 @@ +/* + * 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`; +}