diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx
index 96aac303..e8e6f178 100644
--- a/src/plugins/_core/supportHelper.tsx
+++ b/src/plugins/_core/supportHelper.tsx
@@ -156,7 +156,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:
diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx
index 7d81c6f5..5064bd53 100644
--- a/src/plugins/betterSettings/index.tsx
+++ b/src/plugins/betterSettings/index.tsx
@@ -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
;
+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 (
- props.children as any}>
-
-
- );
+ if (!FocusLock || !ComponentDispatch || !Classes) {
+ new Logger("BetterSettings").error("Failed to find some components");
+ return props.children;
+ }
+
+ return ;
},
wrapMenu(list: SettingsEntry[]) {
diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts
index f8c76d7f..10053021 100644
--- a/src/plugins/crashHandler/index.ts
+++ b/src/plugins/crashHandler/index.ts
@@ -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 {
diff --git a/src/plugins/showTimeoutDuration/README.md b/src/plugins/showTimeoutDuration/README.md
new file mode 100644
index 00000000..13780247
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/README.md
@@ -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!
+
+
+
+
diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx
new file mode 100644
index 00000000..f57ee0fc
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/index.tsx
@@ -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 = () => (
+
+ );
+
+ 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 (
+ <>
+ {i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}
+
+ {renderTimeout(message, false)}
+
+ >
+ );
+ }, { noop: true }),
+
+ TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
+ return (
+
+
+
+
+ {renderTimeout(message, true)} timeout remaining
+
+
+ );
+ }, { noop: true })
+});
diff --git a/src/plugins/showTimeoutDuration/styles.css b/src/plugins/showTimeoutDuration/styles.css
new file mode 100644
index 00000000..70a826e1
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/styles.css
@@ -0,0 +1,4 @@
+.vc-std-wrapper {
+ display: flex;
+ align-items: center;
+}
diff --git a/src/plugins/voiceDownload/index.tsx b/src/plugins/voiceDownload/index.tsx
index 8586b9f9..571c3d0e 100644
--- a/src/plugins/voiceDownload/index.tsx
+++ b/src/plugins/voiceDownload/index.tsx
@@ -28,9 +28,12 @@ export default definePlugin({
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)
+ }
>