/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton";
import { Session, SessionInfo } from "./types";
import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");
const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
const SessionIconClasses = findByPropsLazy("sessionIcon");
const BlobMask = findExportedComponentLazy("BlobMask");
const settings = definePluginSettings({
backgroundCheck: {
type: OptionType.BOOLEAN,
description: "Check for new sessions in the background, and display notifications when they are detected",
default: false,
restartNeeded: true
},
checkInterval: {
description: "How often to check for new sessions in the background (if enabled), in minutes",
type: OptionType.NUMBER,
default: 20,
restartNeeded: true
}
});
export default definePlugin({
name: "BetterSessions",
description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.",
authors: [Devs.amia],
settings: settings,
patches: [
{
find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT",
replacement: [
// Replace children with a single label with state
{
match: /({variant:"eyebrow",className:\i\.sessionInfoRow,children:).{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:\i\[\d+\]}\)\]}\)\]/,
replace: "$1$self.renderName(arguments[0])"
},
{
match: /({variant:"text-sm\/medium",className:\i\.sessionInfoRow,children:.{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:)(\i\[\d+\])}/,
replace: "$1$self.renderTimestamp({ ...arguments[0], timeLabel: $2 })}"
},
// Replace the icon
{
match: /\.currentSession:null\),children:\[(?<=,icon:(\i)\}.+?)/,
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
}
]
},
{
// Add the ability to change BlobMask's lower badge height
// (it allows changing width so we can mirror that logic)
find: "this.getBadgePositionInterpolation(",
replacement: {
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
}
}
],
renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {
const savedSession = savedSessionsCache.get(session.id_hash);
const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));
const [title, setTitle] = state;
// Show a "NEW" badge if the session is seen for the first time
return (
<>
{title}
{(savedSession == null || savedSession.isNew) && (
NEW
)}
>
);
}, { noop: true }),
renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {
return (
{props => (
{timeLabel}
)}
);
}, { noop: true }),
renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType; }) => {
const PlatformIcon = GetPlatformIcon(session.client_info.platform);
return (
}
lowerBadgeWidth={20}
lowerBadgeHeight={20}
>
);
}, { noop: true }),
async checkNewSessions() {
const data = await RestAPI.get({
url: "/auth/sessions"
});
for (const session of data.body.user_sessions) {
if (savedSessionsCache.has(session.id_hash)) continue;
savedSessionsCache.set(session.id_hash, { name: "", isNew: true });
showNotification({
title: "BetterSessions",
body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,
permanent: true,
onClick: () => UserSettingsModal.open("Sessions")
});
}
saveSessionsToDataStore();
},
flux: {
USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {
const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash);
// Add new sessions to cache
lastFetchedHashes.forEach(idHash => {
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });
});
// Delete removed sessions from cache
if (lastFetchedHashes.length > 0) {
savedSessionsCache.forEach((_, idHash) => {
if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);
});
}
// Dismiss the "NEW" badge of all sessions.
// Since the only way for a session to be marked as "NEW" is going to the Devices tab,
// closing the settings means they've been viewed and are no longer considered new.
savedSessionsCache.forEach(data => {
data.isNew = false;
});
saveSessionsToDataStore();
}
},
async start() {
await fetchNamesFromDataStore();
this.checkNewSessions();
if (settings.store.backgroundCheck) {
this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);
}
},
stop() {
clearInterval(this.checkInterval);
}
});