New Plugin: BetterSessions (#1324)

Co-authored-by: V <vendicated@riseup.net>
main
Amia 5 months ago committed by GitHub
parent af67ddefa1
commit 5cf014cb06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
# BetterSessions
Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.
![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8)

@ -0,0 +1,37 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openModal } from "@utils/modal";
import { Button } from "@webpack/common";
import { SessionInfo } from "../types";
import { RenameModal } from "./RenameModal";
export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
return (
<Button
look={Button.Looks.LINK}
color={Button.Colors.LINK}
size={Button.Sizes.NONE}
style={{
paddingTop: "0px",
paddingBottom: "0px",
top: "-2px"
}}
onClick={() =>
openModal(props => (
<RenameModal
props={props}
session={session}
state={state}
/>
))
}
>
Rename
</Button>
);
}

@ -0,0 +1,94 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Button, Forms, React, TextInput } from "@webpack/common";
import { KeyboardEvent } from "react";
import { SessionInfo } from "../types";
import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "../utils";
export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
const [title, setTitle] = state;
const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? "");
function onSaveClick() {
savedSessionsCache.set(session.id_hash, { name: value, isNew: false });
if (value !== "") {
setTitle(`${value}*`);
} else {
setTitle(getDefaultName(session.client_info));
}
saveSessionsToDataStore();
props.onClose();
}
return (
<ModalRoot {...props}>
<ModalHeader>
<Forms.FormTitle tag="h4">Rename</Forms.FormTitle>
</ModalHeader>
<ModalContent>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>New device name</Forms.FormTitle>
<TextInput
style={{ marginBottom: "10px" }}
placeholder={getDefaultName(session.client_info)}
value={value}
onChange={setValue}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSaveClick();
}
}}
/>
<Button
style={{
marginBottom: "20px",
paddingLeft: "1px",
paddingRight: "1px",
opacity: 0.6
}}
look={Button.Looks.LINK}
color={Button.Colors.LINK}
size={Button.Sizes.NONE}
onClick={() => setValue("")}
>
Reset Name
</Button>
</ModalContent>
<ModalFooter>
<Button
color={Button.Colors.BRAND}
onClick={onSaveClick}
>
Save
</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.LINK}
onClick={() => props.onClose()}
>
Cancel
</Button>
</ModalFooter>
</ModalRoot >
);
}

@ -0,0 +1,106 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { LazyComponent } from "@utils/react";
import { findByCode } from "@webpack";
import { SVGProps } from "react";
export const DiscordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
</svg>
);
export const ChromeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M188.8,255.93A67.2,67.2,0,1,0,256,188.75,67.38,67.38,0,0,0,188.8,255.93Z" />
<path d="M476.75,217.79s0,0,0,.05a206.63,206.63,0,0,0-7-28.84h-.11a202.16,202.16,0,0,1,7.07,29h0a203.5,203.5,0,0,0-7.07-29H314.24c19.05,17,31.36,40.17,31.36,67.05a86.55,86.55,0,0,1-12.31,44.73L231,478.45a2.44,2.44,0,0,1,0,.27V479h0v-.26A224,224,0,0,0,256,480c6.84,0,13.61-.39,20.3-1a222.91,222.91,0,0,0,29.78-4.74C405.68,451.52,480,362.4,480,255.94A225.25,225.25,0,0,0,476.75,217.79Z" />
<path d="M256,345.5c-33.6,0-61.6-17.91-77.29-44.79L76,123.05l-.14-.24A224,224,0,0,0,207.4,474.55l0-.05,77.69-134.6A84.13,84.13,0,0,1,256,345.5Z" />
<path d="M91.29,104.57l77.35,133.25A89.19,89.19,0,0,1,256,166H461.17a246.51,246.51,0,0,0-25.78-43.94l.12.08A245.26,245.26,0,0,1,461.17,166h.17a245.91,245.91,0,0,0-25.66-44,2.63,2.63,0,0,1-.35-.26A223.93,223.93,0,0,0,91.14,104.34l.14.24Z" />
</svg>
);
export const EdgeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.58-1.6-.2-.83-.2-1.67 0-1 .32-1.96.33-.97.87-1.8.14.95.55 1.77.41.82 1.02 1.5.6.68 1.38 1.21.78.54 1.64.9.86.36 1.77.56.92.2 1.8.2 1.12 0 2.18-.24 1.06-.23 2.06-.72l.2-.1.2-.05zm-15.5-1.27q0 1.1.27 2.15.27 1.06.78 2.03.51.96 1.24 1.77.74.82 1.66 1.4-1.47-.2-2.8-.74-1.33-.55-2.48-1.37-1.15-.83-2.08-1.9-.92-1.07-1.58-2.33T.36 14.94Q0 13.54 0 12.06q0-.81.32-1.49.31-.68.83-1.23.53-.55 1.2-.96.66-.4 1.35-.66.74-.27 1.5-.39.78-.12 1.55-.12.7 0 1.42.1.72.12 1.4.35.68.23 1.32.57.63.35 1.16.83-.35 0-.7.07-.33.07-.65.23v-.02q-.63.28-1.2.74-.57.46-1.05 1.04-.48.58-.87 1.26-.38.67-.65 1.39-.27.71-.42 1.44-.15.72-.15 1.38zM11.96.06q1.7 0 3.33.39 1.63.38 3.07 1.15 1.43.77 2.62 1.93 1.18 1.16 1.98 2.7.49.94.76 1.96.28 1 .28 2.08 0 .89-.23 1.7-.24.8-.69 1.48-.45.68-1.1 1.22-.64.53-1.45.88-.54.24-1.11.36-.58.13-1.16.13-.42 0-.97-.03-.54-.03-1.1-.12-.55-.1-1.05-.28-.5-.19-.84-.5-.12-.09-.23-.24-.1-.16-.1-.33 0-.15.16-.35.16-.2.35-.5.2-.28.36-.68.16-.4.16-.95 0-1.06-.4-1.96-.4-.91-1.06-1.64-.66-.74-1.52-1.28-.86-.55-1.79-.89-.84-.3-1.72-.44-.87-.14-1.76-.14-1.55 0-3.06.45T.94 7.55q.71-1.74 1.81-3.13 1.1-1.38 2.52-2.35Q6.68 1.1 8.37.58q1.7-.52 3.58-.52Z" />
</svg>
);
export const FirefoxIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M130.22 127.548C130.38 127.558 130.3 127.558 130.22 127.548V127.548ZM481.64 172.898C471.03 147.398 449.56 119.898 432.7 111.168C446.42 138.058 454.37 165.048 457.4 185.168C457.405 185.306 457.422 185.443 457.45 185.578C429.87 116.828 383.098 89.1089 344.9 28.7479C329.908 5.05792 333.976 3.51792 331.82 4.08792L331.7 4.15792C284.99 30.1109 256.365 82.5289 249.12 126.898C232.503 127.771 216.219 131.895 201.19 139.035C199.838 139.649 198.736 140.706 198.066 142.031C197.396 143.356 197.199 144.87 197.506 146.323C197.7 147.162 198.068 147.951 198.586 148.639C199.103 149.327 199.76 149.899 200.512 150.318C201.264 150.737 202.096 150.993 202.954 151.071C203.811 151.148 204.676 151.045 205.491 150.768L206.011 150.558C221.511 143.255 238.408 139.393 255.541 139.238C318.369 138.669 352.698 183.262 363.161 201.528C350.161 192.378 326.811 183.338 304.341 187.248C392.081 231.108 368.541 381.784 246.951 376.448C187.487 373.838 149.881 325.467 146.421 285.648C146.421 285.648 157.671 243.698 227.041 243.698C234.541 243.698 255.971 222.778 256.371 216.698C256.281 214.698 213.836 197.822 197.281 181.518C188.434 172.805 184.229 168.611 180.511 165.458C178.499 163.75 176.392 162.158 174.201 160.688C168.638 141.231 168.399 120.638 173.51 101.058C148.45 112.468 128.96 130.508 114.8 146.428H114.68C105.01 134.178 105.68 93.7779 106.25 85.3479C106.13 84.8179 99.022 89.0159 98.1 89.6579C89.5342 95.7103 81.5528 102.55 74.26 110.088C57.969 126.688 30.128 160.242 18.76 211.318C14.224 231.701 12 255.739 12 263.618C12 398.318 121.21 507.508 255.92 507.508C376.56 507.508 478.939 420.281 496.35 304.888C507.922 228.192 481.64 173.82 481.64 172.898Z" />
</svg>
);
export const IEIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z" />
</svg>
);
export const OperaIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 496 512"
>
<path d="M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z" />
</svg>
);
export const SafariIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M274.69,274.69l-37.38-37.38L166,346ZM256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8ZM411.85,182.79l14.78-6.13A8,8,0,0,1,437.08,181h0a8,8,0,0,1-4.33,10.46L418,197.57a8,8,0,0,1-10.45-4.33h0A8,8,0,0,1,411.85,182.79ZM314.43,94l6.12-14.78A8,8,0,0,1,331,74.92h0a8,8,0,0,1,4.33,10.45l-6.13,14.78a8,8,0,0,1-10.45,4.33h0A8,8,0,0,1,314.43,94ZM256,60h0a8,8,0,0,1,8,8V84a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V68A8,8,0,0,1,256,60ZM181,74.92a8,8,0,0,1,10.46,4.33L197.57,94a8,8,0,1,1-14.78,6.12l-6.13-14.78A8,8,0,0,1,181,74.92Zm-63.58,42.49h0a8,8,0,0,1,11.31,0L140,128.72A8,8,0,0,1,140,140h0a8,8,0,0,1-11.31,0l-11.31-11.31A8,8,0,0,1,117.41,117.41ZM60,256h0a8,8,0,0,1,8-8H84a8,8,0,0,1,8,8h0a8,8,0,0,1-8,8H68A8,8,0,0,1,60,256Zm40.15,73.21-14.78,6.13A8,8,0,0,1,74.92,331h0a8,8,0,0,1,4.33-10.46L94,314.43a8,8,0,0,1,10.45,4.33h0A8,8,0,0,1,100.15,329.21Zm4.33-136h0A8,8,0,0,1,94,197.57l-14.78-6.12A8,8,0,0,1,74.92,181h0a8,8,0,0,1,10.45-4.33l14.78,6.13A8,8,0,0,1,104.48,193.24ZM197.57,418l-6.12,14.78a8,8,0,0,1-14.79-6.12l6.13-14.78A8,8,0,1,1,197.57,418ZM264,444a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V428a8,8,0,0,1,8-8h0a8,8,0,0,1,8,8Zm67-6.92h0a8,8,0,0,1-10.46-4.33L314.43,418a8,8,0,0,1,4.33-10.45h0a8,8,0,0,1,10.45,4.33l6.13,14.78A8,8,0,0,1,331,437.08Zm63.58-42.49h0a8,8,0,0,1-11.31,0L372,383.28A8,8,0,0,1,372,372h0a8,8,0,0,1,11.31,0l11.31,11.31A8,8,0,0,1,394.59,394.59ZM286.25,286.25,110.34,401.66,225.75,225.75,401.66,110.34ZM437.08,331h0a8,8,0,0,1-10.45,4.33l-14.78-6.13a8,8,0,0,1-4.33-10.45h0A8,8,0,0,1,418,314.43l14.78,6.12A8,8,0,0,1,437.08,331ZM444,264H428a8,8,0,0,1-8-8h0a8,8,0,0,1,8-8h16a8,8,0,0,1,8,8h0A8,8,0,0,1,444,264Z" />
</svg>
);
export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
<svg
{...props}
fill="currentColor"
viewBox="0 0 16 16"
>
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
</svg>
);
export const MobileIcon = LazyComponent(() => findByCode("M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38"));

@ -0,0 +1,227 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 } 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 = findByPropsLazy("getSessions");
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 (
<>
<span>{title}</span>
{(savedSession == null || savedSession.isNew) && (
<div
className="vc-plugins-badge"
style={{
backgroundColor: "#ED4245",
marginLeft: "2px"
}}
>
NEW
</div>
)}
<RenameButton session={session} state={state} />
</>
);
}, { noop: true }),
renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {
return (
<Tooltip text={session.approx_last_used_time.toLocaleString()} tooltipClassName={TimestampClasses.timestampTooltip}>
{props => (
<span {...props} className={TimestampClasses.timestamp}>
{timeLabel}
</span>
)}
</Tooltip>
);
}, { noop: true }),
renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType<any>; }) => {
const PlatformIcon = GetPlatformIcon(session.client_info.platform);
return (
<BlobMask
style={{ cursor: "unset" }}
selected={false}
lowerBadge={
<div
style={{
width: "20px",
height: "20px",
display: "flex",
justifyContent: "center",
alignItems: "center",
overflow: "hidden",
borderRadius: "50%",
backgroundColor: "var(--interactive-normal)",
color: "var(--background-secondary)",
}}
>
<PlatformIcon width={14} height={14} />
</div>
}
lowerBadgeWidth={20}
lowerBadgeHeight={20}
>
<div
className={SessionIconClasses.sessionIcon}
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
>
<DeviceIcon width={28} height={28} />
</div>
</BlobMask>
);
}, { 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);
}
});

@ -0,0 +1,32 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
export interface SessionInfo {
session: {
id_hash: string;
approx_last_used_time: Date;
client_info: {
os: string;
platform: string;
location: string;
};
},
current?: boolean;
}
export type Session = SessionInfo["session"];

@ -0,0 +1,90 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { DataStore } from "@api/index";
import { UserStore } from "@webpack/common";
import { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from "./components/icons";
import { SessionInfo } from "./types";
const getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`;
export const savedSessionsCache: Map<string, { name: string, isNew: boolean; }> = new Map();
export function getDefaultName(clientInfo: SessionInfo["session"]["client_info"]) {
return `${clientInfo.os} · ${clientInfo.platform}`;
}
export function saveSessionsToDataStore() {
return DataStore.set(getDataKey(), savedSessionsCache);
}
export async function fetchNamesFromDataStore() {
const savedSessions = await DataStore.get<Map<string, { name: string, isNew: boolean; }>>(getDataKey()) || new Map();
savedSessions.forEach((data, idHash) => {
savedSessionsCache.set(idHash, data);
});
}
export function GetOsColor(os: string) {
switch (os) {
case "Windows Mobile":
case "Windows":
return "#55a6ef"; // Light blue
case "Linux":
return "#cdcd31"; // Yellow
case "Android":
return "#7bc958"; // Green
case "Mac OS X":
case "iOS":
return ""; // Default to white/black (theme-dependent)
default:
return "#f3799a"; // Pink
}
}
export function GetPlatformIcon(platform: string) {
switch (platform) {
case "Discord Android":
case "Discord iOS":
case "Discord Client":
return DiscordIcon;
case "Android Chrome":
case "Chrome iOS":
case "Chrome":
return ChromeIcon;
case "Edge":
return EdgeIcon;
case "Firefox":
return FirefoxIcon;
case "Internet Explorer":
return IEIcon;
case "Opera Mini":
case "Opera":
return OperaIcon;
case "Mobile Safari":
case "Safari":
return SafariIcon;
case "BlackBerry":
case "Facebook Mobile":
case "Android Mobile":
return MobileIcon;
default:
return UnknownIcon;
}
}
Loading…
Cancel
Save