/* * 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 . */ import "./styles.css"; import { 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 { 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"; const cl = classNameFactory("vc-bactivities-"); const ActivityView = findComponentByCodeLazy("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"); migratePluginSettings("MemberListActivites", "BetterActivities"); 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], tags: ["activity"], settings, patchActivityList: ({ activities, user }: { activities: Activity[], user: User; }): JSX.Element | null => { const icons: ActivityListIcon[] = []; 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, guild, channelId, onClose }: ActivityViewProps) { 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; if (settings.store.allActivitiesStyle === "carousel") { 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" /> ; }}
); } else { return (
{activities.map((activity, index) => ( ))}
); } }, patches: [ { // Patch activity icons find: ".getHangStatusActivity():null!", replacement: { match: /null!=(\i)&&\i.some\(\i=>\(0,\i.\i\)\(\i,\i\)\)\?/, replace: "$self.patchActivityList(e),false?" }, predicate: () => settings.store.memberList, }, { // Show all activities in the profile panel find: /\i\.\i\i\.PANEL,themeOverride:\i\i,/, 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 } ], });