Skip to content

Commit 46b9c0c

Browse files
committed
You can now share your Discord activity to Nerimity users.
1 parent 7cc1f11 commit 46b9c0c

File tree

5 files changed

+256
-2
lines changed

5 files changed

+256
-2
lines changed

src/chat-api/events/connectionEvents.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { localRPC } from "@/common/LocalRPC";
1414
import { reactNativeAPI } from "@/common/ReactNative";
1515
import { useWindowProperties } from "@/common/useWindowProperties";
1616
import useChannelProperties from "../store/useChannelProperties";
17+
import { useDiscordActivityTracker } from "@/common/useDiscordActivityTracker";
1718

1819
export const onConnect = (socket: Socket, token?: string) => {
1920
const account = useAccount();
@@ -96,6 +97,11 @@ electronWindowAPI()?.rpcChanged((data) => {
9697
localRPC.onUpdateRPC = (data) => {
9798
if (!data) {
9899
emitActivityStatus(null);
100+
const programs = getStorageObject<ProgramWithExtras[]>(
101+
StorageKeys.PROGRAM_ACTIVITY_STATUS,
102+
[]
103+
);
104+
electronWindowAPI()?.restartActivityStatus(programs);
99105
}
100106
emitActivityStatus({ startedAt: Date.now(), ...data });
101107
};
@@ -227,4 +233,5 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => {
227233

228234
electronWindowAPI()?.restartRPCServer();
229235
localRPC.start();
236+
useDiscordActivityTracker().restart();
230237
};

src/common/LocalRPC.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class LocalRPC {
2929
this.onUpdateRPC(firstRPC.data);
3030
}
3131

32-
updateRPC(id: string, data: RPC) {
32+
updateRPC(id: string, data?: RPC) {
3333
if (!data) return this.removeRPC(id);
3434
const index = this.RPCs.findIndex((rpc) => rpc.id === id);
3535
if (index === -1) {

src/common/localStorage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export enum StorageKeys {
2424
voiceInputMode = "voiceInputMode",
2525
PTTBoundKeys = "pttBoundKeys",
2626
USE_TWITTER_EMBED = "useTwitterEmbed",
27+
DISCORD_USER_ID = "discordUserId",
2728
}
2829

2930
export function getStorageBoolean(
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { createSignal } from "solid-js";
2+
import {
3+
getStorageString,
4+
setStorageString,
5+
StorageKeys,
6+
} from "./localStorage";
7+
import { localRPC } from "./LocalRPC";
8+
9+
const URL = "https://supertiger.nerimity.com/trackdispresence";
10+
const NERIMITY_APP_ID = "1630300334100500480";
11+
12+
let ws: WebSocket | null = null;
13+
14+
interface FormattedPresence {
15+
status: string;
16+
activities: FormattedActivity[];
17+
}
18+
export interface FormattedActivity {
19+
name: string;
20+
createdTimestamp: number | null;
21+
details: string | null;
22+
state: string | null;
23+
syncId: string | null;
24+
url?: string | null;
25+
type: number;
26+
assets?: {
27+
largeText?: string | null;
28+
smallText?: string | null;
29+
largeImageUrl: string | null;
30+
smallImageUrl: string | null;
31+
largeImage?: string | null;
32+
smallImage?: string | null;
33+
};
34+
timestamps?: {
35+
start: number | null;
36+
end: number | null;
37+
} | null;
38+
}
39+
40+
const ActivityType = {
41+
PLAYING: 0,
42+
STREAMING: 1,
43+
LISTENING: 2,
44+
WATCHING: 3,
45+
CUSTOM: 4,
46+
COMPETING: 5,
47+
};
48+
const ActivityTypeToNameAndAction = (activity: FormattedActivity) => {
49+
switch (activity.type) {
50+
case ActivityType.PLAYING:
51+
return { name: activity.name || "Unknown", action: "Playing" };
52+
case ActivityType.STREAMING:
53+
return { name: activity.name || "Unknown", action: "Streaming" };
54+
case ActivityType.LISTENING:
55+
return { name: activity.name || "Unknown", action: "Listening to" };
56+
case ActivityType.WATCHING:
57+
return { name: activity.name || "Unknown", action: "Watching" };
58+
case ActivityType.CUSTOM:
59+
return { name: activity.state || "Unknown", action: "Custom" };
60+
case ActivityType.COMPETING:
61+
return { name: activity.name || "Unknown", action: "Competing in" };
62+
default:
63+
return { name: activity.name || "Unknown", action: "Playing" };
64+
}
65+
};
66+
export const useDiscordActivityTracker = () => {
67+
let intervalId: NodeJS.Timeout;
68+
69+
const start = () => {
70+
const userId = getStorageString(StorageKeys.DISCORD_USER_ID, "");
71+
if (!userId) return;
72+
if (ws) return;
73+
clearInterval(intervalId);
74+
ws = new WebSocket(URL + "/" + userId);
75+
ws.onopen = () => {
76+
console.log("discord activity tracker connected");
77+
startPingInterval();
78+
};
79+
ws.onmessage = (event) => {
80+
const rawData = event.data;
81+
const data = JSON.parse(rawData as string);
82+
if (data.error) {
83+
console.error(data.error);
84+
setStorageString(StorageKeys.DISCORD_USER_ID, "");
85+
return;
86+
}
87+
const activity = (data.activities as FormattedActivity[]).sort((a, b) => {
88+
const isASpotify = !!a.assets?.largeImage?.startsWith("spotify:");
89+
const isBSpotify = !!b.assets?.largeImage?.startsWith("spotify:");
90+
91+
// Ensure Spotify activities are placed at the end of the list
92+
if (isASpotify && !isBSpotify) {
93+
return 1; // a comes after b
94+
}
95+
if (!isASpotify && isBSpotify) {
96+
return -1; // a comes before b
97+
}
98+
99+
// If neither or both are Spotify, maintain the original order (or add another sorting criteria)
100+
return 0;
101+
})[0];
102+
if (!activity) {
103+
localRPC.updateRPC(NERIMITY_APP_ID);
104+
return;
105+
}
106+
let url = undefined;
107+
108+
const isSpotify = !!activity.assets?.largeImage?.startsWith("spotify:");
109+
110+
if (isSpotify && activity.syncId) {
111+
url = `https://open.spotify.com/track/${activity.syncId}`;
112+
}
113+
114+
console.log(`Activity Update: ${activity?.name || null}`);
115+
localRPC.updateRPC(NERIMITY_APP_ID, {
116+
startedAt: activity.timestamps?.start || undefined,
117+
endsAt: activity.timestamps?.end || undefined,
118+
imgSrc:
119+
activity.assets?.largeImageUrl ||
120+
activity.assets?.smallImageUrl ||
121+
undefined,
122+
title: activity.details || undefined,
123+
subtitle: activity.state || undefined,
124+
link: url || activity.url,
125+
...ActivityTypeToNameAndAction(activity),
126+
});
127+
};
128+
ws.onclose = () => {
129+
localRPC.updateRPC(NERIMITY_APP_ID);
130+
clearInterval(intervalId);
131+
setTimeout(() => {
132+
restart();
133+
}, 5000);
134+
};
135+
};
136+
137+
const startPingInterval = () => {
138+
intervalId = setInterval(() => {
139+
ws?.send("ping");
140+
}, 30000);
141+
};
142+
143+
const restart = () => {
144+
localRPC.updateRPC(NERIMITY_APP_ID);
145+
clearInterval(intervalId);
146+
if (ws) ws.close();
147+
ws = null;
148+
start();
149+
};
150+
return { start, restart };
151+
};

src/components/settings/ActivityStatus.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import DropDown, { DropDownItem } from "../ui/drop-down/DropDown";
2525
import Block from "../ui/settings-block/Block";
2626
import {
2727
getStorageObject,
28+
getStorageString,
29+
setStorageString,
2830
StorageKeys,
2931
useReactiveLocalStorage,
3032
} from "@/common/localStorage";
@@ -39,6 +41,7 @@ import { EmojiPicker } from "../ui/emoji-picker/EmojiPicker";
3941
import { Modal } from "../ui/modal";
4042
import { emojiShortcodeToUnicode } from "@/emoji";
4143
import { emojiToUrl } from "@/common/emojiToUrl";
44+
import { useDiscordActivityTracker } from "@/common/useDiscordActivityTracker";
4245

4346
const Container = styled("div")`
4447
display: flex;
@@ -74,7 +77,7 @@ const RPCAdContainer = styled("div")`
7477
border-radius: 8px;
7578
padding: 10px;
7679
77-
margin-bottom: 16px;
80+
margin-bottom: 6px;
7881
`;
7982

8083
const ExampleActivityContainer = styled("div")`
@@ -194,6 +197,7 @@ export default function WindowSettings() {
194197
</CustomLink>
195198
</FlexRow>
196199
</RPCAdContainer>
200+
<DiscordActivity />
197201

198202
<Show when={!isElectron}>
199203
<Notice
@@ -455,3 +459,94 @@ const EditActivityStatusModal = (props: {
455459
</Modal.Root>
456460
);
457461
};
462+
463+
const DiscordActivity = () => {
464+
const { createPortal } = useCustomPortal();
465+
const discordActivityTracker = useDiscordActivityTracker();
466+
const [userId, setUserId] = createSignal<string>(
467+
getStorageString(StorageKeys.DISCORD_USER_ID, "")
468+
);
469+
470+
const onBlur = () => {
471+
const id = userId()?.trim();
472+
if (!id) {
473+
setStorageString(StorageKeys.DISCORD_USER_ID, "");
474+
discordActivityTracker.restart();
475+
return;
476+
}
477+
createPortal((close) => {
478+
const onCloseClick = () => {
479+
setStorageString(StorageKeys.DISCORD_USER_ID, "");
480+
setUserId("");
481+
discordActivityTracker.restart();
482+
close();
483+
};
484+
const onJoinedClick = () => {
485+
setStorageString(StorageKeys.DISCORD_USER_ID, id);
486+
setUserId(id);
487+
discordActivityTracker.restart();
488+
close();
489+
};
490+
return (
491+
<DiscordServerJoinedConfirmModal
492+
close={onCloseClick}
493+
onJoinedClick={onJoinedClick}
494+
/>
495+
);
496+
});
497+
};
498+
return (
499+
<SettingsBlock
500+
label="Discord Activity"
501+
description="Share your Discord activity with users on Nerimity!"
502+
>
503+
<Input
504+
placeholder="Discord User Id"
505+
onText={setUserId}
506+
value={userId()}
507+
onBlur={onBlur}
508+
/>
509+
</SettingsBlock>
510+
);
511+
};
512+
513+
const DiscordServerJoinedConfirmModal = (props: {
514+
close: () => void;
515+
onJoinedClick: () => void;
516+
}) => {
517+
return (
518+
<Modal.Root
519+
close={props.close}
520+
doNotCloseOnBackgroundClick
521+
desktopMaxWidth={500}
522+
>
523+
<Modal.Header title="Discord Activity" icon="edit" />
524+
<Modal.Body>
525+
<FlexColumn padding={6} gap={6}>
526+
<Text>
527+
Please join the Discord server to enable Activity Sharing.
528+
<div>
529+
<a target="_blank" href="https://discord.gg/ggrd2wr4pe">
530+
https://discord.gg/ggrd2wr4pe
531+
</a>
532+
</div>
533+
</Text>
534+
</FlexColumn>
535+
</Modal.Body>
536+
<Modal.Footer>
537+
<Modal.Button
538+
label="Cancel"
539+
alert
540+
onClick={props.close}
541+
iconName="close"
542+
/>
543+
<Modal.Button
544+
label="I have joined"
545+
onClick={props.onJoinedClick}
546+
primary
547+
iconName="check"
548+
/>
549+
</Modal.Footer>
550+
</Modal.Root>
551+
);
552+
};

0 commit comments

Comments
 (0)