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
import "./styles.css";2
3
import { showNotification } from "@api/Notifications";4
import { definePluginSettings } from "@api/Settings";5
import ErrorBoundary from "@components/ErrorBoundary";6
import { Paragraph } from "@components/Paragraph";7
import { Devs } from "@utils/constants";8
import definePlugin, { OptionType } from "@utils/types";9
import { findComponentByCodeLazy, findCssClassesLazy, findStoreLazy } from "@webpack";10
import { Constants, React, RestAPI, SettingsRouter, Tooltip } from "@webpack/common";11
12
import { NewButton, RenameButton } from "./components/RenameButton";13
import { Session, SessionInfo } from "./types";14
import { cl, fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";15
16
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");17
const TimestampClasses = findCssClassesLazy("timestamp", "blockquoteContainer");18
const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");19
20
const 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: true26
},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: true32
}33
});34
35
export 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 time72
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
<BlobMask112
isFolder113
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: 20123
}}124
>125
<div126
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_SESSIONS138
});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 cache160
lastFetchedHashes.forEach(idHash => {161
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });162
});163
164
// Delete removed sessions from cache165
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