Plugin

CustomRPC

Add a fully customisable Rich Presence (Game status) to your Discord profile

Activity Customisation
index.tsx
Download

Source

src/plugins/customRPC/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import { getUserSettingLazy } from "@api/UserSettings";
3import { Divider } from "@components/Divider";
4import { ErrorCard } from "@components/ErrorCard";
5import { Flex } from "@components/Flex";
6import { Link } from "@components/Link";
7import { Devs } from "@utils/constants";
8import { isTruthy } from "@utils/guards";
9import { Margins } from "@utils/margins";
10import { classes } from "@utils/misc";
11import { useAwaiter } from "@utils/react";
12import definePlugin, { OptionType } from "@utils/types";
13import { Activity } from "@vencord/discord-types";
14import { ActivityType } from "@vencord/discord-types/enums";
15import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
16import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
17
18import { RPCSettings } from "./RpcSettings";
19
20const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
21const ActivityView = findComponentByCodeLazy(".party?(0", "USER_PROFILE_ACTIVITY");
22
23const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
24
25async function getApplicationAsset(key: string): Promise<string> {
26 return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
27}
28
29export const enum TimestampMode {
30 NONE,
31 NOW,
32 TIME,
33 CUSTOM,
34}
35
36export const settings = definePluginSettings({
37 config: {
38 type: OptionType.COMPONENT,
39 component: RPCSettings
40 },
41}).withPrivateSettings<{
42 appID?: string;
43 appName?: string;
44 details?: string;
45 detailsURL?: string;
46 state?: string;
47 stateURL?: string;
48 type?: ActivityType;
49 streamLink?: string;
50 timestampMode?: TimestampMode;
51 startTime?: number;
52 endTime?: number;
53 imageBig?: string;
54 imageBigURL?: string;
55 imageBigTooltip?: string;
56 imageSmall?: string;
57 imageSmallURL?: string;
58 imageSmallTooltip?: string;
59 buttonOneText?: string;
60 buttonOneURL?: string;
61 buttonTwoText?: string;
62 buttonTwoURL?: string;
63 partySize?: number;
64 partyMaxSize?: number;
65}>();
66
67async function createActivity(): Promise<Activity | undefined> {
68 const {
69 appID,
70 appName,
71 details,
72 detailsURL,
73 state,
74 stateURL,
75 type,
76 streamLink,
77 startTime,
78 endTime,
79 imageBig,
80 imageBigURL,
81 imageBigTooltip,
82 imageSmall,
83 imageSmallURL,
84 imageSmallTooltip,
85 buttonOneText,
86 buttonOneURL,
87 buttonTwoText,
88 buttonTwoURL,
89 partyMaxSize,
90 partySize,
91 timestampMode
92 } = settings.store;
93
94 if (!appName) return;
95
96 const activity: Activity = {
97 application_id: appID || "0",
98 name: appName,
99 state,
100 details,
101 type: type ?? ActivityType.PLAYING,
102 flags: 1 << 0,
103 };
104
105 if (type === ActivityType.STREAMING) activity.url = streamLink;
106
107 switch (timestampMode) {
108 case TimestampMode.NOW:
109 activity.timestamps = {
110 start: Date.now()
111 };
112 break;
113 case TimestampMode.TIME:
114 activity.timestamps = {
115 start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
116 };
117 break;
118 case TimestampMode.CUSTOM:
119 if (startTime || endTime) {
120 activity.timestamps = {};
121 if (startTime) activity.timestamps.start = startTime;
122 if (endTime) activity.timestamps.end = endTime;
123 }
124 break;
125 case TimestampMode.NONE:
126 default:
127 break;
128 }
129
130 if (detailsURL) {
131 activity.details_url = detailsURL;
132 }
133
134 if (stateURL) {
135 activity.state_url = stateURL;
136 }
137
138 if (buttonOneText) {
139 activity.buttons = [
140 buttonOneText,
141 buttonTwoText
142 ].filter(isTruthy);
143
144 activity.metadata = {
145 button_urls: [
146 buttonOneURL,
147 buttonTwoURL
148 ].filter(isTruthy)
149 };
150 }
151
152 if (imageBig) {
153 activity.assets = {
154 large_image: await getApplicationAsset(imageBig),
155 large_text: imageBigTooltip || undefined,
156 large_url: imageBigURL || undefined
157 };
158 }
159
160 if (imageSmall) {
161 activity.assets = {
162 ...activity.assets,
163 small_image: await getApplicationAsset(imageSmall),
164 small_text: imageSmallTooltip || undefined,
165 small_url: imageSmallURL || undefined
166 };
167 }
168
169 if (partyMaxSize && partySize) {
170 activity.party = {
171 size: [partySize, partyMaxSize]
172 };
173 }
174
175 for (const k in activity) {
176 if (k === "type") continue;
177 const v = activity[k];
178 if (!v || v.length === 0)
179 delete activity[k];
180 }
181
182 return activity;
183}
184
185export async function setRpc(disable?: boolean) {
186 const activity: Activity | undefined = await createActivity();
187
188 FluxDispatcher.dispatch({
189 type: "LOCAL_ACTIVITY_UPDATE",
190 activity: !disable ? activity : null,
191 socketId: "CustomRPC",
192 });
193}
194
195export default definePlugin({
196 name: "CustomRPC",
197 description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
198 tags: ["Activity", "Customisation"],
199 authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
200 dependencies: ["UserSettingsAPI"],
201 // This plugin's patch is not important for functionality, so don't require a restart
202 requiresRestart: false,
203 settings,
204
205 start: setRpc,
206 stop: () => setRpc(true),
207
208 // Discord hides buttons on your own Rich Presence for some reason. This patch disables that behaviour
209 patches: [
210 {
211 find: ".USER_PROFILE_ACTIVITY_BUTTONS),",
212 replacement: {
213 match: /.getId\(\)===\i.id/,
214 replace: "$& && false"
215 }
216 }
217 ],
218
219 settingsAboutComponent: () => {
220 const [activity] = useAwaiter(createActivity, { fallbackValue: undefined, deps: Object.values(settings.store) });
221 const gameActivityEnabled = ShowCurrentGame.useSetting();
222 const { profileThemeStyle } = useProfileThemeStyle({});
223
224 return (
225 <>
226 {!gameActivityEnabled && (
227 <ErrorCard
228 className={classes(Margins.top16, Margins.bottom16)}
229 style={{ padding: "1em" }}
230 >
231 <Forms.FormTitle>Notice</Forms.FormTitle>
232 <Forms.FormText>Activity Sharing isn&#039;t enabled, people won&#039;t be able to see your custom rich presence!</Forms.FormText>
233
234 <Button
235 color={Button.Colors.TRANSPARENT}
236 className={Margins.top8}
237 onClick={() => ShowCurrentGame.updateSetting(true)}
238 >
239 Enable
240 </Button>
241 </ErrorCard>
242 )}
243
244 <Flex flexDirection="column" gap=".5em" className={Margins.top16}>
245 <Forms.FormText>
246 Go to the <Link href="https:class="ts-cmt">//discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
247 get the application ID.
248 </Forms.FormText>
249 <Forms.FormText>
250 Upload images in the Rich Presence tab to get the image keys.
251 </Forms.FormText>
252 <Forms.FormText>
253 If you want to use an image link, download your image and reupload the image to <Link href="https:class="ts-cmt">//imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
254 </Forms.FormText>
255 <Forms.FormText>
256 You can&#039;t see your own buttons on your profile, but everyone else can see it fine.
257 </Forms.FormText>
258 <Forms.FormText>
259 Some weird unicode text ("fonts" ๐–‘๐–Ž๐–๐–Š ๐–™๐–๐–Ž๐–˜) may cause the rich presence to not show up, try using normal letters instead.
260 </Forms.FormText>
261 </Flex>
262
263 <Divider className={Margins.top8} />
264
265 <div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-muted)" }}>
266 {activity && <ActivityView
267 activity={activity}
268 user={UserStore.getCurrentUser()}
269 currentUser={UserStore.getCurrentUser()}
270 />}
271 </div>
272 </>
273 );
274 }
275});
276