Seaswimmer 2024-05-14 21:27:05 -04:00
commit c1bac12a1a
54 changed files with 801 additions and 287 deletions

View file

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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/mergeDefaults";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";

View file

@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
import { localStorage } from "@utils/localStorage"; import { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/mergeDefaults";
import { putCloudSettings } from "@utils/settingsSync"; import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";

View file

@ -21,7 +21,7 @@ import "./addonCard.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge"; import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch"; import { Switch } from "@components/Switch";
import { Text } from "@webpack/common"; import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react"; import type { MouseEventHandler, ReactNode } from "react";
const cl = classNameFactory("vc-addon-"); const cl = classNameFactory("vc-addon-");
@ -42,6 +42,8 @@ interface Props {
} }
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) { export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
const titleRef = useRef<HTMLDivElement>(null);
const titleContainerRef = useRef<HTMLDivElement>(null);
return ( return (
<div <div
className={cl("card", { "card-disabled": disabled })} className={cl("card", { "card-disabled": disabled })}
@ -51,7 +53,21 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
<div className={cl("header")}> <div className={cl("header")}>
<div className={cl("name-author")}> <div className={cl("name-author")}>
<Text variant="text-md/bold" className={cl("name")}> <Text variant="text-md/bold" className={cl("name")}>
{name}{isNew && <Badge text="NEW" color="#ED4245" />} <div ref={titleContainerRef} className={cl("title-container")}>
<div
ref={titleRef}
className={cl("title")}
onMouseOver={() => {
const title = titleRef.current!;
const titleContainer = titleContainerRef.current!;
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
}}
>
{name}
</div>
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
</Text> </Text>
{!!author && ( {!!author && (
<Text variant="text-md/normal" className={cl("author")}> <Text variant="text-md/normal" className={cl("author")}>

View file

@ -62,3 +62,36 @@
.vc-addon-author::before { .vc-addon-author::before {
content: "by "; content: "by ";
} }
.vc-addon-title-container {
width: 100%;
overflow: hidden;
height: 1.25em;
position: relative;
}
.vc-addon-title {
position: absolute;
inset: 0;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes vc-addon-title {
0% {
transform: translateX(0);
}
50% {
transform: translateX(var(--offset));
}
100% {
transform: translateX(0);
}
}
.vc-addon-title:hover {
overflow: visible;
animation: vc-addon-title var(--duration) linear infinite;
}

View file

@ -7,6 +7,7 @@
import type { Settings } from "@api/Settings"; import type { Settings } from "@api/Settings";
import { IpcEvents } from "@shared/IpcEvents"; import { IpcEvents } from "@shared/IpcEvents";
import { SettingsStore } from "@shared/SettingsStore"; import { SettingsStore } from "@shared/SettingsStore";
import { mergeDefaults } from "@utils/mergeDefaults";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { mkdirSync, readFileSync, writeFileSync } from "fs";
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
RendererSettings.setData(data, pathToNotify); RendererSettings.setData(data, pathToNotify);
}); });
export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE)); export interface NativeSettings {
plugins: {
[plugin: string]: {
[setting: string]: any;
};
};
}
const DefaultNativeSettings: NativeSettings = {
plugins: {}
};
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
mergeDefaults(nativeSettings, DefaultNativeSettings);
export const NativeSettings = new SettingsStore(nativeSettings);
NativeSettings.addGlobalChangeListener(() => { NativeSettings.addGlobalChangeListener(() => {
try { try {

View file

@ -26,7 +26,7 @@ import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab"; import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common"; import { i18n, React } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -36,7 +36,8 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu], authors: [Devs.Ven, Devs.Megu],
required: true, required: true,
patches: [{ patches: [
{
find: ".versionHash", find: ".versionHash",
replacement: [ replacement: [
{ {
@ -47,7 +48,10 @@ export default definePlugin({
} }
} }
] ]
}, { },
// Discord Stable
// FIXME: remove once change merged to stable
{
find: "Messages.ACTIVITY_SETTINGS", find: "Messages.ACTIVITY_SETTINGS",
replacement: { replacement: {
get match() { get match() {
@ -64,13 +68,23 @@ export default definePlugin({
}, },
replace: "...$self.makeSettingsCategories($1),$&" replace: "...$self.makeSettingsCategories($1),$&"
} }
}, { },
// Discord Canary
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
},
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL", find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: { replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/, match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;" replace: "$2.default.open($1);return;"
} }
}], }
],
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[], customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
@ -130,19 +144,43 @@ export default definePlugin({
].filter(Boolean); ].filter(Boolean);
}, },
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
const firstChild = settings?.[0];
// lowest two elements... sanity backup
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
const { settingsLocation } = Settings.plugins.Settings;
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
const names = {
top: i18n.Messages.USER_SETTINGS,
aboveNitro: i18n.Messages.BILLING_SETTINGS,
belowNitro: i18n.Messages.APP_SETTINGS,
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
};
return header === names[settingsLocation];
},
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: Record<string, unknown>) {
if (!this.isRightSpot(element)) return;
elements.push(...this.makeSettingsCategories(sectionTypes));
},
options: { options: {
settingsLocation: { settingsLocation: {
type: OptionType.SELECT, type: OptionType.SELECT,
description: "Where to put the Vencord settings section", description: "Where to put the Vencord settings section",
options: [ options: [
{ label: "At the very top", value: "top" }, { label: "At the very top", value: "top" },
{ label: "Above the Nitro section", value: "aboveNitro" }, { label: "Above the Nitro section", value: "aboveNitro", default: true },
{ label: "Below the Nitro section", value: "belowNitro" }, { label: "Below the Nitro section", value: "belowNitro" },
{ label: "Above Activity Settings", value: "aboveActivity", default: true }, { label: "Above Activity Settings", value: "aboveActivity" },
{ label: "Below Activity Settings", value: "belowActivity" }, { label: "Below Activity Settings", value: "belowActivity" },
{ label: "At the very bottom", value: "bottom" }, { label: "At the very bottom", value: "bottom" },
], ]
restartNeeded: true
}, },
}, },

View file

@ -139,7 +139,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles; const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return; if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
if (IS_UPDATER_DISABLED) { if (!IS_WEB && IS_UPDATER_DISABLED) {
return Alerts.show({ return Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>

View file

@ -279,7 +279,7 @@ export default definePlugin({
makeGuildsBarTreeFilter(isBetterFolders: boolean) { makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => { return child => {
if (isBetterFolders) { if (isBetterFolders) {
return "onScroll" in child.props; return child?.props?.onScroll != null;
} }
return true; return true;
}; };

View file

@ -17,6 +17,7 @@
*/ */
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -60,7 +61,7 @@ export default definePlugin({
find: ".popularApplicationCommandIds,", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /lastSection:(!?\i)}\),/, match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding($1)," replace: "$&$self.patchPadding({lastSection:$1}),"
} }
} }
], ],
@ -80,10 +81,10 @@ export default definePlugin({
} }
}, },
patchPadding(lastSection: any) { patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return; if (!lastSection) return null;
return ( return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div> <div className={UserPopoutSectionCssClasses.lastSection} ></div>
); );
} })
}); });

View file

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { React, RestAPI, Tooltip } from "@webpack/common"; import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton"; import { RenameButton } from "./components/RenameButton";
import { Session, SessionInfo } from "./types"; import { Session, SessionInfo } from "./types";
@ -168,7 +168,7 @@ export default definePlugin({
async checkNewSessions() { async checkNewSessions() {
const data = await RestAPI.get({ const data = await RestAPI.get({
url: "/auth/sessions" url: Constants.Endpoints.AUTH_SESSIONS
}); });
for (const session of data.body.user_sessions) { for (const session of data.body.user_sessions) {

View file

@ -6,17 +6,18 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common"; import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react"; import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; }; type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory(""); const cl = classNameFactory("");
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers"); let Classes: Record<string, string>;
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
const settings = definePluginSettings({ const settings = definePluginSettings({
disableFade: { disableFade: {
@ -124,12 +125,19 @@ export default definePlugin({
} }
], ],
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
// without possibly also catching unrelated errors of children.
//
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
// not in children
Layer(props: LayerProps) { Layer(props: LayerProps) {
return ( if (!FocusLock || !ComponentDispatch || !Classes) {
<ErrorBoundary fallback={() => props.children as any}> new Logger("BetterSettings").error("Failed to find some components");
<Layer {...props} /> return props.children;
</ErrorBoundary> }
);
return <Layer {...props} />;
}, },
wrapMenu(list: SettingsEntry[]) { wrapMenu(list: SettingsEntry[]) {

View file

@ -104,7 +104,7 @@ export default definePlugin({
shouldAttemptRecover = false; shouldAttemptRecover = false;
// This is enough to avoid a crash loop // This is enough to avoid a crash loop
setTimeout(() => shouldAttemptRecover = true, 500); setTimeout(() => shouldAttemptRecover = true, 1000);
} catch { } } catch { }
try { try {

View file

@ -17,13 +17,16 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color"); const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,
settingsAboutComponent: () => { settingsAboutComponent: () => {
const activity = useAwaiter(createActivity); const activity = useAwaiter(createActivity);
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({}); const { profileThemeStyle } = useProfileThemeStyle({});
return ( return (
<> <>
{!gameActivityEnabled && (
<ErrorCard
className={classes(Margins.top16, Margins.bottom16)}
style={{ padding: "1em" }}
>
<Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button
color={Button.Colors.TRANSPARENT}
className={Margins.top8}
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
>
Enable
</Button>
</ErrorCard>
)}
<Forms.FormText> <Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID. get the application ID.
@ -407,7 +429,9 @@ export default definePlugin({
<Forms.FormText> <Forms.FormText>
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". 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.FormText>
<Forms.FormDivider />
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle }}> <div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()} {activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())} guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}

View file

@ -6,10 +6,11 @@
import "./styles.css"; import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Tooltip } from "@webpack/common"; import { Tooltip } from "@webpack/common";
import type { Component } from "react"; import type { Component } from "react";
@ -34,11 +35,19 @@ interface Props {
}; };
} }
const enum ReplaceElements {
ReplaceAllElements,
ReplaceTitlesOnly,
ReplaceThumbnailsOnly
}
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/; const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) { async function embedDidMount(this: Component<Props>) {
try { try {
const { embed } = this.props; const { embed } = this.props;
const { replaceElements } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return; if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1]; const videoId = embedUrlRe.exec(embed.video.url)?.[1];
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
enabled: true enabled: true
}; };
if (hasTitle) { if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle; embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1"); embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
} }
if (hasThumb) { if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL; embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
} }
@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
); );
} }
const settings = definePluginSettings({
hideButton: {
description: "Hides the Dearrow button from YouTube embeds",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
replaceElements: {
description: "Choose which elements of the embed will be replaced",
type: OptionType.SELECT,
restartNeeded: true,
options: [
{ label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
],
}
});
export default definePlugin({ export default definePlugin({
name: "Dearrow", name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow", description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven], authors: [Devs.Ven],
settings,
embedDidMount, embedDidMount,
renderButton(component: Component<Props>) { renderButton(component: Component<Props>) {
@ -154,7 +183,8 @@ export default definePlugin({
// add dearrow button // add dearrow button
{ {
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/, match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this)," replace: "children:[$self.renderButton(this),",
predicate: () => !settings.store.hideButton
} }
] ]
}], }],

View file

@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal"; import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common"; import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore"); const StickersStore = findStoreLazy("StickersStore");
@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
if (cached) return cached; if (cached) return cached;
const { body } = await RestAPI.get({ const { body } = await RestAPI.get({
url: `/stickers/${id}` url: Constants.Endpoints.STICKER(id)
}); });
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
data.append("file", await fetchBlob(getUrl(sticker))); data.append("file", await fetchBlob(getUrl(sticker)));
const { body } = await RestAPI.post({ const { body } = await RestAPI.post({
url: `/guilds/${guildId}/stickers`, url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
body: data, body: data,
}); });
@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
switch (favoriteableType) { switch (favoriteableType) {
case "emoji": case "emoji":
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`)); const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
if (!match) return; const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
const name = match[1] ?? "FakeNitroEmoji"; if (!match && !reaction) return;
const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
return buildMenuItem("Emoji", () => ({ return buildMenuItem("Emoji", () => ({
id: favoriteableId, id: favoriteableId,

View file

@ -39,6 +39,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore"); const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS"); const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
function searchProtoClassField(localName: string, protoClass: any) { function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName); const field = protoClass?.fields?.find((field: any) => field.localName === localName);
@ -111,7 +112,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({ const settings = definePluginSettings({
enableEmojiBypass: { enableEmojiBypass: {
description: "Allow sending fake emojis", description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
restartNeeded: true restartNeeded: true
@ -129,7 +130,7 @@ const settings = definePluginSettings({
restartNeeded: true restartNeeded: true
}, },
enableStickerBypass: { enableStickerBypass: {
description: "Allow sending fake stickers", description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
restartNeeded: true restartNeeded: true
@ -190,7 +191,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({ export default definePlugin({
name: "FakeNitro", name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.", description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
settings, settings,
@ -408,6 +409,15 @@ export default definePlugin({
match: /canUseCustomNotificationSounds:function\(\i\){/, match: /canUseCustomNotificationSounds:function\(\i\){/,
replace: "$&return true;" replace: "$&return true;"
} }
},
// Allows the usage of subscription-locked emojis
{
find: "isUnusableRoleSubscriptionEmoji:function",
replacement: {
match: /isUnusableRoleSubscriptionEmoji:function/,
// replace the original export with a func that always returns false and alias the original
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
}
} }
], ],
@ -804,6 +814,9 @@ export default definePlugin({
if (e.require_colons === false) return true; if (e.require_colons === false) return true;
if (e.available === false) return false; if (e.available === false) return false;
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
if (this.canUseEmotes) if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId); return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else else

View file

@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { RestAPI, UserStore } from "@webpack/common"; import { Constants, RestAPI, UserStore } from "@webpack/common";
const FriendInvites = findByPropsLazy("createFriendInvite"); const FriendInvites = findByPropsLazy("createFriendInvite");
const { uuid4 } = findByPropsLazy("uuid4"); const { uuid4 } = findByPropsLazy("uuid4");
@ -58,7 +58,7 @@ export default definePlugin({
if (uses === 1) { if (uses === 1) {
const random = uuid4(); const random = uuid4();
const { body: { invite_suggestions } } = await RestAPI.post({ const { body: { invite_suggestions } } = await RestAPI.post({
url: "/friend-finder/find-friends", url: Constants.Endpoints.FRIEND_FINDER,
body: { body: {
modified_contacts: { modified_contacts: {
[random]: [1, "", ""] [random]: [1, "", ""]

View file

@ -17,6 +17,7 @@
*/ */
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common"; import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants"; import { ELEMENT_ID } from "../constants";
@ -36,7 +37,7 @@ export interface MagnifierProps {
const cl = classNameFactory("vc-imgzoom-"); const cl = classNameFactory("vc-imgzoom-");
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => { export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 }); const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
)} )}
</div> </div>
); );
}; }, { noop: true });

View file

@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies"; import { getStegCloak } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal"; import { buildDecModal } from "./components/DecryptionModal";
@ -153,7 +153,7 @@ export default definePlugin({
// Gets the Embed of a Link // Gets the Embed of a Link
async getEmbed(url: URL): Promise<Object | {}> { async getEmbed(url: URL): Promise<Object | {}> {
const { body } = await RestAPI.post({ const { body } = await RestAPI.post({
url: "/unfurler/embed-urls", url: Constants.Endpoints.UNFURL_EMBED_URLS,
body: { body: {
urls: [url] urls: [url]
} }

View file

@ -13,7 +13,7 @@ import { findExportedComponentLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common"; import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
type FillValue = ("status-danger" | "status-warning" | "text-muted"); type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
type Fill = [FillValue, FillValue, FillValue]; type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff; type DiffKey = keyof Diff;
@ -24,19 +24,27 @@ interface Diff {
seconds: number; seconds: number;
} }
const DISCORD_KT_DELAY = 1471228.928;
const HiddenVisually = findExportedComponentLazy("HiddenVisually"); const HiddenVisually = findExportedComponentLazy("HiddenVisually");
export default definePlugin({ export default definePlugin({
name: "MessageLatency", name: "MessageLatency",
description: "Displays an indicator for messages that took ≥n seconds to send", description: "Displays an indicator for messages that took ≥n seconds to send",
authors: [Devs.arHSM], authors: [Devs.arHSM],
settings: definePluginSettings({ settings: definePluginSettings({
latency: { latency: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "Threshold in seconds for latency indicator", description: "Threshold in seconds for latency indicator",
default: 2 default: 2
},
detectDiscordKotlin: {
type: OptionType.BOOLEAN,
description: "Detect old Discord Android clients",
default: true
} }
}), }),
patches: [ patches: [
{ {
find: "showCommunicationDisabledStyles", find: "showCommunicationDisabledStyles",
@ -46,6 +54,7 @@ export default definePlugin({
} }
} }
], ],
stringDelta(delta: number) { stringDelta(delta: number) {
const diff: Diff = { const diff: Diff = {
days: Math.round(delta / (60 * 60 * 24)), days: Math.round(delta / (60 * 60 * 24)),
@ -54,18 +63,42 @@ export default definePlugin({
seconds: Math.round(delta % 60), seconds: Math.round(delta % 60),
}; };
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${k}` : null; const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
const keys = Object.keys(diff) as DiffKey[]; const keys = Object.keys(diff) as DiffKey[];
return keys.map(str).filter(isNonNullish).join(" ") || "0 seconds"; const ts = keys.reduce((prev, k) => {
const s = str(k);
return prev + (
isNonNullish(s)
? (prev !== ""
? k === "seconds"
? " and "
: " "
: "") + s
: ""
);
}, "");
return ts || "0 seconds";
}, },
latencyTooltipData(message: Message) { latencyTooltipData(message: Message) {
const { latency, detectDiscordKotlin } = this.settings.store;
const { id, nonce } = message; const { id, nonce } = message;
// Message wasn't received through gateway // Message wasn't received through gateway
if (!isNonNullish(nonce)) return null; if (!isNonNullish(nonce)) return null;
const delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000); let isDiscordKotlin = false;
let delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
// Old Discord Android clients have a delay of around 17 days
// This is a workaround for that
if (-delta >= DISCORD_KT_DELAY - 86400) { // One day of padding for good measure
isDiscordKotlin = detectDiscordKotlin;
delta += DISCORD_KT_DELAY;
}
// Thanks dziurwa (I hate you) // Thanks dziurwa (I hate you)
// This is when the user's clock is ahead // This is when the user's clock is ahead
@ -73,26 +106,38 @@ export default definePlugin({
const abs = Math.abs(delta); const abs = Math.abs(delta);
const ahead = abs !== delta; const ahead = abs !== delta;
const stringDelta = this.stringDelta(abs); const stringDelta = abs >= latency ? this.stringDelta(abs) : null;
// Also thanks dziurwa // Also thanks dziurwa
// 2 minutes // 2 minutes
const TROLL_LIMIT = 2 * 60; const TROLL_LIMIT = 2 * 60;
const { latency } = this.settings.store;
const fill: Fill = delta >= TROLL_LIMIT || ahead ? ["text-muted", "text-muted", "text-muted"] : delta >= (latency * 2) ? ["status-danger", "text-muted", "text-muted"] : ["status-warning", "status-warning", "text-muted"]; const fill: Fill = isDiscordKotlin
? ["status-positive", "status-positive", "text-muted"]
: delta >= TROLL_LIMIT || ahead
? ["text-muted", "text-muted", "text-muted"]
: delta >= (latency * 2)
? ["status-danger", "text-muted", "text-muted"]
: ["status-warning", "status-warning", "text-muted"];
return abs >= latency ? { delta: stringDelta, ahead: abs !== delta, fill } : null; return (abs >= latency || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
}, },
Tooltip() { Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => { return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
const d = this.latencyTooltipData(message); const d = this.latencyTooltipData(message);
if (!isNonNullish(d)) return null; if (!isNonNullish(d)) return null;
let text: string;
if (!d.delta) {
text = "User is suspected to be on an old Discord Android client";
} else {
text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
}
return <Tooltip return <Tooltip
text={d.ahead ? `This user's clock is ${d.delta} ahead` : `This message was sent with a delay of ${d.delta}.`} text={text}
position="top" position="top"
> >
{ {
@ -105,8 +150,9 @@ export default definePlugin({
</Tooltip>; </Tooltip>;
}); });
}, },
Icon({ delta, fill, props }: { Icon({ delta, fill, props }: {
delta: string; delta: string | null;
fill: Fill, fill: Fill,
props: { props: {
onClick(): void; onClick(): void;
@ -126,7 +172,7 @@ export default definePlugin({
role="img" role="img"
fill="none" fill="none"
style={{ marginRight: "8px", verticalAlign: -1 }} style={{ marginRight: "8px", verticalAlign: -1 }}
aria-label={delta} aria-label={delta ?? "Old Discord Android client"}
aria-hidden="false" aria-hidden="false"
{...props} {...props}
> >

View file

@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { import {
Button, Button,
ChannelStore, ChannelStore,
Constants,
FluxDispatcher, FluxDispatcher,
GuildStore, GuildStore,
IconUtils, IconUtils,
@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
messageCache.set(messageID, { fetched: false }); messageCache.set(messageID, { fetched: false });
const res = await RestAPI.get({ const res = await RestAPI.get({
url: `/channels/${channelID}/messages`, url: Constants.Endpoints.MESSAGES(channelID),
query: { query: {
limit: 1, limit: 1,
around: messageID around: messageID

View file

@ -1,3 +1,3 @@
.messagelogger-deleted { .messagelogger-deleted {
background-color: rgba(240 71 71 / 15%) !important; background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
} }

View file

@ -1,19 +1,19 @@
/* Message content highlighting */ /* Message content highlighting */
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) { .messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
/* Bot "thinking" text highlighting */ /* Bot "thinking" text highlighting */
.messagelogger-deleted [class*="colorStandard"] { .messagelogger-deleted [class*="colorStandard"] {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
/* Embed highlighting */ /* Embed highlighting */
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) { .messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
color: #f04747 !important; color: var(--status-danger, #f04747) !important;
} }
.messagelogger-deleted a { .messagelogger-deleted a {
color: #be3535 !important; color: var(--red-460, #be3535) !important;
text-decoration: underline; text-decoration: underline;
} }

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -55,12 +56,12 @@ export default definePlugin({
find: ".UserProfileSections.USER_INFO_CONNECTIONS:", find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
replacement: { replacement: {
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/, match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);" replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
} }
} }
], ],
renderMutualGDMs(user: User, onClose: () => void) { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => ( const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable <Clickable
className={ProfileListClasses.listRow} className={ProfileListClasses.listRow}
@ -99,5 +100,5 @@ export default definePlugin({
} }
</ScrollerThin> </ScrollerThin>
); );
} })
}); });

View file

@ -16,14 +16,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { GuildStore, RestAPI } from "@webpack/common"; import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment"); const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
function showDisableInvites(guildId: string) {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
}
function disableInvites(guildId: string) {
const guild = GuildStore.getGuild(guildId);
const features = [...guild.features, "INVITES_DISABLED"];
RestAPI.patch({
url: Constants.Endpoints.GUILD(guildId),
body: { features },
});
}
export default definePlugin({ export default definePlugin({
name: "PauseInvitesForever", name: "PauseInvitesForever",
tags: ["DisableInvitesForever"], tags: ["DisableInvitesForever"],
@ -33,42 +49,29 @@ export default definePlugin({
patches: [ patches: [
{ {
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION", find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
replacement: [{ group: true,
replacement: [
{
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/, match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)", replace: "children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})",
}, },
{ {
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/, match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
replace: "$1,setChecked=$2" replace: "$1,setChecked=$2"
}] }
]
} }
], ],
showDisableInvites(guildId: string) { renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
},
disableInvites(guildId: string) {
const guild = GuildStore.getGuild(guildId);
const features = [...guild.features, "INVITES_DISABLED"];
RestAPI.patch({
url: `/guilds/${guild.id}`,
body: { features },
});
},
renderInvitesLabel(guildId: string, setChecked: Function) {
return ( return (
<div> <div>
{Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} {i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
{this.showDisableInvites(guildId) && <a role="button" onClick={() => { {showDisableInvites(guildId) && <a role="button" onClick={() => {
setChecked(true); setChecked(true);
this.disableInvites(guildId); disableInvites(guildId);
}}> Pause Indefinitely.</a>} }}> Pause Indefinitely.</a>}
</div> </div>
); );
} })
}); });

View file

@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord"; import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general"; import type { Guild } from "discord-types/general";
import { settings } from ".."; import { settings } from "..";
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
<div <div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
onContextMenu={e => { onContextMenu={e => {
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role) if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => ( ContextMenuApi.openContextMenu(e, () => (
<RoleContextMenu <RoleContextMenu
guild={guild} guild={guild}
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
onClose={modalProps.onClose} onClose={modalProps.onClose}
/> />
)); ));
else if (permission.type === PermissionType.User) {
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
onClose={modalProps.onClose}
/>
));
}
}} }}
> >
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
@ -199,9 +207,18 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
onClose={ContextMenuApi.closeContextMenu} onClose={ContextMenuApi.closeContextMenu}
aria-label="Role Options" aria-label="Role Options"
> >
<Menu.MenuItem
id="vc-copy-role-id"
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
Clipboard.copy(roleId);
}}
/>
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem <Menu.MenuItem
id="vc-pw-view-as-role" id="vc-pw-view-as-role"
label="View As Role" label={i18n.Messages.VIEW_AS_ROLE}
action={() => { action={() => {
const role = GuildStore.getRole(guild.id, roleId); const role = GuildStore.getRole(guild.id, roleId);
if (!role) return; if (!role) return;
@ -218,6 +235,26 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
} }
} }
}); });
}
}
/>
)}
</Menu.Menu>
);
}
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
return (
<Menu.Menu
navId={cl("user-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
label={i18n.Messages.COPY_ID_USER}
action={() => {
Clipboard.copy(userId);
}} }}
/> />
</Menu.Menu> </Menu.Menu>

View file

@ -83,7 +83,7 @@ export default definePlugin({
// Rendering // Rendering
{ {
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/, match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);" replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
}, },
{ {
match: /"renderSection",(\i)=>{/, match: /"renderSection",(\i)=>{/,
@ -320,9 +320,10 @@ export default definePlugin({
</svg> </svg>
</h2> </h2>
); );
}), }, { noop: true }),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) { renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
return ErrorBoundary.wrap(() => {
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels); const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
if (!channel || !category) return null; if (!channel || !category) return null;
@ -336,9 +337,9 @@ export default definePlugin({
{channel.id} {channel.id}
</ChannelComponent> </ChannelComponent>
); );
}, { noop: true });
}, },
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) { getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
const category = categories[sectionIndex - 1]; const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null }; if (!category) return { channel: null, category: null };

View file

@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
export default definePlugin({ export default definePlugin({
name: "PronounDB", name: "PronounDB",
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven], authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
description: "Adds pronouns to user messages using pronoundb", description: "Adds pronouns to user messages using pronoundb",
patches: [ patches: [
{ {

View file

@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
import { UserProfileStore, UserStore } from "@webpack/common"; import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types"; import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
type PronounsWithSource = [string | null, string]; type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""]; const EmptyPronouns: PronounsWithSource = [null, ""];
@ -40,9 +40,9 @@ export const enum PronounSource {
} }
// A map of cached pronouns so the same request isn't sent twice // A map of cached pronouns so the same request isn't sent twice
const cache: Record<string, PronounCode> = {}; const cache: Record<string, CachePronouns> = {};
// A map of ids and callbacks that should be triggered on fetch // A map of ids and callbacks that should be triggered on fetch
const requestQueue: Record<string, ((pronouns: PronounCode) => void)[]> = {}; const requestQueue: Record<string, ((pronouns: string) => void)[]> = {};
// Executes all queued requests and calls their callbacks // Executes all queued requests and calls their callbacks
const bulkFetch = debounce(async () => { const bulkFetch = debounce(async () => {
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
const pronouns = await bulkFetchPronouns(ids); const pronouns = await bulkFetchPronouns(ids);
for (const id of ids) { for (const id of ids) {
// Call all callbacks for the id // Call all callbacks for the id
requestQueue[id]?.forEach(c => c(pronouns[id])); requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
delete requestQueue[id]; delete requestQueue[id];
} }
}); });
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns) if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"]; return [discordPronouns, "Discord"];
if (result && result !== "unspecified") if (result && result !== PronounMapping.unspecified)
return [formatPronouns(result), "PronounDB"]; return [result, "PronounDB"];
return [discordPronouns, "Discord"]; return [discordPronouns, "Discord"];
} }
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise! // Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): string | null { export function getCachedPronouns(id: string): string | null {
const cached = cache[id]; const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
if (cached && cached !== "unspecified") return cached;
if (cached && cached !== PronounMapping.unspecified) return cached;
return cached || null; return cached || null;
} }
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
params.append("ids", ids.join(",")); params.append("ids", ids.join(","));
try { try {
const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), { const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
method: "GET", method: "GET",
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
} catch (e) { } catch (e) {
// If the request errors, treat it as if no pronouns were found for all ids, and log it // If the request errors, treat it as if no pronouns were found for all ids, and log it
console.error("PronounDB fetching failed: ", e); console.error("PronounDB fetching failed: ", e);
const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const)); const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
Object.assign(cache, dummyPronouns); Object.assign(cache, dummyPronouns);
return dummyPronouns; return dummyPronouns;
} }
} }
export function formatPronouns(pronouns: string): string { export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
// PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
const pronouns = pronounSet.en;
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; }; const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
// For capitalized pronouns, just return the mapping (it is by default capitalized)
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns]; if (pronouns.length === 1) {
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text // For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
else if ( if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
pronounsFormat === PronounsFormat.Lowercase return PronounMapping[pronouns[0]];
&& ["any", "ask", "avoid", "other"].includes(pronouns) else return PronounMapping[pronouns[0]].toLowerCase();
) return PronounMapping[pronouns]; }
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
else return PronounMapping[pronouns].toLowerCase(); return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
} }

View file

@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
} }
export interface PronounsResponse { export interface PronounsResponse {
[id: string]: PronounCode; [id: string]: {
sets?: {
[locale: string]: PronounCode[];
}
}
}
export interface CachePronouns {
sets?: {
[locale: string]: PronounCode[];
}
} }
export type PronounCode = keyof typeof PronounMapping; export type PronounCode = keyof typeof PronounMapping;
export const PronounMapping = { export const PronounMapping = {
hh: "He/Him", he: "He/Him",
hi: "He/It", it: "It/Its",
hs: "He/She", she: "She/Her",
ht: "He/They", they: "They/Them",
ih: "It/Him",
ii: "It/Its",
is: "It/She",
it: "It/They",
shh: "She/He",
sh: "She/Her",
si: "She/It",
st: "She/They",
th: "They/He",
ti: "They/It",
ts: "They/She",
tt: "They/Them",
any: "Any pronouns", any: "Any pronouns",
other: "Other pronouns", other: "Other pronouns",
ask: "Ask me my pronouns", ask: "Ask me my pronouns",
avoid: "Avoid pronouns, use my name", avoid: "Avoid pronouns, use my name",
unspecified: "Unspecified" unspecified: "No pronouns specified.",
} as const; } as const;

View file

@ -19,6 +19,7 @@
import "./style.css"; import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common"; import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
@ -64,7 +65,7 @@ export default definePlugin({
authors: [Devs.kemo], authors: [Devs.kemo],
dependencies: ["ServerListAPI"], dependencies: ["ServerListAPI"],
renderReadAllButton: () => <ReadAllButton />, renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
start() { start() {
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton); addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);

View file

@ -1,6 +1,6 @@
# ShowHiddenThings # ShowHiddenThings
Displays various moderator-only elements regardless of permissions. Displays various hidden & moderator-only things regardless of permissions.
## Features ## Features
@ -15,3 +15,5 @@ Displays various moderator-only elements regardless of permissions.
![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab) ![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria - Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
- Disable filters in Server Discovery search that hide NSFW & disallowed servers

View file

@ -41,13 +41,18 @@ const settings = definePluginSettings({
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.", description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
default: true, default: true,
}, },
disableDisallowedDiscoveryFilters: {
type: OptionType.BOOLEAN,
description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
default: true,
},
}); });
migratePluginSettings("ShowHiddenThings", "ShowTimeouts"); migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({ export default definePlugin({
name: "ShowHiddenThings", name: "ShowHiddenThings",
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"], tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
description: "Displays various moderator-only elements regardless of permissions.", description: "Displays various hidden & moderator-only things regardless of permissions.",
authors: [Devs.Dolfies], authors: [Devs.Dolfies],
patches: [ patches: [
{ {
@ -81,6 +86,23 @@ export default definePlugin({
match: /filters:\i\.join\(" AND "\),facets:\[/, match: /filters:\i\.join\(" AND "\),facets:\[/,
replace: "facets:[" replace: "facets:["
} }
},
{
find: "DiscoveryBannedSearchWords.includes",
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
replacement: {
match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
replace: "return false;"
}
},
{
find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
all: true,
replacement: {
match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
replace: "Promise.resolve({ body: { valid: true } });"
}
} }
], ],
settings, settings,

View file

@ -7,6 +7,7 @@
import "./styles.css"; import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Message, User } from "discord-types/general"; import { Message, User } from "discord-types/general";
@ -56,7 +57,7 @@ export default definePlugin({
], ],
settings, settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => { renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
try { try {
const user = userOverride ?? message.author; const user = userOverride ?? message.author;
let { username } = user; let { username } = user;
@ -66,14 +67,14 @@ export default definePlugin({
const { nick } = author; const { nick } = author;
const prefix = withMentionPrefix ? "@" : ""; const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies) if (username === nick || isRepliedMessage && !settings.store.inReplies)
return prefix + nick; return <>{prefix}{nick}</>;
if (settings.store.mode === "user-nick") if (settings.store.mode === "user-nick")
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>; return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
if (settings.store.mode === "nick-user") if (settings.store.mode === "nick-user")
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>; return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
return prefix + username; return <>{prefix}{username}</>;
} catch { } catch {
return author?.nick; return <>{author?.nick}</>;
} }
}, }, { noop: true }),
}); });

View file

@ -0,0 +1,8 @@
# ShowTimeoutDuration
Displays how much longer a user's timeout will last.
Either in the timeout icon tooltip, or next to it, configurable via settings!
![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)

View file

@ -0,0 +1,106 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentLazy } from "@webpack";
import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
const enum DisplayStyle {
Tooltip = "tooltip",
Inline = "ssalggnikool"
}
const settings = definePluginSettings({
displayStyle: {
description: "How to display the timeout duration",
type: OptionType.SELECT,
restartNeeded: true,
options: [
{ label: "In the Tooltip", value: DisplayStyle.Tooltip },
{ label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
],
}
});
function renderTimeout(message: Message, inline: boolean) {
const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
if (!guildId) return null;
const member = GuildMemberStore.getMember(guildId, message.author.id);
if (!member?.communicationDisabledUntil) return null;
const countdown = () => (
<CountDown
deadline={new Date(member.communicationDisabledUntil!)}
showUnits
stopAtOneSec
/>
);
return inline
? countdown()
: i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
username: message.author.username,
countdown
});
}
export default definePlugin({
name: "ShowTimeoutDuration",
description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
authors: [Devs.Ven],
settings,
patches: [
{
find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
replacement: [
{
match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
get replace() {
if (settings.store.displayStyle === DisplayStyle.Inline)
return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
}
}
]
}
],
renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
return (
<>
<Forms.FormText>{i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}</Forms.FormText>
<Forms.FormText className={Margins.top8}>
{renderTimeout(message, false)}
</Forms.FormText>
</>
);
}, { noop: true }),
TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
return (
<div className="vc-std-wrapper">
<Tooltip {...tooltipProps as any} />
<Text variant="text-md/normal" color="status-danger">
{renderTimeout(message, true)} timeout remaining
</Text>
</div>
);
}, { noop: true })
});

View file

@ -0,0 +1,4 @@
.vc-std-wrapper {
display: flex;
align-items: center;
}

View file

@ -18,10 +18,11 @@
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, React } from "@webpack/common"; import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({ const settings = definePluginSettings({
showIcon: { showIcon: {
@ -30,6 +31,11 @@ const settings = definePluginSettings({
description: "Show an icon for toggling the plugin", description: "Show an icon for toggling the plugin",
restartNeeded: true, restartNeeded: true,
}, },
contextMenu: {
type: OptionType.BOOLEAN,
description: "Add option to toggle the functionality in the chat input context menu",
default: true
},
isEnabled: { isEnabled: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Toggle functionality", description: "Toggle functionality",
@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
); );
}; };
const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
if (!contextMenu) return;
const group = findGroupChildrenByChildId("submit-button", children);
if (!group) return;
const idx = group.findIndex(c => c?.props?.id === "submit-button");
group.splice(idx + 1, 0,
<Menu.MenuCheckboxItem
id="vc-silent-typing"
label="Enable Silent Typing"
checked={isEnabled}
action={() => settings.store.isEnabled = !settings.store.isEnabled}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "SilentTyping", name: "SilentTyping",
authors: [Devs.Ven, Devs.Rini], authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing", description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"], dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
settings, settings,
contextMenus: {
"textarea-context": ChatBarContextCheckbox
},
patches: [ patches: [
{ {
find: '.dispatch({type:"TYPING_START_LOCAL"', find: '.dispatch({type:"TYPING_START_LOCAL"',

View file

@ -15,6 +15,7 @@ This allows themes to more easily theme those elements or even do things that ot
### Chat Messages ### Chat Messages
- `data-author-id` contains the id of the author - `data-author-id` contains the id of the author
- `data-author-username` contains the username of the author
- `data-is-self` is a boolean indicating whether this is the current user's message - `data-is-self` is a boolean indicating whether this is the current user's message
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122) ![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)

View file

@ -36,10 +36,12 @@ export default definePlugin({
], ],
getMessageProps(props: { message: Message; }) { getMessageProps(props: { message: Message; }) {
const authorId = props.message?.author?.id; const author = props.message?.author;
const authorId = author?.id;
return { return {
"data-author-id": authorId, "data-author-id": authorId,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id "data-author-username": author?.username,
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
}; };
} }
}); });

View file

@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
} }
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const { autoTranslate } = settings.use(["autoTranslate"]); const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
if (!isMainChat) return null; if (!isMainChat || !showChatBarButton) return null;
const toggle = () => { const toggle = () => {
const newState = !autoTranslate; const newState = !autoTranslate;

View file

@ -48,6 +48,11 @@ export const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this", description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false default: false
},
showChatBarButton: {
type: OptionType.BOOLEAN,
description: "Show translate button in chat bar",
default: true
} }
}).withPrivateSettings<{ }).withPrivateSettings<{
showAutoTranslateAlert: boolean; showAutoTranslateAlert: boolean;

View file

@ -20,7 +20,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
import { ImageInvisible, ImageVisible } from "@components/Icons"; import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
const EMBED_SUPPRESSED = 1 << 2; const EMBED_SUPPRESSED = 1 << 2;
@ -44,7 +44,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channe
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible} icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
action={() => action={() =>
RestAPI.patch({ RestAPI.patch({
url: `/channels/${channel.id}/messages/${messageId}`, url: Constants.Endpoints.MESSAGE(channel.id, messageId),
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED } body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
}) })
} }

View file

@ -18,28 +18,30 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common"; import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
import type { ComponentType, ReactNode } from "react"; import { type ComponentType, type ReactNode } from "react";
// LYING to the type checker here // LYING to the type checker here
const UserFlags = Constants.UserFlags as Record<string, number>; const UserFlags = Constants.UserFlags as Record<string, number>;
const badges: Record<string, ProfileBadge> = { const badges: Record<string, ProfileBadge> = {
"active_developer": { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" }, active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
"bug_hunter_level_1": { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" }, bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
"bug_hunter_level_2": { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" }, bug_hunter_level_2: { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
"certified_moderator": { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" }, certified_moderator: { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
"discord_employee": { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" }, discord_employee: { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
"hypesquad": { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" }, get staff() { return this.discord_employee; },
"hypesquad_online_house_1": { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" }, hypesquad: { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
"hypesquad_online_house_2": { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" }, hypesquad_online_house_1: { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
"hypesquad_online_house_3": { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" }, hypesquad_online_house_2: { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
"partner": { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" }, hypesquad_online_house_3: { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
"premium": { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" }, partner: { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
"premium_early_supporter": { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" }, premium: { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
"verified_developer": { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" }, premium_early_supporter: { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
verified_developer: { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
}; };
const fetching = new Set<string>(); const fetching = new Set<string>();
@ -73,7 +75,7 @@ async function getUser(id: string) {
if (userObj) if (userObj)
return userObj; return userObj;
const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => { const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "USER_UPDATE", type: "USER_UPDATE",
user: response.body, user: response.body,
@ -93,7 +95,8 @@ async function getUser(id: string) {
userObj = UserStore.getUser(id); userObj = UserStore.getUser(id);
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags) const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag)) .filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
.map(([key]) => badges[key.toLowerCase()]); .map(([key]) => badges[key.toLowerCase()])
.filter(isNonNullish);
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_"))) if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
fakeBadges.push(badges.premium); fakeBadges.push(badges.premium);
@ -202,6 +205,7 @@ export default definePlugin({
return ( return (
<ErrorBoundary noop> <ErrorBoundary noop>
<MentionWrapper <MentionWrapper
key={"mention" + data.userId}
RoleMention={RoleMention} RoleMention={RoleMention}
UserMention={UserMention} UserMention={UserMention}
data={data} data={data}

View file

@ -28,9 +28,12 @@ export default definePlugin({
<a <a
className="vc-voice-download" className="vc-voice-download"
href={src} href={src}
download="voice-message.ogg"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
aria-label="Download voice message" aria-label="Download voice message"
{...IS_DISCORD_DESKTOP
? { target: "_blank" } // open externally
: { download: "voice-message.ogg" } // download directly (not supported on discord desktop)
}
> >
<this.Icon /> <this.Icon />
</a> </a>

View file

@ -28,7 +28,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web"; import { chooseFile } from "@utils/web";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react"; import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder"; import { VoiceRecorderDesktop } from "./DesktopRecorder";
@ -98,7 +98,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
upload.on("complete", () => { upload.on("complete", () => {
RestAPI.post({ RestAPI.post({
url: `/channels/${channelId}/messages`, url: Constants.Endpoints.MESSAGES(channelId),
body: { body: {
flags: 1 << 13, flags: 1 << 13,
channel_id: channelId, channel_id: channelId,

View file

@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
import { useForceUpdater } from "@utils/react"; import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common"; import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { CustomEmoji } from "@webpack/types"; import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general"; import { Message, ReactionEmoji, User } from "discord-types/general";
@ -36,7 +36,7 @@ let reactions: Record<string, ReactionCacheEntry>;
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
return RestAPI.get({ return RestAPI.get({
url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
query: { query: {
limit: 100, limit: 100,
type type

View file

@ -1,6 +1,6 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const enum ChannelTypes { const { ChannelTypes } = findByPropsLazy("ChannelTypes");
DM = 1,
GROUP_DM = 3
}
interface Message { interface Message {
guild_id: string, guild_id: string,
@ -71,15 +68,35 @@ interface Call {
ringing: string[]; ringing: string[];
} }
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled"); const Notifs = findByPropsLazy("makeTextChatNotification");
const XSLog = new Logger("XSOverlay"); const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({ const settings = definePluginSettings({
ignoreBots: { botNotifications: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Ignore messages from bots", description: "Allow bot notifications",
default: false default: false
}, },
serverNotifications: {
type: OptionType.BOOLEAN,
description: "Allow server notifications",
default: true
},
dmNotifications: {
type: OptionType.BOOLEAN,
description: "Allow Direct Message notifications",
default: true
},
groupDmNotifications: {
type: OptionType.BOOLEAN,
description: "Allow Group DM notifications",
default: true
},
callNotifications: {
type: OptionType.BOOLEAN,
description: "Allow call notifications",
default: true
},
pingColor: { pingColor: {
type: OptionType.STRING, type: OptionType.STRING,
description: "User mention color", description: "User mention color",
@ -97,8 +114,13 @@ const settings = definePluginSettings({
}, },
timeout: { timeout: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "Notif duration (secs)", description: "Notification duration (secs)",
default: 1.0, default: 3,
},
lengthBasedTimeout: {
type: OptionType.BOOLEAN,
description: "Extend duration with message length",
default: true
}, },
opacity: { opacity: {
type: OptionType.SLIDER, type: OptionType.SLIDER,
@ -124,7 +146,7 @@ export default definePlugin({
settings, settings,
flux: { flux: {
CALL_UPDATE({ call }: { call: Call; }) { CALL_UPDATE({ call }: { call: Call; }) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) { if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
const channel = ChannelStore.getChannel(call.channel_id); const channel = ChannelStore.getChannel(call.channel_id);
sendOtherNotif("Incoming call", `${channel.name} is calling you...`); sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
} }
@ -134,7 +156,7 @@ export default definePlugin({
try { try {
if (optimistic) return; if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id); const channel = ChannelStore.getChannel(message.channel_id);
if (!shouldNotify(message, channel)) return; if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim(); const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim(); const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
@ -194,6 +216,7 @@ export default definePlugin({
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`); finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
} }
// color role mentions (unity styling btw lol)
if (message.mention_roles.length > 0) { if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) { for (const roleId of message.mention_roles) {
const role = GuildStore.getRole(channel.guild_id, roleId); const role = GuildStore.getRole(channel.guild_id, roleId);
@ -213,6 +236,7 @@ export default definePlugin({
} }
} }
// color channel mentions
if (channelMatches) { if (channelMatches) {
for (const cMatch of channelMatches) { for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1]; let channelId = cMatch.split("<#")[1];
@ -221,6 +245,7 @@ export default definePlugin({
} }
} }
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message); sendMsgNotif(titleString, finalMsg, message);
} catch (err) { } catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`); XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
@ -229,13 +254,19 @@ export default definePlugin({
} }
}); });
function shouldIgnoreForChannelType(channel: Channel) {
if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
else return !settings.store.serverNotifications;
}
function sendMsgNotif(titleString: string, content: string, message: Message) { function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => { fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = { const msgData = {
messageType: 1, messageType: 1,
index: 0, index: 0,
timeout: settings.store.timeout, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(cleanMessage(content)), height: calculateHeight(content),
opacity: settings.store.opacity, opacity: settings.store.opacity,
volume: settings.store.volume, volume: settings.store.volume,
audioPath: settings.store.soundPath, audioPath: settings.store.soundPath,
@ -253,8 +284,8 @@ function sendOtherNotif(content: string, titleString: string) {
const msgData = { const msgData = {
messageType: 1, messageType: 1,
index: 0, index: 0,
timeout: settings.store.timeout, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(cleanMessage(content)), height: calculateHeight(content),
opacity: settings.store.opacity, opacity: settings.store.opacity,
volume: settings.store.volume, volume: settings.store.volume,
audioPath: settings.store.soundPath, audioPath: settings.store.soundPath,
@ -267,13 +298,11 @@ function sendOtherNotif(content: string, titleString: string) {
Native.sendToOverlay(msgData); Native.sendToOverlay(msgData);
} }
function shouldNotify(message: Message, channel: Channel) { function shouldNotify(message: Message, channel: string) {
const currentUser = UserStore.getCurrentUser(); const currentUser = UserStore.getCurrentUser();
if (message.author.id === currentUser.id) return false; if (message.author.id === currentUser.id) return false;
if (message.author.bot && settings.store.ignoreBots) return false; if (message.author.bot && !settings.store.botNotifications) return false;
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true; return Notifs.shouldNotify(message, channel);
return message.mentions.some(m => m.id === currentUser.id);
} }
function calculateHeight(content: string) { function calculateHeight(content: string) {
@ -283,6 +312,9 @@ function calculateHeight(content: string) {
return 250; return 250;
} }
function cleanMessage(content: string) { function calculateTimeout(content: string) {
return content.replace(new RegExp("<[^>]*>", "g"), ""); if (content.length <= 100) return 3;
if (content.length <= 200) return 4;
if (content.length <= 300) return 5;
return 6;
} }

View file

@ -418,6 +418,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Kyuuhachi", name: "Kyuuhachi",
id: 236588665420251137n, id: 236588665420251137n,
}, },
nin0dev: {
name: "nin0dev",
id: 886685857560539176n
},
Elvyra: { Elvyra: {
name: "Elvyra", name: "Elvyra",
id: 708275751816003615n, id: 708275751816003615n,
@ -458,9 +462,21 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Oleh Polisan", name: "Oleh Polisan",
id: 242305263313485825n id: 242305263313485825n
}, },
HAHALOSAH: {
name: "HAHALOSAH",
id: 903418691268513883n
},
GabiRP: { GabiRP: {
name: "GabiRP", name: "GabiRP",
id: 507955112027750401n id: 507955112027750401n
},
ImBanana: {
name: "Im_Banana",
id: 635250116688871425n
},
xocherry: {
name: "xocherry",
id: 221288171013406720n
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View file

@ -17,7 +17,7 @@
*/ */
import { MessageObject } from "@api/MessageEvents"; import { MessageObject } from "@api/MessageEvents";
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general"; import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@ -162,7 +162,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id }); FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
const { body } = await RestAPI.get({ const { body } = await RestAPI.get({
url: `/users/${id}/profile`, url: Constants.Endpoints.USER_PROFILE(id),
query: { query: {
with_mutual_guilds: false, with_mutual_guilds: false,
with_mutual_friends_count: false, with_mutual_friends_count: false,

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Recursively merges defaults into an object and returns the same object
* @param obj Object
* @param defaults Defaults
* @returns obj
*/
export function mergeDefaults<T>(obj: T, defaults: T): T {
for (const key in defaults) {
const v = defaults[key];
if (typeof v === "object" && !Array.isArray(v)) {
obj[key] ??= {} as any;
mergeDefaults(obj[key], v);
} else {
obj[key] ??= v;
}
}
return obj;
}

View file

@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common";
import { DevsById } from "./constants"; import { DevsById } from "./constants";
/**
* Recursively merges defaults into an object and returns the same object
* @param obj Object
* @param defaults Defaults
* @returns obj
*/
export function mergeDefaults<T>(obj: T, defaults: T): T {
for (const key in defaults) {
const v = defaults[key];
if (typeof v === "object" && !Array.isArray(v)) {
obj[key] ??= {} as any;
mergeDefaults(obj[key], v);
} else {
obj[key] ??= v;
}
}
return obj;
}
/** /**
* Calls .join(" ") on the arguments * Calls .join(" ") on the arguments
* classes("one", "two") => "one two" * classes("one", "two") => "one two"

View file

@ -18,7 +18,7 @@
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { PlainSettings, Settings } from "@api/Settings"; import { PlainSettings, Settings } from "@api/Settings";
import { Toasts } from "@webpack/common"; import { moment, Toasts } from "@webpack/common";
import { deflateSync, inflateSync } from "fflate"; import { deflateSync, inflateSync } from "fflate";
import { getCloudAuth, getCloudUrl } from "./cloud"; import { getCloudAuth, getCloudUrl } from "./cloud";
@ -49,7 +49,7 @@ export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
} }
export async function downloadSettingsBackup() { export async function downloadSettingsBackup() {
const filename = "vencord-settings-backup.json"; const filename = `vencord-settings-backup-${moment().format("YYYY-MM-DD")}.json`;
const backup = await exportSettings(); const backup = await exportSettings();
const data = new TextEncoder().encode(backup); const data = new TextEncoder().encode(backup);