mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-01-25 16:56:23 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
3ed07a6bfe
37 changed files with 557 additions and 158 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* 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 { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
|
|
@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
|
|||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Text } from "@webpack/common";
|
||||
import { Text, useRef } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
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) {
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<div
|
||||
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("name-author")}>
|
||||
<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>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
|
|
|
@ -62,3 +62,36 @@
|
|||
.vc-addon-author::before {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import type { Settings } from "@api/Settings";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { SettingsStore } from "@shared/SettingsStore";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { ipcMain } from "electron";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
|
||||
|
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
|
|||
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(() => {
|
||||
try {
|
||||
|
|
|
@ -139,7 +139,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
|||
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
||||
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
|
||||
|
||||
if (IS_UPDATER_DISABLED) {
|
||||
if (!IS_WEB && IS_UPDATER_DISABLED) {
|
||||
return Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
@ -83,7 +84,9 @@ export default definePlugin({
|
|||
patchPadding(lastSection: any) {
|
||||
if (!lastSection) return;
|
||||
return (
|
||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
||||
<ErrorBoundary noop>
|
||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,17 +6,18 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
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 type { HTMLAttributes, ReactElement } from "react";
|
||||
|
||||
type SettingsEntry = { section: string, label: string; };
|
||||
|
||||
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({
|
||||
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) {
|
||||
return (
|
||||
<ErrorBoundary fallback={() => props.children as any}>
|
||||
<Layer {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||
new Logger("BetterSettings").error("Failed to find some components");
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return <Layer {...props} />;
|
||||
},
|
||||
|
||||
wrapMenu(list: SettingsEntry[]) {
|
||||
|
|
|
@ -104,7 +104,7 @@ export default definePlugin({
|
|||
|
||||
shouldAttemptRecover = false;
|
||||
// This is enough to avoid a crash loop
|
||||
setTimeout(() => shouldAttemptRecover = true, 500);
|
||||
setTimeout(() => shouldAttemptRecover = true, 1000);
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
|
|
|
@ -17,13 +17,16 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
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 ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||
|
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
|
|||
export default definePlugin({
|
||||
name: "CustomRPC",
|
||||
description: "Allows you to set a custom rich presence.",
|
||||
authors: [Devs.captain, Devs.AutumnVN],
|
||||
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||
start: setRpc,
|
||||
stop: () => setRpc(true),
|
||||
settings,
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const activity = useAwaiter(createActivity);
|
||||
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
|
||||
const { profileThemeStyle } = useProfileThemeStyle({});
|
||||
|
||||
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>
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
get the application ID.
|
||||
|
@ -407,7 +429,9 @@ export default definePlugin({
|
|||
<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".
|
||||
</Forms.FormText>
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormDivider className={Margins.top8} />
|
||||
|
||||
<div style={{ width: "284px", ...profileThemeStyle }}>
|
||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Tooltip } from "@webpack/common";
|
||||
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})/;
|
||||
|
||||
async function embedDidMount(this: Component<Props>) {
|
||||
try {
|
||||
const { embed } = this.props;
|
||||
const { replaceElements } = settings.store;
|
||||
|
||||
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||
|
||||
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||
|
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
|
|||
enabled: true
|
||||
};
|
||||
|
||||
if (hasTitle) {
|
||||
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
||||
}
|
||||
|
||||
if (hasThumb) {
|
||||
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
|
||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||
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({
|
||||
name: "Dearrow",
|
||||
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||
authors: [Devs.Ven],
|
||||
settings,
|
||||
|
||||
embedDidMount,
|
||||
renderButton(component: Component<Props>) {
|
||||
|
@ -154,7 +183,8 @@ export default definePlugin({
|
|||
// add dearrow button
|
||||
{
|
||||
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||
replace: "children:[$self.renderButton(this),"
|
||||
replace: "children:[$self.renderButton(this),",
|
||||
predicate: () => !settings.store.hideButton
|
||||
}
|
||||
]
|
||||
}],
|
||||
|
|
|
@ -111,7 +111,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
|||
|
||||
const settings = definePluginSettings({
|
||||
enableEmojiBypass: {
|
||||
description: "Allow sending fake emojis",
|
||||
description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
|
@ -129,7 +129,7 @@ const settings = definePluginSettings({
|
|||
restartNeeded: true
|
||||
},
|
||||
enableStickerBypass: {
|
||||
description: "Allow sending fake stickers",
|
||||
description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
|
@ -190,7 +190,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
|||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
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"],
|
||||
|
||||
settings,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||
|
||||
import { ELEMENT_ID } from "../constants";
|
||||
|
@ -36,7 +37,7 @@ export interface MagnifierProps {
|
|||
|
||||
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 [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||
|
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, { noop: true });
|
||||
|
|
|
@ -13,7 +13,7 @@ import { findExportedComponentLazy } from "@webpack";
|
|||
import { SnowflakeUtils, Tooltip } from "@webpack/common";
|
||||
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 DiffKey = keyof Diff;
|
||||
|
||||
|
@ -54,10 +54,24 @@ export default definePlugin({
|
|||
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[];
|
||||
|
||||
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", diff.days === 17 && diff.hours === 1] as const;
|
||||
},
|
||||
latencyTooltipData(message: Message) {
|
||||
const { id, nonce } = message;
|
||||
|
@ -73,16 +87,23 @@ export default definePlugin({
|
|||
const abs = Math.abs(delta);
|
||||
const ahead = abs !== delta;
|
||||
|
||||
const stringDelta = this.stringDelta(abs);
|
||||
const [stringDelta, isSuspectedKotlinDiscord] = this.stringDelta(abs);
|
||||
const isKotlinDiscord = ahead && isSuspectedKotlinDiscord;
|
||||
|
||||
// Also thanks dziurwa
|
||||
// 2 minutes
|
||||
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 = isKotlinDiscord
|
||||
? ["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 ? { delta: stringDelta, ahead, fill, isKotlinDiscord } : null;
|
||||
},
|
||||
Tooltip() {
|
||||
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||
|
@ -92,7 +113,7 @@ export default definePlugin({
|
|||
if (!isNonNullish(d)) return null;
|
||||
|
||||
return <Tooltip
|
||||
text={d.ahead ? `This user's clock is ${d.delta} ahead` : `This message was sent with a delay of ${d.delta}.`}
|
||||
text={d.ahead ? `This user's clock is ${d.delta} ahead. ${d.isKotlinDiscord ? "User is suspected to be on an old mobile client" : ""}` : `This message was sent with a delay of ${d.delta}.`}
|
||||
position="top"
|
||||
>
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import definePlugin from "@utils/types";
|
||||
|
@ -60,7 +61,7 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
renderMutualGDMs(user: User, onClose: () => void) {
|
||||
renderMutualGDMs: ErrorBoundary.wrap((user: User, onClose: () => void) => {
|
||||
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
||||
<Clickable
|
||||
className={ProfileListClasses.listRow}
|
||||
|
@ -99,5 +100,5 @@ export default definePlugin({
|
|||
}
|
||||
</ScrollerThin>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { GuildStore, RestAPI } from "@webpack/common";
|
||||
import { GuildStore, i18n, RestAPI } from "@webpack/common";
|
||||
|
||||
const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
|
||||
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -62,13 +62,15 @@ export default definePlugin({
|
|||
|
||||
renderInvitesLabel(guildId: string, setChecked: Function) {
|
||||
return (
|
||||
<div>
|
||||
{Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
|
||||
{this.showDisableInvites(guildId) && <a role="button" onClick={() => {
|
||||
setChecked(true);
|
||||
this.disableInvites(guildId);
|
||||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
<ErrorBoundary noop>
|
||||
<div>
|
||||
{i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
|
||||
{this.showDisableInvites(guildId) && <a role="button" onClick={() => {
|
||||
setChecked(true);
|
||||
this.disableInvites(guildId);
|
||||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
|
|||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { getUniqueUsername } from "@utils/discord";
|
||||
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 { settings } from "..";
|
||||
|
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
<div
|
||||
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
||||
onContextMenu={e => {
|
||||
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
|
||||
if (permission.type === PermissionType.Role)
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<RoleContextMenu
|
||||
guild={guild}
|
||||
|
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
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) && (
|
||||
|
@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
aria-label="Role Options"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="vc-pw-view-as-role"
|
||||
label="View As Role"
|
||||
id="vc-copy-role-id"
|
||||
label={i18n.Messages.COPY_ID_ROLE}
|
||||
action={() => {
|
||||
const role = GuildStore.getRole(guild.id, roleId);
|
||||
if (!role) return;
|
||||
Clipboard.copy(roleId);
|
||||
}}
|
||||
/>
|
||||
|
||||
onClose();
|
||||
{(settings.store as any).unsafeViewAsRole && (
|
||||
<Menu.MenuItem
|
||||
id="vc-pw-view-as-role"
|
||||
label={i18n.Messages.VIEW_AS_ROLE}
|
||||
action={() => {
|
||||
const role = GuildStore.getRole(guild.id, roleId);
|
||||
if (!role) return;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "IMPERSONATE_UPDATE",
|
||||
guildId: guild.id,
|
||||
data: {
|
||||
type: "ROLES",
|
||||
roles: {
|
||||
[roleId]: role
|
||||
onClose();
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "IMPERSONATE_UPDATE",
|
||||
guildId: guild.id,
|
||||
data: {
|
||||
type: "ROLES",
|
||||
roles: {
|
||||
[roleId]: role
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
|
|
@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
|
|||
|
||||
export default definePlugin({
|
||||
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",
|
||||
patches: [
|
||||
{
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
|
|||
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||
import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||
|
||||
type PronounsWithSource = [string | null, string];
|
||||
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
|
||||
const cache: Record<string, PronounCode> = {};
|
||||
const cache: Record<string, CachePronouns> = {};
|
||||
// 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
|
||||
const bulkFetch = debounce(async () => {
|
||||
|
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
|
|||
const pronouns = await bulkFetchPronouns(ids);
|
||||
for (const id of ids) {
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
|
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
|
|||
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
||||
return [discordPronouns, "Discord"];
|
||||
|
||||
if (result && result !== "unspecified")
|
||||
return [formatPronouns(result), "PronounDB"];
|
||||
if (result && result !== PronounMapping.unspecified)
|
||||
return [result, "PronounDB"];
|
||||
|
||||
return [discordPronouns, "Discord"];
|
||||
}
|
||||
|
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
|
|||
|
||||
// Gets the cached pronouns, if you're too impatient for a promise!
|
||||
export function getCachedPronouns(id: string): string | null {
|
||||
const cached = cache[id];
|
||||
if (cached && cached !== "unspecified") return cached;
|
||||
const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
|
||||
|
||||
if (cached && cached !== PronounMapping.unspecified) return cached;
|
||||
|
||||
return cached || null;
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
|||
params.append("ids", ids.join(","));
|
||||
|
||||
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",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
|
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
|||
} catch (e) {
|
||||
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
||||
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);
|
||||
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; };
|
||||
// For capitalized pronouns, just return the mapping (it is by default capitalized)
|
||||
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
|
||||
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
|
||||
else if (
|
||||
pronounsFormat === PronounsFormat.Lowercase
|
||||
&& ["any", "ask", "avoid", "other"].includes(pronouns)
|
||||
) return PronounMapping[pronouns];
|
||||
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase
|
||||
else return PronounMapping[pronouns].toLowerCase();
|
||||
|
||||
if (pronouns.length === 1) {
|
||||
// For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
|
||||
if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
|
||||
return PronounMapping[pronouns[0]];
|
||||
else return PronounMapping[pronouns[0]].toLowerCase();
|
||||
}
|
||||
const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
|
||||
return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
|
|||
}
|
||||
|
||||
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 const PronounMapping = {
|
||||
hh: "He/Him",
|
||||
hi: "He/It",
|
||||
hs: "He/She",
|
||||
ht: "He/They",
|
||||
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",
|
||||
he: "He/Him",
|
||||
it: "It/Its",
|
||||
she: "She/Her",
|
||||
they: "They/Them",
|
||||
any: "Any pronouns",
|
||||
other: "Other pronouns",
|
||||
ask: "Ask me my pronouns",
|
||||
avoid: "Avoid pronouns, use my name",
|
||||
unspecified: "Unspecified"
|
||||
unspecified: "No pronouns specified.",
|
||||
} as const;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import "./style.css";
|
||||
|
||||
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
||||
|
@ -64,7 +65,7 @@ export default definePlugin({
|
|||
authors: [Devs.kemo],
|
||||
dependencies: ["ServerListAPI"],
|
||||
|
||||
renderReadAllButton: () => <ReadAllButton />,
|
||||
renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
|
||||
|
||||
start() {
|
||||
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
@ -56,7 +57,7 @@ export default definePlugin({
|
|||
],
|
||||
settings,
|
||||
|
||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||
renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||
try {
|
||||
const user = userOverride ?? message.author;
|
||||
let { username } = user;
|
||||
|
@ -66,14 +67,14 @@ export default definePlugin({
|
|||
const { nick } = author;
|
||||
const prefix = withMentionPrefix ? "@" : "";
|
||||
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
||||
return prefix + nick;
|
||||
return <>{prefix}{nick}</>;
|
||||
if (settings.store.mode === "user-nick")
|
||||
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
||||
if (settings.store.mode === "nick-user")
|
||||
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
||||
return prefix + username;
|
||||
return <>{prefix}{username}</>;
|
||||
} catch {
|
||||
return author?.nick;
|
||||
return <>{author?.nick}</>;
|
||||
}
|
||||
},
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
|
8
src/plugins/showTimeoutDuration/README.md
Normal file
8
src/plugins/showTimeoutDuration/README.md
Normal 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)
|
106
src/plugins/showTimeoutDuration/index.tsx
Normal file
106
src/plugins/showTimeoutDuration/index.tsx
Normal 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 })
|
||||
});
|
4
src/plugins/showTimeoutDuration/styles.css
Normal file
4
src/plugins/showTimeoutDuration/styles.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.vc-std-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
|
@ -18,10 +18,11 @@
|
|||
|
||||
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher, React } from "@webpack/common";
|
||||
import { FluxDispatcher, Menu, React } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showIcon: {
|
||||
|
@ -30,6 +31,11 @@ const settings = definePluginSettings({
|
|||
description: "Show an icon for toggling the plugin",
|
||||
restartNeeded: true,
|
||||
},
|
||||
contextMenu: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Add option to toggle the functionality in the chat input context menu",
|
||||
default: true
|
||||
},
|
||||
isEnabled: {
|
||||
type: OptionType.BOOLEAN,
|
||||
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({
|
||||
name: "SilentTyping",
|
||||
authors: [Devs.Ven, Devs.Rini],
|
||||
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
|
||||
description: "Hide that you are typing",
|
||||
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"textarea-context": ChatBarContextCheckbox
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
||||
|
|
|
@ -15,6 +15,7 @@ This allows themes to more easily theme those elements or even do things that ot
|
|||
### Chat Messages
|
||||
|
||||
- `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
|
||||
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
||||
|
|
|
@ -36,10 +36,12 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
getMessageProps(props: { message: Message; }) {
|
||||
const authorId = props.message?.author?.id;
|
||||
const author = props.message?.author;
|
||||
const authorId = author?.id;
|
||||
return {
|
||||
"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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
|
|||
}
|
||||
|
||||
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 newState = !autoTranslate;
|
||||
|
|
|
@ -48,6 +48,11 @@ export const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
||||
default: false
|
||||
},
|
||||
showChatBarButton: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show translate button in chat bar",
|
||||
default: true
|
||||
}
|
||||
}).withPrivateSettings<{
|
||||
showAutoTranslateAlert: boolean;
|
||||
|
|
|
@ -28,9 +28,12 @@ export default definePlugin({
|
|||
<a
|
||||
className="vc-voice-download"
|
||||
href={src}
|
||||
download="voice-message.ogg"
|
||||
onClick={e => e.stopPropagation()}
|
||||
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 />
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
|
|||
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
||||
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||
|
||||
const enum ChannelTypes {
|
||||
DM = 1,
|
||||
GROUP_DM = 3
|
||||
}
|
||||
const { ChannelTypes } = findByPropsLazy("ChannelTypes");
|
||||
|
||||
interface Message {
|
||||
guild_id: string,
|
||||
|
@ -72,14 +69,35 @@ interface Call {
|
|||
}
|
||||
|
||||
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
|
||||
const Notifs = findByPropsLazy("makeTextChatNotification");
|
||||
const XSLog = new Logger("XSOverlay");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
ignoreBots: {
|
||||
botNotifications: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Ignore messages from bots",
|
||||
description: "Allow bot notifications",
|
||||
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: {
|
||||
type: OptionType.STRING,
|
||||
description: "User mention color",
|
||||
|
@ -100,6 +118,11 @@ const settings = definePluginSettings({
|
|||
description: "Notif duration (secs)",
|
||||
default: 1.0,
|
||||
},
|
||||
timeoutPerCharacter: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Duration multiplier per character",
|
||||
default: 0.5
|
||||
},
|
||||
opacity: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Notif opacity",
|
||||
|
@ -124,7 +147,7 @@ export default definePlugin({
|
|||
settings,
|
||||
flux: {
|
||||
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);
|
||||
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
||||
}
|
||||
|
@ -134,7 +157,7 @@ export default definePlugin({
|
|||
try {
|
||||
if (optimistic) return;
|
||||
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 channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
||||
|
@ -194,6 +217,7 @@ export default definePlugin({
|
|||
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) {
|
||||
for (const roleId of message.mention_roles) {
|
||||
const role = GuildStore.getRole(channel.guild_id, roleId);
|
||||
|
@ -213,6 +237,7 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
// color channel mentions
|
||||
if (channelMatches) {
|
||||
for (const cMatch of channelMatches) {
|
||||
let channelId = cMatch.split("<#")[1];
|
||||
|
@ -221,6 +246,7 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
if (shouldIgnoreForChannelType(channel)) return;
|
||||
sendMsgNotif(titleString, finalMsg, message);
|
||||
} catch (err) {
|
||||
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
||||
|
@ -229,13 +255,20 @@ 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) {
|
||||
const timeout = Math.max(settings.store.timeout, content.length * settings.store.timeoutPerCharacter);
|
||||
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
|
||||
const msgData = {
|
||||
messageType: 1,
|
||||
index: 0,
|
||||
timeout: settings.store.timeout,
|
||||
height: calculateHeight(cleanMessage(content)),
|
||||
timeout,
|
||||
height: calculateHeight(content),
|
||||
opacity: settings.store.opacity,
|
||||
volume: settings.store.volume,
|
||||
audioPath: settings.store.soundPath,
|
||||
|
@ -254,7 +287,7 @@ function sendOtherNotif(content: string, titleString: string) {
|
|||
messageType: 1,
|
||||
index: 0,
|
||||
timeout: settings.store.timeout,
|
||||
height: calculateHeight(cleanMessage(content)),
|
||||
height: calculateHeight(content),
|
||||
opacity: settings.store.opacity,
|
||||
volume: settings.store.volume,
|
||||
audioPath: settings.store.soundPath,
|
||||
|
@ -267,13 +300,11 @@ function sendOtherNotif(content: string, titleString: string) {
|
|||
Native.sendToOverlay(msgData);
|
||||
}
|
||||
|
||||
function shouldNotify(message: Message, channel: Channel) {
|
||||
function shouldNotify(message: Message, channel: string) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
if (message.author.id === currentUser.id) return false;
|
||||
if (message.author.bot && settings.store.ignoreBots) return false;
|
||||
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
|
||||
|
||||
return message.mentions.some(m => m.id === currentUser.id);
|
||||
if (message.author.bot && !settings.store.botNotifications) return false;
|
||||
return Notifs.shouldNotify(message, channel);
|
||||
}
|
||||
|
||||
function calculateHeight(content: string) {
|
||||
|
@ -282,7 +313,3 @@ function calculateHeight(content: string) {
|
|||
if (content.length <= 300) return 200;
|
||||
return 250;
|
||||
}
|
||||
|
||||
function cleanMessage(content: string) {
|
||||
return content.replace(new RegExp("<[^>]*>", "g"), "");
|
||||
}
|
||||
|
|
|
@ -423,6 +423,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
id: 347096063569559553n
|
||||
},
|
||||
|
||||
nin0dev: {
|
||||
name: "nin0dev",
|
||||
id: 886685857560539176n
|
||||
},
|
||||
Elvyra: {
|
||||
name: "Elvyra",
|
||||
id: 708275751816003615n,
|
||||
|
@ -463,9 +467,17 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "Oleh Polisan",
|
||||
id: 242305263313485825n
|
||||
},
|
||||
HAHALOSAH: {
|
||||
name: "HAHALOSAH",
|
||||
id: 903418691268513883n
|
||||
},
|
||||
GabiRP: {
|
||||
name: "GabiRP",
|
||||
id: 507955112027750401n
|
||||
},
|
||||
ImBanana: {
|
||||
name: "Im_Banana",
|
||||
id: 635250116688871425n
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
|
24
src/utils/mergeDefaults.ts
Normal file
24
src/utils/mergeDefaults.ts
Normal 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;
|
||||
}
|
|
@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common";
|
|||
|
||||
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
|
||||
* classes("one", "two") => "one two"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { PlainSettings, Settings } from "@api/Settings";
|
||||
import { Toasts } from "@webpack/common";
|
||||
import { moment, Toasts } from "@webpack/common";
|
||||
import { deflateSync, inflateSync } from "fflate";
|
||||
|
||||
import { getCloudAuth, getCloudUrl } from "./cloud";
|
||||
|
@ -49,7 +49,7 @@ export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
|
|||
}
|
||||
|
||||
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 data = new TextEncoder().encode(backup);
|
||||
|
||||
|
|
Loading…
Reference in a new issue