Plugin

BetterSessions

Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.

Notifications Customisation Utility
index.tsx
Download

Source

src/plugins/betterSessions/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 "./styles.css";
8
9import { showNotification } from "@api/Notifications";
10import { definePluginSettings } from "@api/Settings";
11import ErrorBoundary from "@components/ErrorBoundary";
12import { Paragraph } from "@components/Paragraph";
13import { Devs } from "@utils/constants";
14import definePlugin, { OptionType } from "@utils/types";
15import { findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from "@webpack";
16import { Constants, React, RestAPI, SettingsRouter, Tooltip } from "@webpack/common";
17
18import { NewButton, RenameButton } from "./components/RenameButton";
19import { Session, SessionInfo } from "./types";
20import { cl, fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";
21
22const AuthSessionsStore = findStoreLazy("AuthSessionsStore");
23const TimestampClasses = findCssClassesLazy("timestamp", "blockquoteContainer");
24const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
25
26const settings = definePluginSettings({
27 backgroundCheck: {
28 type: OptionType.BOOLEAN,
29 description: "Check for new sessions in the background, and display notifications when they are detected",
30 default: false,
31 restartNeeded: true
32 },
33 checkInterval: {
34 description: "How often to check for new sessions in the background (if enabled), in minutes",
35 type: OptionType.NUMBER,
36 default: 20,
37 restartNeeded: true
38 }
39});
40
41export default definePlugin({
42 name: "BetterSessions",
43 description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.",
44 authors: [Devs.amia],
45 tags: ["Notifications", "Customisation", "Utility"],
46 settings: settings,
47
48 patches: [
49 {
50 find: "#{intl::AUTH_SESSIONS_OS_UNKNOWN}",
51 replacement: [
52 {
53 match: /(#{intl::AUTH_SESSIONS_ACTIVE_RECENTLY}.{0,230}role:"listitem",children:\[.{0,15},\{Icon:)\i/,
54 replace: "$1()=>$self.renderIcon(arguments[0])"
55 },
56 {
57 match: /("horizontal",gap:"xs",children:)\[.{0,250}"text-subtle",children:\i\}\)\]\}\),/,
58 replace: "$1$self.renderName(arguments[0])}),"
59 },
60 {
61 match: /("text-muted",children:)\i(?=\}\)\]\}\),.{0,120}\.client_info\?\.location)/,
62 replace: "$1$self.renderDescription(arguments[0])"
63 },
64 {
65 match: /:\i\(\i\.approx_last_used_time\).{0,40}\(0,\i\.jsxs?\)\(\i,\{/,
66 replace: "$&session:arguments[0]?.session,"
67 },
68 ]
69 },
70 ],
71
72 renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {
73 const savedSession = savedSessionsCache.get(session.id_hash);
74
75 const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));
76 const [title, setTitle] = state;
77 // Show a "NEW" badge if the session is seen for the first time
78 return (
79 <>
80 <Paragraph size="md" weight="semibold" color="text-strong">{title}</Paragraph>
81 <div className={cl("footer-buttons")}>
82 {(savedSession == null || savedSession.isNew) && (
83 <NewButton />
84 )}
85 <RenameButton session={session} state={state} />
86 </div>
87 </>
88 );
89 }, { noop: true }),
90
91 renderDescription: ErrorBoundary.wrap(({ session, description }: { session: Session, description: string; }) => {
92 const [label, timeLabel] = description.split(" \xb7 ");
93
94 return (
95 <div className={cl("description")}>
96 <Paragraph size="sm" weight="normal" color="text-muted">{label}</Paragraph>
97 {timeLabel && (
98 <>
99 {" \xb7 "}
100 <Tooltip text={session.approx_last_used_time.toLocaleString()}>
101 {props => (
102 <span {...props} className={TimestampClasses.timestamp}>
103 {timeLabel}
104 </span>
105 )}
106 </Tooltip>
107 </>
108 )}
109 </div>
110 );
111 }, { noop: true }),
112
113 renderIcon: ErrorBoundary.wrap(({ session, icon: DeviceIcon }: { session: Session; icon: React.ComponentType<any>; }) => {
114 const PlatformIcon = GetPlatformIcon(session.client_info.platform);
115
116 return (
117 <BlobMask
118 isFolder
119 style={{ cursor: "unset" }}
120 selected={false}
121 lowerBadge={
122 <div className={cl("lowerBadge")}>
123 <PlatformIcon width={14} height={14} className={cl("lowerBadge-icon")} />
124 </div>
125 }
126 lowerBadgeSize={{
127 width: 20,
128 height: 20
129 }}
130 >
131 <div
132 className={cl("icon")}
133 style={{ backgroundColor: GetOsColor(session.client_info.os) }}
134 >
135 <DeviceIcon size="md" color="currentColor" />
136 </div>
137 </BlobMask>
138 );
139 }, { noop: true }),
140
141 async checkNewSessions() {
142 const data = await RestAPI.get({
143 url: Constants.Endpoints.AUTH_SESSIONS
144 });
145
146 for (const session of data.body.user_sessions) {
147 if (savedSessionsCache.has(session.id_hash)) continue;
148
149 savedSessionsCache.set(session.id_hash, { name: "", isNew: true });
150 showNotification({
151 title: "BetterSessions",
152 body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,
153 permanent: true,
154 onClick: () => SettingsRouter.openUserSettings("sessions_panel")
155 });
156 }
157
158 saveSessionsToDataStore();
159 },
160
161 flux: {
162 USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {
163 const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash);
164
165 // Add new sessions to cache
166 lastFetchedHashes.forEach(idHash => {
167 if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });
168 });
169
170 // Delete removed sessions from cache
171 if (lastFetchedHashes.length > 0) {
172 savedSessionsCache.forEach((_, idHash) => {
173 if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);
174 });
175 }
176
177 // Dismiss the "NEW" badge of all sessions.
178 // Since the only way for a session to be marked as "NEW" is going to the Devices tab,
179 // closing the settings means they've been viewed and are no longer considered new.
180 savedSessionsCache.forEach(data => {
181 data.isNew = false;
182 });
183 saveSessionsToDataStore();
184 }
185 },
186
187 async start() {
188 await fetchNamesFromDataStore();
189
190 this.checkNewSessions();
191 if (settings.store.backgroundCheck) {
192 this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);
193 }
194 },
195
196 stop() {
197 clearInterval(this.checkInterval);
198 }
199});
200