diff --git a/src/plugins/betterActivities/README.md b/src/plugins/betterActivities/README.md new file mode 100644 index 000000000..b40385675 --- /dev/null +++ b/src/plugins/betterActivities/README.md @@ -0,0 +1,6 @@ +# BetterActivities + +Shows activity icons in the member list and allows showing all activities + +![Screenshot](screenshot.png) +![Popout](popout.png) diff --git a/src/plugins/betterActivities/components/Caret.tsx b/src/plugins/betterActivities/components/Caret.tsx new file mode 100644 index 000000000..b948d4358 --- /dev/null +++ b/src/plugins/betterActivities/components/Caret.tsx @@ -0,0 +1,13 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function Caret({ disabled, direction }: { disabled: boolean; direction: "left" | "right"; }) { + return ( + + + + ); +} diff --git a/src/plugins/memberListActivities/components/SpotifyIcon.tsx b/src/plugins/betterActivities/components/SpotifyIcon.tsx similarity index 100% rename from src/plugins/memberListActivities/components/SpotifyIcon.tsx rename to src/plugins/betterActivities/components/SpotifyIcon.tsx diff --git a/src/plugins/memberListActivities/components/TwitchIcon.tsx b/src/plugins/betterActivities/components/TwitchIcon.tsx similarity index 100% rename from src/plugins/memberListActivities/components/TwitchIcon.tsx rename to src/plugins/betterActivities/components/TwitchIcon.tsx diff --git a/src/plugins/memberListActivities/index.tsx b/src/plugins/betterActivities/index.tsx similarity index 66% rename from src/plugins/memberListActivities/index.tsx rename to src/plugins/betterActivities/index.tsx index ec45fa2e0..23ae64daa 100644 --- a/src/plugins/memberListActivities/index.tsx +++ b/src/plugins/betterActivities/index.tsx @@ -23,15 +23,34 @@ 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, React, Tooltip, useMemo } from "@webpack/common"; -import { User } from "discord-types/general"; +import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { moment, PresenceStore, React, Tooltip, useMemo, useStateFromStores } from "@webpack/common"; +import { Guild, User } from "discord-types/general"; +import { Caret } from "./components/Caret"; import { SpotifyIcon } from "./components/SpotifyIcon"; import { TwitchIcon } from "./components/TwitchIcon"; -import { Activity, ActivityListIcon, Application, ApplicationIcon, Timestamp } from "./types"; +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, + }, + 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, + }, iconSize: { type: OptionType.SLIDER, description: "Size of the activity icons", @@ -47,7 +66,7 @@ const settings = definePluginSettings({ }, }); -const cl = classNameFactory("vc-mla-"); +const cl = classNameFactory("vc-bactivities-"); const ApplicationStore: { getApplication: (id: string) => Application | null; @@ -57,12 +76,14 @@ const { fetchApplication }: { fetchApplication: (id: string) => Promise; } = findByPropsLazy("fetchApplication"); -const TimeBar: React.ComponentType> = findComponentByCodeLazy("isSingleLine"); +}>("isSingleLine"); + +const ActivityView = findByCodeLazy("onOpenGameProfile:"); // 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"); @@ -117,7 +138,7 @@ function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): st return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`; } -const ActivityTooltip = ({ activity, application }: Readonly<{ activity: Activity, application?: Application }>) => { +const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => { const image = useMemo(() => { const activityImage = getActivityImage(activity, application); if (activityImage) { @@ -145,7 +166,7 @@ const ActivityTooltip = ({ activity, application }: Readonly<{ activity: Activit } - {timestamps && } + {timestamps && } ); @@ -240,28 +261,28 @@ function getApplicationIcons(activities: Activity[], preferSmall = false) { } export default definePlugin({ - name: "MemberListActivities", - description: "Shows activity icons in the member list", - authors: [Devs.D3SOX], + name: "BetterActivities", + description: "Shows activity icons in the member list and allows showing all activities", + authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN], tags: ["activity"], settings, - patchActivityList: ({ activities, user }: { activities: Activity[], user: User }): JSX.Element | null => { + patchActivityList: ({ activities, user }: { activities: Activity[], user: User; }): JSX.Element | null => { const icons: ActivityListIcon[] = []; const spotifyActivity = activities.find(({ name }) => name === "Spotify"); if (spotifyActivity) { icons.push({ iconElement: , - tooltip: + tooltip: }); } const twitchActivity = activities.find(({ name }) => name === "Twitch"); if (twitchActivity) { icons.push({ iconElement: , - tooltip: + tooltip: }); } @@ -276,19 +297,20 @@ export default definePlugin({ for (const appIcon of uniqueIcons) { icons.push({ iconElement: , - tooltip: + tooltip: }); } } if (icons.length) { + const iconStyle: IconCSSProperties = { + "--icon-size": `${settings.store.iconSize}px`, + }; + return
{icons.map(({ iconElement, tooltip }, i) => ( -
+
{tooltip ? {({ onMouseEnter, onMouseLeave }) => (
void; }) { + const [currentActivity, setCurrentActivity] = React.useState( + activity?.type !== 4 ? activity! : null + ); + + const activities = useStateFromStores( + [PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4) + ) ?? []; + + React.useEffect(() => { + if (!activities.length) { + setCurrentActivity(null); + return; + } + + if (!currentActivity || !activities.includes(currentActivity)) + setCurrentActivity(activities[0]); + + }, [activities]); + + if (!activities.length) return null; + + return ( +
+ +
+ {({ 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" : ""}`} + /> + ))} +
+ + {({ onMouseEnter, onMouseLeave }) => { + return { + const index = activities.indexOf(currentActivity!); + if (index + 1 < activities.length) + setCurrentActivity(activities[index + 1]); + }} + > + = activities.length - 1} + direction="right" + /> + ; + }} +
+
+ ); + }, + patches: [ { // Patch activity icons @@ -321,7 +430,26 @@ export default definePlugin({ replacement: { match: /null!=(\i)&&\i.some\(\i=>\(0,\i.default\)\(\i,\i\)\)\?/, replace: "$self.patchActivityList(e),false?" - } + }, + predicate: () => settings.store.memberList, }, + { + // Show all activities in the profile panel + find: "Profile Panel: user cannot be undefined", + 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" + }, + predicate: () => settings.store.userPopout + } ], }); diff --git a/src/plugins/betterActivities/popout.png b/src/plugins/betterActivities/popout.png new file mode 100644 index 000000000..c7e572188 Binary files /dev/null and b/src/plugins/betterActivities/popout.png differ diff --git a/src/plugins/memberListActivities/screenshot.png b/src/plugins/betterActivities/screenshot.png similarity index 100% rename from src/plugins/memberListActivities/screenshot.png rename to src/plugins/betterActivities/screenshot.png diff --git a/src/plugins/betterActivities/styles.css b/src/plugins/betterActivities/styles.css new file mode 100644 index 000000000..743f5d213 --- /dev/null +++ b/src/plugins/betterActivities/styles.css @@ -0,0 +1,131 @@ +.vc-bactivities-row { + display: flex; + flex-wrap: nowrap; + align-items: center; + margin-left: 5px; + text-align: center; + gap: 3px; +} + +.vc-bactivities-icon { + height: var(--icon-size); + width: var(--icon-size); +} + +.vc-bactivities-icon img { + width: var(--icon-size); + height: var(--icon-size); + object-fit: cover; + 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-caret-left, +.vc-bactivities-caret-right { + color: #ddd; +} + +.vc-bactivities-caret-left { + transform: rotate(90deg); +} + +.vc-bactivities-caret-right { + transform: rotate(-90deg); +} + +.vc-bactivities-controls { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px; + background: var(--background-secondary-alt); + border-radius: 3px; + flex: 1 0; + margin-top: 10px; +} + +.vc-bactivities-controls [class^="vc-activities-caret-"] { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 3px; + background-color: #ffffff4d; +} + +.vc-bactivities-controls [class^="vc-activities-caret-"].disabled { + cursor: not-allowed; + opacity: 0.3; +} + +.vc-bactivities-controls [class^="vc-activities-caret-"]:hover:not(.disabled) { + background: var(--background-modifier-accent); +} + +.vc-bactivities-controls .carousell { + display: flex; + align-items: center; +} + +.vc-bactivities-controls .carousell .dot { + margin: 0 4px; + width: 10px; + cursor: pointer; + height: 10px; + border-radius: 100px; + background: var(--interactive-muted); + transition: background 0.3s; + opacity: 0.6; +} + +.vc-bactivities-controls .carousell .dot:hover:not(.selected) { + opacity: 1; +} + +.vc-bactivities-controls .carousell .dot.selected { + opacity: 1; + background: var(--dot-color, var(--brand-experiment)); +} + +.vc-bactivities-controls-tooltip { + --background-floating: var(--background-secondary); +} diff --git a/src/plugins/memberListActivities/types.ts b/src/plugins/betterActivities/types.ts similarity index 90% rename from src/plugins/memberListActivities/types.ts rename to src/plugins/betterActivities/types.ts index 7f3f2b509..7e7421cb7 100644 --- a/src/plugins/memberListActivities/types.ts +++ b/src/plugins/betterActivities/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import type { ImgHTMLAttributes } from "react"; +import { CSSProperties, ImgHTMLAttributes } from "react"; export interface Timestamp { start?: number; @@ -76,3 +76,7 @@ export interface ActivityListIcon { iconElement: JSX.Element; tooltip?: JSX.Element | string; } + +export interface IconCSSProperties extends CSSProperties { + "--icon-size": string; +} diff --git a/src/plugins/memberListActivities/README.md b/src/plugins/memberListActivities/README.md deleted file mode 100644 index fe67bad79..000000000 --- a/src/plugins/memberListActivities/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# MemberListActivities - -Shows activity icons in the member list - -![Screenshot](screenshot.png) diff --git a/src/plugins/memberListActivities/styles.css b/src/plugins/memberListActivities/styles.css deleted file mode 100644 index 9caeb4319..000000000 --- a/src/plugins/memberListActivities/styles.css +++ /dev/null @@ -1,60 +0,0 @@ -.vc-mla-row { - display: flex; - flex-wrap: nowrap; - align-items: center; - margin-left: 5px; - text-align: center; - gap: 3px; -} - -.vc-mla-icon { - height: 20px; - width: 20px; -} - -.vc-mla-icon img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 50%; -} - -.vc-mla-activity { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 5px; -} - -.vc-mla-activity-title { - font-weight: bold; - text-align: center; -} - -.vc-mla-activity-image { - height: 20px; - width: 20px; - border-radius: 50%; - object-fit: cover; -} - -.vc-mla-activity-divider { - width: 100%; - border-top: 1px dotted rgb(255 255 255 / 20%); - margin-top: 3px; - margin-bottom: 3px; -} - -.vc-mla-activity-details { - display: flex; - flex-direction: column; - color: var(--text-muted); - word-break: break-word; -} - -.vc-mla-activity-time-bar { - width: 100%; - margin-top: 3px; - margin-bottom: 3px; -}