Merge branch 'main' of https://www.coastalcommits.com/CoastalCommitsArchival/github.com-Vendicated-Vencord
This commit is contained in:
commit
2dd955f57c
22 changed files with 944 additions and 52 deletions
|
@ -27,6 +27,7 @@ export { PlainSettings, Settings };
|
|||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
|
@ -85,7 +86,7 @@ async function init() {
|
|||
|
||||
syncSettings();
|
||||
|
||||
if (!IS_WEB) {
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
@ -103,16 +104,13 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Settings.notifyAboutUpdates)
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
}), 10_000);
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: openUpdaterModal!
|
||||
}), 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import plugins from "~plugins";
|
|||
|
||||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
|
@ -78,8 +77,7 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
autoUpdate: true,
|
||||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
|
|||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
|
@ -29,7 +30,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
|
|||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
|
@ -38,21 +39,24 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||
await action();
|
||||
} catch (e: any) {
|
||||
UpdateLogger.error("Failed to update", e);
|
||||
|
||||
let err: string;
|
||||
if (!e) {
|
||||
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
} else if (e.code && e.cmd) {
|
||||
const { code, path, cmd, stderr } = e;
|
||||
|
||||
if (code === "ENOENT")
|
||||
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
else {
|
||||
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||
}
|
||||
|
||||
} else {
|
||||
var err = "An unknown error occurred. See the console for more info.";
|
||||
err = "An unknown error occurred. See the console for more info.";
|
||||
}
|
||||
|
||||
Alerts.show({
|
||||
title: "Oops!",
|
||||
body: (
|
||||
|
@ -186,7 +190,7 @@ function Newer(props: CommonProps) {
|
|||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
|
@ -203,14 +207,6 @@ function Updater() {
|
|||
return (
|
||||
<SettingsTab title="Vencord Updater">
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a notification on startup"
|
||||
disabled={settings.autoUpdate}
|
||||
>
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.autoUpdate}
|
||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||
|
@ -253,3 +249,20 @@ function Updater() {
|
|||
}
|
||||
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-updater-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||
<UpdaterTab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), "UpdaterModal"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -65,3 +65,11 @@
|
|||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.vc-updater-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
||||
.vc-updater-modal-close-button {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
|||
);
|
||||
}
|
||||
|
||||
const onError = onlyOnce(handleComponentFailed);
|
||||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType, tab: string) {
|
||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError,
|
||||
onError: handleSettingsTabError,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
/* Patch the badge list component on user profiles */
|
||||
{
|
||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||
find: 'id:"premium",',
|
||||
replacement: [
|
||||
{
|
||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||
|
|
5
src/plugins/betterSessions/README.md
Normal file
5
src/plugins/betterSessions/README.md
Normal file
|
@ -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)
|
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
|
@ -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>
|
||||
);
|
||||
}
|
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
|
@ -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 >
|
||||
);
|
||||
}
|
106
src/plugins/betterSessions/components/icons.tsx
Normal file
106
src/plugins/betterSessions/components/icons.tsx
Normal file
|
@ -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"));
|
227
src/plugins/betterSessions/index.tsx
Normal file
227
src/plugins/betterSessions/index.tsx
Normal file
|
@ -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, 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 (
|
||||
<>
|
||||
<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);
|
||||
}
|
||||
});
|
|
@ -16,20 +16,17 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
export interface SessionInfo {
|
||||
session: {
|
||||
id_hash: string;
|
||||
approx_last_used_time: Date;
|
||||
client_info: {
|
||||
os: string;
|
||||
platform: string;
|
||||
location: string;
|
||||
};
|
||||
},
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ShowTimeouts",
|
||||
description: "Display member timeout icons in chat regardless of permissions.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
replacement: {
|
||||
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
|
||||
replace: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
export type Session = SessionInfo["session"];
|
90
src/plugins/betterSessions/utils.ts
Normal file
90
src/plugins/betterSessions/utils.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -92,8 +92,9 @@ export default definePlugin({
|
|||
match: /(?<=getUserProfile\(\i\){return )(\i\[\i\])/,
|
||||
replace: "$self.colorDecodeHook($1)"
|
||||
}
|
||||
}, {
|
||||
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
|
||||
},
|
||||
{
|
||||
find: ".USER_SETTINGS_RESET_PROFILE_THEME",
|
||||
replacement: {
|
||||
match: /RESET_PROFILE_THEME}\)(?<=color:(\i),.{0,500}?color:(\i),.{0,500}?)/,
|
||||
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
|
||||
|
|
7
src/plugins/implicitRelationships/README.md
Normal file
7
src/plugins/implicitRelationships/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# ImplicitRelationships
|
||||
|
||||
Shows your implicit relationships in the Friends tab.
|
||||
|
||||
Implicit relationships on Discord are people with whom you've frecently interacted and share a mutual server; even though Discord thinks you should be friends with them, you haven't added them as friends.
|
||||
|
||||
![](https://camo.githubusercontent.com/6927161ee0c933f7ef6d61f243cca3e6ea4c8db9d1becd8cbf73c45e1bd0d127/68747470733a2f2f692e646f6c66692e65732f7055447859464662674d2e706e673f6b65793d736e3950343936416c32444c7072)
|
182
src/plugins/implicitRelationships/index.ts
Normal file
182
src/plugins/implicitRelationships/index.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByProps, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
||||
|
||||
interface UserAffinity {
|
||||
user_id: string;
|
||||
affinity: number;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ImplicitRelationships",
|
||||
description: "Shows your implicit relationships in the Friends tab.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
// Counts header
|
||||
{
|
||||
find: ".FRIENDS_ALL_HEADER",
|
||||
replacement: {
|
||||
match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/,
|
||||
replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
|
||||
},
|
||||
},
|
||||
// No friends page
|
||||
{
|
||||
find: "FriendsEmptyState: Invalid empty state",
|
||||
replacement: {
|
||||
match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/,
|
||||
replace: "case $1.ONLINE:case $1.IMPLICIT:"
|
||||
},
|
||||
},
|
||||
// Sections header
|
||||
{
|
||||
find: ".FRIENDS_SECTION_ONLINE",
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.Messages\.BLOCKED\}\)/,
|
||||
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
|
||||
},
|
||||
},
|
||||
// Sections content
|
||||
{
|
||||
find: '"FriendsStore"',
|
||||
replacement: {
|
||||
match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/,
|
||||
replace: ";case $1.IMPLICIT:return $2.type===5"
|
||||
},
|
||||
},
|
||||
// Piggyback relationship fetch
|
||||
{
|
||||
find: ".fetchRelationships()",
|
||||
replacement: {
|
||||
match: /(\i\.\i)\.fetchRelationships\(\)/,
|
||||
// This relationship fetch is actually completely useless, but whatevs
|
||||
replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()"
|
||||
},
|
||||
},
|
||||
// Modify sort -- thanks megu for the patch (from sortFriendRequests)
|
||||
{
|
||||
find: "getRelationshipCounts(){",
|
||||
replacement: {
|
||||
predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
|
||||
match: /\.sortBy\(\i=>\i\.comparator\)/,
|
||||
replace: "$&.sortBy((row) => $self.sortList(row))"
|
||||
}
|
||||
},
|
||||
|
||||
// Add support for the nonce parameter to Discord's shitcode
|
||||
{
|
||||
find: ".REQUEST_GUILD_MEMBERS",
|
||||
replacement: {
|
||||
match: /\.send\(8,{/,
|
||||
replace: "$&nonce:arguments[1].nonce,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "GUILD_MEMBERS_REQUEST:",
|
||||
replacement: {
|
||||
match: /presences:!!(\i)\.presences/,
|
||||
replace: "$&,nonce:$1.nonce"
|
||||
},
|
||||
},
|
||||
{
|
||||
find: ".not_found",
|
||||
replacement: {
|
||||
match: /notFound:(\i)\.not_found/,
|
||||
replace: "$&,nonce:$1.nonce"
|
||||
},
|
||||
}
|
||||
],
|
||||
settings: definePluginSettings(
|
||||
{
|
||||
sortByAffinity: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Whether to sort implicit relationships by their affinity to you.",
|
||||
restartNeeded: true
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
sortList(row: any) {
|
||||
return row.type === 5
|
||||
? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
|
||||
: row.comparator;
|
||||
},
|
||||
|
||||
async fetchImplicitRelationships() {
|
||||
// Implicit relationships are defined as users that you:
|
||||
// 1. Have an affinity for
|
||||
// 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships
|
||||
// 3. Have a mutual guild with
|
||||
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
|
||||
const nonFriendAffinities = Array.from(userAffinities).filter(
|
||||
id => !RelationshipStore.getRelationshipType(id)
|
||||
);
|
||||
|
||||
// I would love to just check user cache here (falling back to the gateway of course)
|
||||
// However, users in user cache may just be there because they share a DM or group DM with you
|
||||
// So there's no guarantee that a user being in user cache means they have a mutual with you
|
||||
// To get around this, we request users we have DMs with, and ignore them below if we don't get them back
|
||||
const dmUserIds = new Set(
|
||||
Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients)
|
||||
);
|
||||
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id));
|
||||
const allGuildIds = Object.keys(GuildStore.getGuilds());
|
||||
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
||||
|
||||
// OP 8 Request Guild Members allows 100 user IDs at a time
|
||||
const ignore = new Set(toRequest);
|
||||
const relationships = RelationshipStore.getRelationships();
|
||||
const callback = ({ nonce, members }) => {
|
||||
if (nonce !== sentNonce) return;
|
||||
members.forEach(member => {
|
||||
ignore.delete(member.user.id);
|
||||
});
|
||||
|
||||
nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
|
||||
RelationshipStore.emitChange();
|
||||
if (--count === 0) {
|
||||
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback);
|
||||
}
|
||||
};
|
||||
|
||||
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback);
|
||||
for (let i = 0; i < toRequest.length; i += 100) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GUILD_MEMBERS_REQUEST",
|
||||
guildIds: allGuildIds,
|
||||
userIds: toRequest.slice(i, i + 100),
|
||||
nonce: sentNonce,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
const { FriendsSections } = findByProps("FriendsSections");
|
||||
FriendsSections.IMPLICIT = "IMPLICIT";
|
||||
}
|
||||
});
|
11
src/plugins/showHiddenThings/README.md
Normal file
11
src/plugins/showHiddenThings/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# ShowHiddenThings
|
||||
|
||||
Displays various moderator-only elements regardless of permissions.
|
||||
|
||||
## Features
|
||||
|
||||
- Show member timeout icons in chat
|
||||
![](https://github.com/Vendicated/Vencord/assets/47677887/75e1f6ba-8921-4188-9c2d-c9c3f9d07101)
|
||||
|
||||
- Show the invites paused tooltip in the server list
|
||||
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
|
61
src/plugins/showHiddenThings/index.ts
Normal file
61
src/plugins/showHiddenThings/index.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showTimeouts: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show member timeout icons in chat.",
|
||||
default: true,
|
||||
},
|
||||
showInvitesPaused: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show the invites paused tooltip in the server list.",
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
|
||||
export default definePlugin({
|
||||
name: "ShowHiddenThings",
|
||||
tags: ["ShowTimeouts", "ShowInvitesPaused"],
|
||||
description: "Displays various moderator-only elements regardless of permissions.",
|
||||
authors: [Devs.Dolfies],
|
||||
patches: [
|
||||
{
|
||||
find: "showCommunicationDisabledStyles",
|
||||
predicate: () => settings.store.showTimeouts,
|
||||
replacement: {
|
||||
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
|
||||
replace: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
find: "useShouldShowInvitesDisabledNotif:",
|
||||
predicate: () => settings.store.showInvitesPaused,
|
||||
replacement: {
|
||||
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
||||
replace: "true",
|
||||
},
|
||||
}
|
||||
],
|
||||
settings,
|
||||
});
|
|
@ -42,7 +42,7 @@ export default definePlugin({
|
|||
find: "getRelationshipCounts(){",
|
||||
replacement: {
|
||||
match: /\.sortBy\(\i=>\i\.comparator\)/,
|
||||
replace: ".sortBy((row) => $self.sortList(row))"
|
||||
replace: "$&.sortBy((row) => $self.sortList(row))"
|
||||
}
|
||||
}, {
|
||||
find: ".Messages.FRIEND_REQUEST_CANCEL",
|
||||
|
|
45
src/plugins/streamerModeOnStream/index.ts
Normal file
45
src/plugins/streamerModeOnStream/index.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2024 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 { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { FluxDispatcher, UserStore } from "@webpack/common";
|
||||
|
||||
interface StreamEvent {
|
||||
streamKey: string;
|
||||
}
|
||||
|
||||
function toggleStreamerMode({ streamKey }: StreamEvent, value: boolean) {
|
||||
if (!streamKey.endsWith(UserStore.getCurrentUser().id)) return;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "STREAMER_MODE_UPDATE",
|
||||
key: "enabled",
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "StreamerModeOnStream",
|
||||
description: "Automatically enables streamer mode when you start streaming in Discord",
|
||||
authors: [Devs.Kodarru],
|
||||
flux: {
|
||||
STREAM_CREATE: d => toggleStreamerMode(d, true),
|
||||
STREAM_DELETE: d => toggleStreamerMode(d, false)
|
||||
}
|
||||
});
|
|
@ -120,7 +120,7 @@ export default definePlugin({
|
|||
find: 'className:"mention"',
|
||||
replacement: {
|
||||
// mention = { react: function (data, parse, props) { if (data.userId == null) return RoleMention() else return UserMention()
|
||||
match: /react(?=\(\i,\i,\i\).{0,50}return null==.{0,70}\?\(0,\i\.jsx\)\((\i\.\i),.+?jsx\)\((\i\.\i),\{className:"mention")/,
|
||||
match: /react(?=\(\i,\i,\i\).{0,100}return null==.{0,70}\?\(0,\i\.jsx\)\((\i\.\i),.+?jsx\)\((\i\.\i),\{className:"mention")/,
|
||||
// react: (...args) => OurWrapper(RoleMention, UserMention, ...args), originalReact: theirFunc
|
||||
replace: "react:(...args)=>$self.renderMention($1,$2,...args),originalReact"
|
||||
}
|
||||
|
|
|
@ -426,6 +426,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "newwares",
|
||||
id: 421405303951851520n
|
||||
},
|
||||
Kodarru: {
|
||||
name: "Kodarru",
|
||||
id: 785227396218748949n
|
||||
},
|
||||
nakoyasha: {
|
||||
name: "nakoyasha",
|
||||
id: 222069018507345921n
|
||||
|
@ -437,6 +441,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
Byron: {
|
||||
name: "byeoon",
|
||||
id: 1167275288036655133n
|
||||
},
|
||||
Kaitlyn: {
|
||||
name: "kaitlyn",
|
||||
id: 306158896630988801n
|
||||
},
|
||||
PolisanTheEasyNick: {
|
||||
name: "Oleh Polisan",
|
||||
id: 242305263313485825n
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
|
Loading…
Reference in a new issue