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