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.
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./styles.css";8
9
import { showNotification } from "@api/Notifications";10
import { definePluginSettings } from "@api/Settings";11
import ErrorBoundary from "@components/ErrorBoundary";12
import { Paragraph } from "@components/Paragraph";13
import { Devs } from "@utils/constants";14
import definePlugin, { OptionType } from "@utils/types";15
import { findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from "@webpack";16
import { Constants, React, RestAPI, SettingsRouter, Tooltip } from "@webpack/common";17
18
import { NewButton, RenameButton } from "./components/RenameButton";19
import { Session, SessionInfo } from "./types";20
import { cl, fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";21
22
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");23
const TimestampClasses = findCssClassesLazy("timestamp", "blockquoteContainer");24
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");25
26
const 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: true32
},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: true38
}39
});40
41
export 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 time78
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
<BlobMask118
isFolder119
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: 20129
}}130
>131
<div132
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_SESSIONS144
});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 cache166
lastFetchedHashes.forEach(idHash => {167
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });168
});169
170
// Delete removed sessions from cache171
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