feat(betterActivities): rename and add show all activities functionality

This commit is contained in:
D3SOX 2024-05-06 19:04:17 +02:00
parent 957639be6c
commit 4e430c4252
No known key found for this signature in database
GPG key ID: 39EC1673FC37B048
11 changed files with 304 additions and 87 deletions

View file

@ -0,0 +1,6 @@
# BetterActivities
Shows activity icons in the member list and allows showing all activities
![Screenshot](screenshot.png)
![Popout](popout.png)

View file

@ -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 (
<svg className={`vc-bactivities-caret-${direction.toLowerCase()} ${disabled && "disabled"}`} width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M16.59 8.59004L12 13.17L7.41 8.59004L6 10L12 16L18 10L16.59 8.59004Z" />
</svg>
);
}

View file

@ -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<Application | null>;
} = findByPropsLazy("fetchApplication");
const TimeBar: React.ComponentType<React.PropsWithChildren<{
const TimeBar = findComponentByCodeLazy<{
start: number;
end: number;
themed: boolean;
className: string;
}>> = 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) {
@ -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: <SpotifyIcon />,
tooltip: <ActivityTooltip activity={spotifyActivity} />
tooltip: <ActivityTooltip activity={spotifyActivity} user={user} />
});
}
const twitchActivity = activities.find(({ name }) => name === "Twitch");
if (twitchActivity) {
icons.push({
iconElement: <TwitchIcon />,
tooltip: <ActivityTooltip activity={twitchActivity} />
tooltip: <ActivityTooltip activity={twitchActivity} user={user} />
});
}
@ -276,19 +297,20 @@ export default definePlugin({
for (const appIcon of uniqueIcons) {
icons.push({
iconElement: <img {...appIcon.image} />,
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} />
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
});
}
}
if (icons.length) {
const iconStyle: IconCSSProperties = {
"--icon-size": `${settings.store.iconSize}px`,
};
return <ErrorBoundary noop>
<div className={cl("row")}>
{icons.map(({ iconElement, tooltip }, i) => (
<div key={i} className={cl("icon")} style={{
width: `${settings.store.iconSize}px`,
height: `${settings.store.iconSize}px`
}}>
<div key={i} className={cl("icon")} style={iconStyle}>
{tooltip ? <Tooltip text={tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div
@ -314,6 +336,93 @@ export default definePlugin({
return null;
},
showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: { activity: Activity; user: User, guild: Guild, channelId: string, onClose: () => void; }) {
const [currentActivity, setCurrentActivity] = React.useState<Activity | null>(
activity?.type !== 4 ? activity! : null
);
const activities = useStateFromStores<Activity[]>(
[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 (
<div style={{ display: "flex", flexDirection: "column" }}>
<ActivityView
activity={currentActivity}
user={user}
guild={guild}
channelId={channelId}
onClose={onClose}
/>
<div
className={cl("controls")}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({ onMouseEnter, onMouseLeave }) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index - 1 >= 0)
setCurrentActivity(activities[index - 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) < 1}
direction="left"
/>
</span>;
}}</Tooltip>
<div className="carousell">
{activities.map((activity, index) => (
<div
key={"dot--" + index}
onClick={() => setCurrentActivity(activity)}
className={`dot ${currentActivity === activity ? "selected" : ""}`}
/>
))}
</div>
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({ onMouseEnter, onMouseLeave }) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index + 1 < activities.length)
setCurrentActivity(activities[index + 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
direction="right"
/>
</span>;
}}</Tooltip>
</div>
</div>
);
},
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
}
],
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -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);
}

View file

@ -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;
}

View file

@ -1,5 +0,0 @@
# MemberListActivities
Shows activity icons in the member list
![Screenshot](screenshot.png)

View file

@ -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;
}