Plugin

IgnoreActivities

Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below

Activity Privacy Customisation
index.tsx
Download

Source

src/plugins/ignoreActivities/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { definePluginSettings } from "@api/Settings";
8import { getUserSettingLazy } from "@api/UserSettings";
9import ErrorBoundary from "@components/ErrorBoundary";
10import { Flex } from "@components/Flex";
11import CustomRpcPlugin from "@plugins/customRPC";
12import { Devs } from "@utils/constants";
13import { Margins } from "@utils/margins";
14import definePlugin, { OptionType } from "@utils/types";
15import { Button, Forms, RunningGameStore, showToast, TextArea, Toasts, Tooltip, useEffect, useState } from "@webpack/common";
16
17const enum ActivitiesTypes {
18 Game,
19 Embedded
20}
21
22interface IgnoredActivity {
23 id: string;
24 name: string;
25 type: ActivitiesTypes;
26}
27
28const enum FilterMode {
29 Whitelist,
30 Blacklist
31}
32
33const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!;
34
35function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
36 return (
37 <Tooltip text={tooltipText}>
38 {tooltipProps => (
39 <button
40 {...tooltipProps}
41 onClick={e => handleActivityToggle(e, activity)}
42 style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
43 >
44 <svg
45 width="24"
46 height="24"
47 viewBox="0 -960 960 960"
48 >
49 <path fill={fill} d={path} />
50 </svg>
51 </button>
52 )}
53 </Tooltip>
54 );
55}
56
57const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill);
58const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill);
59
60function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
61 const s = settings.use(["ignoredActivities"]);
62 const { ignoredActivities } = s;
63
64 if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
65 return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--interactive-icon-default)");
66}
67
68function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity) {
69 e.stopPropagation();
70
71 const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id);
72 if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity);
73 else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1);
74}
75
76function recalculateActivities() {
77 ShowCurrentGame.updateSetting(old => old);
78}
79
80function ImportCustomRPCComponent() {
81 return (
82 <Flex flexDirection="column">
83 <Forms.FormText>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
84 <div>
85 <Button
86 onClick={() => {
87 const id = CustomRpcPlugin.settings.store.appID;
88 if (!id) {
89 return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
90 }
91
92 const isAlreadyAdded = idsListPushID?.(id);
93 if (isAlreadyAdded) {
94 showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
95 }
96 }}
97 >
98 Import CustomRPC ID
99 </Button>
100 </div>
101 </Flex>
102 );
103}
104
105let idsListPushID: ((id: string) => boolean) | null = null;
106
107function IdsListComponent(props: { setValue: (value: string) => void; }) {
108 const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? "");
109
110 idsListPushID = (id: string) => {
111 const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean));
112
113 const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
114
115 const ids = Array.from(currentIds).join(", ");
116 setIdsList(ids);
117 props.setValue(ids);
118
119 return isAlreadyAdded;
120 };
121
122 useEffect(() => () => {
123 idsListPushID = null;
124 }, []);
125
126 function handleChange(newValue: string) {
127 setIdsList(newValue);
128 props.setValue(newValue);
129 }
130
131 return (
132 <section>
133 <Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
134 <Forms.FormText className={Margins.bottom8}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
135 <TextArea
136 type="text"
137 value={idsList}
138 onChange={handleChange}
139 placeholder="235834946571337729, 343383572805058560"
140 />
141 </section>
142 );
143}
144
145const settings = definePluginSettings({
146 importCustomRPC: {
147 type: OptionType.COMPONENT,
148 component: ImportCustomRPCComponent
149 },
150 listMode: {
151 type: OptionType.SELECT,
152 description: "Change the mode of the filter list",
153 options: [
154 {
155 label: "Whitelist",
156 value: FilterMode.Whitelist,
157 default: true
158 },
159 {
160 label: "Blacklist",
161 value: FilterMode.Blacklist,
162 }
163 ],
164 onChange: recalculateActivities
165 },
166 idsList: {
167 type: OptionType.COMPONENT,
168 default: "",
169 onChange(newValue: string) {
170 const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
171 settings.store.idsList = Array.from(ids).join(", ");
172 recalculateActivities();
173 },
174 component: props => <IdsListComponent setValue={props.setValue} />
175 },
176 ignorePlaying: {
177 type: OptionType.BOOLEAN,
178 description: "Ignore all playing activities (These are usually game and RPC activities)",
179 default: false,
180 onChange: recalculateActivities
181 },
182 ignoreStreaming: {
183 type: OptionType.BOOLEAN,
184 description: "Ignore all streaming activities",
185 default: false,
186 onChange: recalculateActivities
187 },
188 ignoreListening: {
189 type: OptionType.BOOLEAN,
190 description: "Ignore all listening activities (These are usually spotify activities)",
191 default: false,
192 onChange: recalculateActivities
193 },
194 ignoreWatching: {
195 type: OptionType.BOOLEAN,
196 description: "Ignore all watching activities",
197 default: false,
198 onChange: recalculateActivities
199 },
200 ignoreCompeting: {
201 type: OptionType.BOOLEAN,
202 description: "Ignore all competing activities (These are normally special game activities)",
203 default: false,
204 onChange: recalculateActivities
205 },
206 ignoredActivities: {
207 type: OptionType.CUSTOM,
208 default: [] as IgnoredActivity[],
209 onChange: recalculateActivities
210 }
211});
212
213function isActivityTypeIgnored(type: number, id?: string) {
214 if (id && settings.store.idsList.includes(id)) {
215 return settings.store.listMode === FilterMode.Blacklist;
216 }
217
218 switch (type) {
219 case 0: return settings.store.ignorePlaying;
220 case 1: return settings.store.ignoreStreaming;
221 case 2: return settings.store.ignoreListening;
222 case 3: return settings.store.ignoreWatching;
223 case 5: return settings.store.ignoreCompeting;
224 }
225
226 return false;
227}
228
229export default definePlugin({
230 name: "IgnoreActivities",
231 authors: [Devs.Nuckyz, Devs.Kylie],
232 description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below",
233 tags: ["Activity", "Privacy", "Customisation"],
234 dependencies: ["UserSettingsAPI"],
235
236 settings,
237
238 patches: [
239 {
240 find: &#039;"LocalActivityStore"&#039;,
241 replacement: [
242 {
243 match: /\.LISTENING.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/,
244 replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
245 }
246 ]
247 },
248 {
249 find: &#039;"ActivityTrackingStore"&#039;,
250 replacement: {
251 match: /getVisibleRunningGames\(\).+?;(?=for)(?<=(\i)=\i\.\i\.getVisibleRunningGames.+?)/,
252 replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));`
253 }
254 },
255 {
256 find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}",
257 replacement: {
258 match: /(\i)&&!\i\|\|\i\?null(?<=(\i)\.verified&&.+?)/,
259 replace: "$self.renderToggleGameActivityButton($2,$1),$&"
260 }
261 },
262
263 // Activities from the apps launcher in the bottom right of the chat bar
264 {
265 find: "#{intl::EMBEDDED_ACTIVITIES_DEVELOPER_ACTIVITY}",
266 replacement: {
267 match: /lineClamp:1.{0,50}?(?=!\i&&\i\?.+?application:(\i))/,
268 replace: "$&$self.renderToggleActivityButton($1),"
269 }
270 }
271 ],
272
273 async start() {
274 if (settings.store.ignoredActivities.length !== 0) {
275 const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
276
277 for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) {
278 if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
279
280 if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
281 settings.store.ignoredActivities.splice(index, 1);
282 }
283 }
284 }
285 },
286
287 isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
288 if (isActivityTypeIgnored(props.type, props.application_id)) return false;
289
290 if (props.application_id != null) {
291 return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
292 } else {
293 const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
294 if (exePath) {
295 return !settings.store.ignoredActivities.some(activity => activity.id === exePath);
296 }
297 }
298
299 return true;
300 },
301
302 renderToggleGameActivityButton(props: { id?: string; name: string, exePath: string; }, nowPlaying: boolean) {
303 return (
304 <ErrorBoundary noop>
305 <div style={{ marginLeft: 12, zIndex: 0 }}>
306 {ToggleActivityComponent({ id: props.id ?? props.exePath, name: props.name, type: ActivitiesTypes.Game }, nowPlaying)}
307 </div>
308 </ErrorBoundary>
309 );
310 },
311
312 renderToggleActivityButton(props: { id: string; name: string; }) {
313 return (
314 <ErrorBoundary noop>
315 {ToggleActivityComponent({ id: props.id, name: props.name, type: ActivitiesTypes.Embedded })}
316 </ErrorBoundary>
317 );
318 }
319});
320