Merge branch 'dev' into betterQuickReact

This commit is contained in:
Sqaaakoi 2024-04-25 00:55:37 +12:00
commit 5f670a9010
No known key found for this signature in database
53 changed files with 1240 additions and 201 deletions

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.7.4",
"version": "1.7.8",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -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);
}

View file

@ -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: [],

View file

@ -315,7 +315,6 @@ export default function PluginSettings() {
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
<div className={InputStyles.inputWrapper}>
<Select
className={InputStyles.inputDefault}
options={[
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: SearchStatus.ENABLED },

View file

@ -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();
}
};

View file

@ -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;
}

View file

@ -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,
});
}

View file

@ -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".+?\}\);/,

View file

@ -45,14 +45,14 @@ export default definePlugin({
replacement: {
get match() {
switch (Settings.plugins.Settings.settingsLocation) {
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/;
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/;
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
}
},
replace: "...$self.makeSettingsCategories($1),$&"

View 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)

View 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>
);
}

View 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 >
);
}

View 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"));

View 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);
}
});

View file

@ -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"];

View 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;
}
}

View file

@ -22,12 +22,12 @@ import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors");
async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
@ -393,6 +393,8 @@ export default definePlugin({
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
<Forms.FormText>
@ -406,7 +408,7 @@ export default definePlugin({
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
</Forms.FormText>
<Forms.FormDivider />
<div style={{ width: "284px" }} className={Colors.profileColors}>
<div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}

View file

@ -54,9 +54,9 @@ const StickerExt = [, "png", "png", "json", "gif"] as const;
function getUrl(data: Data) {
if (data.t === "Emoji")
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}?size=4096&lossless=true`;
return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}`;
return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}?size=4096&lossless=true`;
}
async function fetchSticker(id: string) {
@ -130,7 +130,8 @@ function getGuildCandidates(data: Data) {
let count = 0;
for (const emoji of emojis)
if (emoji.animated === isAnimated) count++;
if (emoji.animated === isAnimated && !emoji.managed)
count++;
return count < emojiSlots;
}).sort((a, b) => a.name.localeCompare(b.name));
}

View file

@ -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})"

View file

@ -83,6 +83,6 @@ export default definePlugin({
if (!aIsFavorite && bIsFavorite) return 1;
return 0;
}).slice(0, query.results.emojis.sliceTo ?? 10);
}).slice(0, query.results.emojis.sliceTo ?? Infinity);
}
});

View file

@ -6,12 +6,23 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { filters, find, findByPropsLazy, handleModuleNotFound } from "@webpack";
import { React, RelationshipStore } from "@webpack/common";
const { Heading, Text } = findByPropsLazy("Heading", "Text");
const container = findByPropsLazy("memberSinceContainer");
// Workaround for module differing on stable & canary
// FIXME: remove once merged into stable
const getMemberSinceContainer = makeLazy(() => {
for (const name of ["memberSinceWrapper", "memberSinceContainer"]) {
const mod = find(filters.byProps(name), { isIndirect: true });
if (mod) return mod[name];
}
handleModuleNotFound("findByProps", "memberSinceWrapper/memberSinceContainer");
return "";
});
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");
const locale = findByPropsLazy("getLocale");
@ -48,7 +59,19 @@ export default definePlugin({
Friends Since
</Heading>
<div className={container.memberSinceContainer}>
<div className={getMemberSinceContainer()}>
{!!getCurrentChannel()?.guild_id && (
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="var(--interactive-normal)"
>
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg>
)}
<Text variant="text-sm/normal" className={clydeMoreInfo.body}>
{getCreatedAtDate(friendsSince, locale.getLocale())}
</Text>

View 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)

View 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";
}
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addAccessory } from "@api/MessageAccessories";
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
@ -389,4 +389,8 @@ export default definePlugin({
);
}, 4 /* just above rich embeds */);
},
stop() {
removeAccessory("messageLinkEmbed");
}
});

View file

@ -337,12 +337,12 @@ export default definePlugin({
{
// Attachment renderer
// Module 96063
find: ".removeAttachmentHoverButton",
find: ".removeMosaicItemHoverButton",
group: true,
replacement: [
{
match: /(className:\i,attachment:\i),/,
replace: "$1,attachment: {deleted},"
match: /(className:\i,item:\i),/,
replace: "$1,item: deleted,"
},
{
match: /\[\i\.obscured\]:.+?,/,

View file

@ -45,7 +45,7 @@ export default definePlugin({
patches: [
{
find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'

View file

@ -5,27 +5,15 @@
*/
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import style from "./styles.css?managed";
const settings = definePluginSettings({
inlineVideo: {
description: "Play videos without carousel modal",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
mediaLayoutType: {
description: "Choose media layout type",
type: OptionType.SELECT,
restartNeeded: true,
options: [
{ label: "STATIC, render loading image but image isn't resposive, no problem unless discord window width is too small", value: "STATIC", default: true },
{ label: "RESPONSIVE, image is responsive but not render loading image, cause messages shift when loaded", value: "RESPONSIVE" },
]
}
});
@ -39,15 +27,11 @@ export default definePlugin({
patches: [
{
find: ".oneByTwoLayoutThreeGrid",
replacement: [{
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
replace: "mediaLayoutType:$self.mediaLayoutType()",
},
{
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
replace: '"INVALID"',
}]
find: "isGroupableMedia:function()",
replacement: {
match: /=>"IMAGE"===\i\|\|"VIDEO"===\i;/,
replace: "=>false;"
}
},
{
find: "renderAttachments(",
@ -57,24 +41,5 @@ export default definePlugin({
replace: "$&$1.content_type?.startsWith('image/')&&"
}
},
{
find: "Messages.REMOVE_ATTACHMENT_TOOLTIP_TEXT",
replacement: {
match: /\i===\i\.\i\.MOSAIC/,
replace: "true"
}
}
],
mediaLayoutType() {
return settings.store.mediaLayoutType;
},
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
]
});

View file

@ -1,3 +0,0 @@
[class^="nonMediaAttachmentsContainer_"] [class*="messageAttachment_"] {
position: relative;
}

View file

@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React, Tooltip } from "@webpack/common";
import { Tooltip } from "@webpack/common";
const settings = definePluginSettings({
loop: {
@ -28,9 +28,9 @@ export default definePlugin({
settings,
patches: [
{
find: ".nonMediaAttachment]",
find: ".nonMediaMosaicItem]",
replacement: {
match: /\.nonMediaAttachment\]:!(\i).{0,10}children:\[(\S)/,
match: /\.nonMediaMosaicItem\]:!(\i).{0,10}children:\[(\S)/,
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
},
},

View file

@ -9,7 +9,7 @@ import { Settings } from "@api/Settings";
import { UserStore } from "@webpack/common";
import { DEFAULT_COLOR } from "./constants";
import { forceUpdate } from "./index";
import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index";
export interface Category {
id: string;
@ -106,7 +106,12 @@ export function categoryLen() {
}
export function getAllUncollapsedChannels() {
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
if (settings.store.pinOrder === PinOrder.LastMessage) {
const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds();
return categories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel)));
}
return categories.filter(c => !c.collapsed).flatMap(c => c.channels);
}
export function getSections() {

View file

@ -29,7 +29,7 @@ interface ChannelComponentProps {
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export let instance: any;
export const forceUpdate = () => instance?.props?._forceUpdate?.();
@ -236,7 +236,7 @@ export default definePlugin({
const category = categories[categoryIndex - 1];
if (!category) return false;
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
},
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {

View file

@ -36,25 +36,24 @@ export default definePlugin({
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
description: "Adds pronouns to user messages using pronoundb",
patches: [
// Add next to username (compact mode)
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /("span",{id:\i,className:\i,children:\i}\))/,
replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])"
}
},
// Patch the chat timestamp element (normal mode)
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/,
replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]"
}
replacement: [
// Add next to username (compact mode)
{
match: /("span",{id:\i,className:\i,children:\i}\))/,
replace: "$1, $self.CompactPronounsChatComponentWrapper(arguments[0])"
},
// Patch the chat timestamp element (normal mode)
{
match: /(?<=return\s*\(0,\i\.jsxs?\)\(.+!\i&&)(\(0,\i.jsxs?\)\(.+?\{.+?\}\))/,
replace: "[$1, $self.PronounsChatComponentWrapper(arguments[0])]"
}
]
},
// Patch the profile popout username header to use our pronoun hook instead of Discord's pronouns
{
find: ".userTagNoNickname",
find: ".pronouns,children",
replacement: [
{
match: /{user:(\i),[^}]*,pronouns:(\i),[^}]*}=\i;/,

View file

@ -18,9 +18,68 @@
import { findGroupChildrenByChildId } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Menu } from "@webpack/common";
import { findByPropsLazy } from "@webpack";
import { Button, Menu, Tooltip, useEffect, useState } from "@webpack/common";
const ChannelRowClasses = findByPropsLazy("modeConnected", "modeLocked", "icon");
let currentShouldViewServerHome = false;
const shouldViewServerHomeStates = new Set<React.Dispatch<React.SetStateAction<boolean>>>();
function ViewServerHomeButton() {
return (
<Tooltip text="View Server Home">
{tooltipProps => (
<Button
{...tooltipProps}
look={Button.Looks.BLANK}
size={Button.Sizes.NONE}
innerClassName={ChannelRowClasses.icon}
onClick={e => {
e.preventDefault();
currentShouldViewServerHome = true;
for (const setState of shouldViewServerHomeStates) {
setState(true);
}
}}
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="currentColor" d="m2.4 8.4 8.38-6.46a2 2 0 0 1 2.44 0l8.39 6.45a2 2 0 0 1-.79 3.54l-.32.07-.82 8.2a2 2 0 0 1-1.99 1.8H16a1 1 0 0 1-1-1v-5a3 3 0 0 0-6 0v5a1 1 0 0 1-1 1H6.31a2 2 0 0 1-1.99-1.8L3.5 12l-.32-.07a2 2 0 0 1-.79-3.54Z" />
</svg>
</Button>
)}
</Tooltip>
);
}
function useForceServerHome() {
const { forceServerHome } = settings.use(["forceServerHome"]);
const [shouldViewServerHome, setShouldViewServerHome] = useState(currentShouldViewServerHome);
useEffect(() => {
shouldViewServerHomeStates.add(setShouldViewServerHome);
return () => {
shouldViewServerHomeStates.delete(setShouldViewServerHome);
};
}, []);
return shouldViewServerHome || forceServerHome;
}
function useDisableViewServerHome() {
useEffect(() => () => {
currentShouldViewServerHome = false;
for (const setState of shouldViewServerHomeStates) {
setState(false);
}
}, []);
}
const settings = definePluginSettings({
forceServerHome: {
@ -30,12 +89,6 @@ const settings = definePluginSettings({
}
});
function useForceServerHome() {
const { forceServerHome } = settings.use(["forceServerHome"]);
return forceServerHome;
}
export default definePlugin({
name: "ResurrectHome",
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
@ -89,17 +142,40 @@ export default definePlugin({
{
find: "61eef9_2",
replacement: {
match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/,
replace: "if($self.useForceServerHome())return false;"
match: /getMutableGuildChannelsForGuild\(\i\);return\(0,\i\.useStateFromStores\).+?\]\)(?=}function)/,
replace: m => `${m}&&!$self.useForceServerHome()`
}
},
// Add View Server Home Button to Server Guide
{
find: "487e85_1",
replacement: {
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
replace: "badge:$self.ViewServerHomeButton({serverGuide:$1}),"
}
},
// Disable view Server Home override when the Server Home is unmouted
{
find: "69386d_5",
replacement: {
match: /location:"69386d_5".+?;/,
replace: "$&$self.useDisableViewServerHome();"
}
}
],
ViewServerHomeButton: ErrorBoundary.wrap(({ serverGuide }: { serverGuide?: boolean; }) => {
if (serverGuide !== true) return null;
return <ViewServerHomeButton />;
}),
useForceServerHome,
useDisableViewServerHome,
contextMenus: {
"guild-context"(children, props) {
const forceServerHome = useForceServerHome();
const { forceServerHome } = settings.use(["forceServerHome"]);
if (!props?.guild) return;

View file

@ -50,7 +50,7 @@ export default definePlugin({
patches: [
// Chat Mentions
{
find: "CLYDE_AI_MENTION_COLOR:null,",
find: 'location:"UserMention',
replacement: [
{
match: /user:(\i),channel:(\i).{0,400}?"@"\.concat\(.+?\)/,
@ -94,7 +94,7 @@ export default definePlugin({
find: "renderPrioritySpeaker",
replacement: [
{
match: /renderName\(\).{0,100}speaking:.{50,100}jsx.{5,10}{/,
match: /renderName\(\).{0,100}speaking:.{50,150}"div",{/,
replace: "$&...$self.getVoiceProps(this.props),"
}
],

View file

@ -35,7 +35,7 @@ const Section = findComponentByCodeLazy(".lastSection", "children:");
const ThemeStore = findStoreLazy("ThemeStore");
const platformHooks: { useLegacyPlatformType(platform: string): string; } = findByPropsLazy("useLegacyPlatformType");
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');
const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:");
const enum Spacing {
COMPACT,
@ -74,8 +74,8 @@ interface ConnectionPlatform {
icon: { lightSVG: string, darkSVG: string; };
}
const profilePopoutComponent = ErrorBoundary.wrap(({ user, displayProfile }: { user: User, displayProfile; }) =>
<ConnectionsComponent id={user.id} theme={getTheme(user, displayProfile).profileTheme} />
const profilePopoutComponent = ErrorBoundary.wrap((props: { user: User, displayProfile; }) =>
<ConnectionsComponent id={props.user.id} theme={getProfileThemeProps(props).theme} />
);
const profilePanelComponent = ErrorBoundary.wrap(({ id }: { id: string; }) =>

View file

@ -452,7 +452,7 @@ export default definePlugin({
{
// Filter hidden channels from GuildChannelStore.getChannels unless told otherwise
match: /(?<=getChannels\(\i)(\){.+?)return (.+?)}/,
replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??false);}`
replace: (_, rest, channels) => `,shouldIncludeHidden${rest}return $self.resolveGuildChannels(${channels},shouldIncludeHidden??arguments[0]==="@favorites");}`
}
]
},

View 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)

View 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,
});

View file

@ -49,7 +49,7 @@ export default definePlugin({
{
find: ".useCanSeeRemixBadge)",
replacement: {
match: /(?<=onContextMenu:\i,children:).*?\}/,
match: /(?<=onContextMenu:\i,children:).*?\)}/,
replace: "$self.renderUsername(arguments[0])}"
}
},

View file

@ -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",

View file

@ -18,7 +18,6 @@
import "./spotifyStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { debounce } from "@shared/debounce";
@ -376,17 +375,10 @@ export function Player() {
} as React.CSSProperties;
return (
<ErrorBoundary fallback={() => (
<div className="vc-spotify-fallback">
<p>Failed to render Spotify Modal :(</p>
<p >Check the console for errors</p>
</div>
)}>
<div id={cl("player")} style={exportTrackImageStyle}>
<Info track={track} />
<SeekBar />
<Controls />
</div>
</ErrorBoundary>
<div id={cl("player")} style={exportTrackImageStyle}>
<Info track={track} />
<SeekBar />
<Controls />
</div>
);
}

View file

@ -18,6 +18,7 @@
import { Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -49,10 +50,10 @@ export default definePlugin({
{
find: "showTaglessAccountPanel:",
replacement: {
// return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/,
// return [Player, Panel]
replace: "return [$self.renderPlayer(),$1]"
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?showTaglessAccountPanel:)/,
// react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...
replace: "$self.PanelWrapper,{VencordOriginal:$1,"
}
},
{
@ -78,6 +79,25 @@ export default definePlugin({
}
}
],
start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),
renderPlayer: () => <Player />
PanelWrapper({ VencordOriginal, ...props }) {
return (
<>
<ErrorBoundary
fallback={() => (
<div className="vc-spotify-fallback">
<p>Failed to render Spotify Modal :(</p>
<p >Check the console for errors</p>
</div>
)}
>
<Player />
</ErrorBoundary>
<VencordOriginal {...props} />
</>
);
}
});

View file

@ -17,9 +17,10 @@
*/
import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/react";
import definePlugin from "@utils/types";
import StartupTimingPage from "./StartupTimingPage";
export default definePlugin({
name: "StartupTimings",
description: "Adds Startup Timings to the Settings menu",
@ -31,5 +32,5 @@ export default definePlugin({
replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
}
}],
StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default)
StartupTimingPage
});

View file

@ -16,20 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findExportedComponentLazy, findStoreLazy } from "@webpack";
import { findComponentByCodeLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks";
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const TypingStore = findStoreLazy("TypingStore");
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
const enum IndicatorMode {
Dots = 1 << 0,
Avatars = 1 << 1
}
function getDisplayName(guildId: string, userId: string) {
const user = UserStore.getUser(userId);
return GuildMemberStore.getNick(guildId, userId) ?? (user as any).globalName ?? user.username;
@ -90,11 +98,24 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
return (
<Tooltip text={tooltipText!}>
{props => (
<div
{...props}
style={{ marginLeft: 6, height: 16, display: "flex", alignItems: "center", zIndex: 0, cursor: "pointer" }}
>
<ThreeDots dotRadius={3} themed={true} />
<div className="vc-typing-indicator" {...props}>
{((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && (
<UserSummaryItem
users={typingUsersArray.map(id => UserStore.getUser(id))}
guildId={guildId}
renderIcon={false}
max={3}
showDefaultAvatarsForNullUsers
showUserPopout
size={16}
className="vc-typing-indicator-avatars"
/>
)}
{((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (
<div className="vc-typing-indicator-dots">
<ThreeDots dotRadius={3} themed={true} />
</div>
)}
</div>
)}
</Tooltip>
@ -119,13 +140,22 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for blocked users.",
default: false
},
indicatorMode: {
type: OptionType.SELECT,
description: "How should the indicator be displayed?",
options: [
{ label: "Avatars and animated dots", value: IndicatorMode.Dots | IndicatorMode.Avatars, default: true },
{ label: "Animated dots", value: IndicatorMode.Dots },
{ label: "Avatars", value: IndicatorMode.Avatars },
],
}
});
export default definePlugin({
name: "TypingIndicator",
description: "Adds an indicator if someone is typing on a channel.",
authors: [Devs.Nuckyz, Devs.fawn],
authors: [Devs.Nuckyz, Devs.fawn, Devs.Sqaaakoi],
settings,
patches: [

View file

@ -0,0 +1,18 @@
.vc-typing-indicator {
display: flex;
align-items: center;
height: 20px;
}
.vc-typing-indicator-avatars {
margin-left: 6px;
}
.vc-typing-indicator-dots {
margin-left: 6px;
height: 16px;
display: flex;
align-items: center;
z-index: 0;
cursor: pointer;
}

View file

@ -0,0 +1,5 @@
# UnlockedAvatarZoom
Allows you to zoom in further in the image crop tool when changing your avatar
![](https://raw.githubusercontent.com/Vencord/plugin-assets/main/UnlockedAvatarZoom/demo.avif)

View file

@ -0,0 +1,35 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
zoomMultiplier: {
type: OptionType.SLIDER,
description: "Zoom multiplier",
markers: makeRange(2, 16),
default: 4,
},
});
export default definePlugin({
name: "UnlockedAvatarZoom",
description: "Allows you to zoom in further in the image crop tool when changing your avatar",
authors: [Devs.nakoyasha],
settings,
patches: [
{
find: ".Messages.AVATAR_UPLOAD_EDIT_MEDIA",
replacement: {
match: /maxValue:\d/,
replace: "maxValue:$self.settings.store.zoomMultiplier",
}
}
]
});

View file

@ -104,7 +104,7 @@ export default definePlugin({
},
// below username
{
find: ".USER_PROFILE_MODAL",
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Lazy-loaded
replacement: {
match: /\.body.+?displayProfile:\i}\),/,
replace: "$&$self.patchModal(arguments[0]),",

View file

@ -111,19 +111,28 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
export default definePlugin({
name: "ValidUser",
description: "Fix mentions for unknown users showing up as '<@343383572805058560>' (hover over a mention to fix it)",
description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)",
authors: [Devs.Ven],
tags: ["MentionCacheFix"],
patches: [{
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==\i\?\(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"
patches: [
{
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,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"
}
},
{
find: "unknownUserMentionPlaceholder:",
replacement: {
match: /unknownUserMentionPlaceholder:/,
replace: "$&false&&"
}
}
}],
],
renderMention(RoleMention, UserMention, data, parse, props) {
return (

View file

@ -174,7 +174,7 @@ export default definePlugin({
find: ".NITRO_BANNER,",
replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/,
match: /style:\{(?=backgroundImage:(null!=\i)\?"url\("\.concat\((\i),)/,
replace:
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'

View file

@ -426,9 +426,25 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "newwares",
id: 421405303951851520n
},
nakoyasha: {
name: "nakoyasha",
id: 222069018507345921n
},
Sqaaakoi: {
name: "Sqaaakoi",
id: 259558259491340288n,
id: 259558259491340288n
},
Byron: {
name: "byeoon",
id: 1167275288036655133n
},
Kaitlyn: {
name: "kaitlyn",
id: 306158896630988801n
},
PolisanTheEasyNick: {
name: "Oleh Polisan",
id: 242305263313485825n
}
} satisfies Record<string, Dev>);

View file

@ -116,8 +116,11 @@ export function proxyLazy<T>(factory: () => T, attempts = 5, isChild = false): T
attempts,
true
);
return Reflect.get(target[kGET](), p, receiver);
const lazyTarget = target[kGET]();
if (typeof lazyTarget === "object" || typeof lazyTarget === "function") {
return Reflect.get(lazyTarget, p, receiver);
}
throw new Error("proxyLazy called on a primitive value");
}
}) as any;
}

View file

@ -92,7 +92,7 @@ if (IS_DEV && IS_DISCORD_DESKTOP) {
}, 0);
}
function handleModuleNotFound(method: string, ...filter: unknown[]) {
export function handleModuleNotFound(method: string, ...filter: unknown[]) {
const err = new Error(`webpack.${method} found no module`);
logger.error(err, "Filter:", filter);
@ -406,13 +406,15 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
});
}
const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\((\[\i\.\i\(".+?"\).+?\])\)|Promise\.resolve\(\)).then\(\i\.bind\(\i,"(.+?)"\)\)/;
/**
* Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
* @returns A promise that resolves when the chunks were loaded
*/
export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) {
export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code);
if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@ -434,7 +436,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr
}
const [, rawChunkIds, entryPointId] = match;
if (!rawChunkIds || Number.isNaN(entryPointId)) {
if (Number.isNaN(entryPointId)) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -445,9 +447,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr
return;
}
const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]);
if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
await Promise.all(chunkIds.map(id => wreq.e(id)));
wreq(entryPointId);
}
@ -459,7 +463,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Pr
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
* @returns A function that returns a promise that resolves when the chunks were loaded, on first call
*/
export function extractAndLoadChunksLazy(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) {
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) {
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return () => extractAndLoadChunks(code, matcher);