From 49c48393e05b4c2986bcda1161e379d01e4e4541 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 24 Aug 2024 17:15:36 +1200 Subject: [PATCH 01/14] ShowTimeoutDuration: Rename to ShowTimeoutDetails and add timeout reasons --- .../showTimeoutDuration/TimeoutReasonStore.ts | 90 +++++++++++++++++++ src/plugins/showTimeoutDuration/index.tsx | 77 ++++++++++++---- src/plugins/showTimeoutDuration/styles.css | 8 ++ 3 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/plugins/showTimeoutDuration/TimeoutReasonStore.ts diff --git a/src/plugins/showTimeoutDuration/TimeoutReasonStore.ts b/src/plugins/showTimeoutDuration/TimeoutReasonStore.ts new file mode 100644 index 000000000..c693f2c04 --- /dev/null +++ b/src/plugins/showTimeoutDuration/TimeoutReasonStore.ts @@ -0,0 +1,90 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { proxyLazy } from "@utils/lazy"; +import { findByPropsLazy } from "@webpack"; +import { Constants, Flux, FluxDispatcher, GuildMemberStore, PermissionsBits, PermissionStore, RestAPI, useStateFromStores } from "@webpack/common"; + +import { settings } from "."; + +const AuditLogReasons: { + MEMBER_UPDATE: number; + AUTO_MODERATION_USER_COMMUNICATION_DISABLED: number; + [reason: string]: number; +} = findByPropsLazy("ALL", "AUTO_MODERATION_USER_COMMUNICATION_DISABLED", "MEMBER_UPDATE"); + +export type TimeoutEntry = { + reason: string | undefined; + moderator: string | undefined; + automod: boolean | undefined; + expires: string | undefined; // used to compare if timeout reason is different + loading: boolean; +}; + +export const NoTimeout: TimeoutEntry = { + reason: undefined, + moderator: undefined, + automod: undefined, + expires: undefined, + loading: false +}; + +export const TimeoutLoading: TimeoutEntry = { + reason: undefined, + moderator: undefined, + automod: undefined, + expires: undefined, + loading: true +}; + +export const TimeoutReasonStore = proxyLazy(() => { + class TimeoutReasonStore extends Flux.Store { + public reasonMap = new Map(); + + getReason(guildId: string, userId: string) { + const member = GuildMemberStore.getMember(guildId, userId); + if (!member?.communicationDisabledUntil) return NoTimeout; + const timeoutExpiry = new Date(member?.communicationDisabledUntil!); + if (timeoutExpiry <= new Date()) return NoTimeout; + const reason = this.reasonMap.get(`${guildId}-${userId}`); + if (reason && !reason.loading && reason.expires === member?.communicationDisabledUntil) return reason; + if (!PermissionStore.canWithPartialContext(PermissionsBits.VIEW_AUDIT_LOG, { guildId })) return NoTimeout; + this.reasonMap.set(`${guildId}-${userId}`, TimeoutLoading); + RestAPI.get({ + url: Constants.Endpoints.GUILD_AUDIT_LOG(guildId), + query: { + target_id: userId, + limit: 100 + } + }).then(logs => { + const entry = logs.body.audit_log_entries.find((entry: { action_type: number; changes: any[]; }) => { + if (entry.action_type === AuditLogReasons.AUTO_MODERATION_USER_COMMUNICATION_DISABLED) return true; + if (entry.action_type === AuditLogReasons.MEMBER_UPDATE && entry?.changes.some((change: { key: string; }) => change.key === "communication_disabled_until")) return true; + }); + if (!entry) return this.reasonMap.set(`${guildId}-${userId}`, NoTimeout); + const isAutomod = entry.action_type === AuditLogReasons.AUTO_MODERATION_USER_COMMUNICATION_DISABLED; + this.reasonMap.set(`${guildId}-${userId}`, { + reason: entry.reason, + moderator: isAutomod ? undefined : entry.user_id, + automod: isAutomod, + expires: member?.communicationDisabledUntil, + loading: false + }); + this.emitChange(); + }); + return TimeoutLoading; + } + } + + const store = new TimeoutReasonStore(FluxDispatcher, {}); + + return store; +}); + +export const useTimeoutReason = (guildId: string, userId: string) => useStateFromStores([TimeoutReasonStore], () => { + if (settings.store.showReason) return TimeoutReasonStore.getReason(guildId, userId); + return NoTimeout; +}); diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx index bfe806802..bcb27a821 100644 --- a/src/plugins/showTimeoutDuration/index.tsx +++ b/src/plugins/showTimeoutDuration/index.tsx @@ -6,15 +6,20 @@ import "./styles.css"; -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { findComponentLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, i18n, Parser, Text, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { FunctionComponent, ReactNode } from "react"; +import { TimeoutEntry, TimeoutReasonStore, useTimeoutReason } from "./TimeoutReasonStore"; + + + const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER")); const enum DisplayStyle { @@ -22,18 +27,23 @@ const enum DisplayStyle { Inline = "ssalggnikool" } -const settings = definePluginSettings({ +export const settings = definePluginSettings({ displayStyle: { - description: "How to display the timeout duration", + description: "How to display the timeout duration and reason", type: OptionType.SELECT, options: [ { label: "In the Tooltip", value: DisplayStyle.Tooltip }, { label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true }, ], + }, + showReason: { + description: "Should timeout reasons be shown?", + type: OptionType.BOOLEAN, + default: false } }); -function renderTimeout(message: Message, inline: boolean) { +function renderTimeout(message: Message, inline: boolean, reason?: TimeoutEntry) { const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id; if (!guildId) return null; @@ -50,15 +60,38 @@ function renderTimeout(message: Message, inline: boolean) { return inline ? countdown() - : i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({ - username: message.author.username, - countdown - }); + : <> + {i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({ + username: message.author.username, + countdown + })} + {reason && } + ; } + +function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: TimeoutEntry; message: Message; }) { + if (reason.loading) return null; + const details = [ + reason.moderator && Parser.parse(`<@${reason.moderator}>`, true, { + channelId: message.channel_id, + messageId: message.id + }), + reason.moderator && " ", + reason.reason + ]; + if (!details.some(Boolean)) return null; + return [ + isTooltip ? "\n" : : , + ...details + ]; +} + +migratePluginSettings("ShowTimeoutDetails", "ShowTimeoutDuration"); + 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", + name: "ShowTimeoutDetails", + description: "Shows how much longer a user's timeout will last and why they are timed out, either in the timeout icon tooltip or next to it", authors: [Devs.Ven, Devs.Sqaaakoi], settings, @@ -75,18 +108,28 @@ export default definePlugin({ } ], + TimeoutReasonStore, + TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent; text: ReactNode; }) => { + const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id; + const timeoutReason = useTimeoutReason(guildId, message.author.id); + if (settings.store.displayStyle === DisplayStyle.Tooltip) return ( + + {children(props)} + + )} />; return ( -
+
- - {renderTimeout(message, true)} timeout remaining + + {renderTimeout(message, true)} timeout remaining + {settings.store.showReason && }
); - }, { noop: true }) + }, { noop: true }), }); diff --git a/src/plugins/showTimeoutDuration/styles.css b/src/plugins/showTimeoutDuration/styles.css index a6f830c38..c2010d268 100644 --- a/src/plugins/showTimeoutDuration/styles.css +++ b/src/plugins/showTimeoutDuration/styles.css @@ -6,3 +6,11 @@ .vc-std-wrapper [class*="communicationDisabled"] { margin-right: 0; } + +.vc-std-wrapper-text { + color: var(--status-danger); +} + +.vc-std-automod :is(svg, .vc-std-wrapper-text) { + color: var(--status-warning); +} From ad64da0621194d9948c1cc7361278fef5be29e68 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 24 Aug 2024 17:23:28 +1200 Subject: [PATCH 02/14] ShowTimeoutDetails: Rename folder to showTimeoutDetails --- src/plugins/{showTimeoutDuration => showTimeoutDetails}/README.md | 0 .../TimeoutReasonStore.ts | 0 src/plugins/{showTimeoutDuration => showTimeoutDetails}/index.tsx | 0 .../{showTimeoutDuration => showTimeoutDetails}/styles.css | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/plugins/{showTimeoutDuration => showTimeoutDetails}/README.md (100%) rename src/plugins/{showTimeoutDuration => showTimeoutDetails}/TimeoutReasonStore.ts (100%) rename src/plugins/{showTimeoutDuration => showTimeoutDetails}/index.tsx (100%) rename src/plugins/{showTimeoutDuration => showTimeoutDetails}/styles.css (100%) diff --git a/src/plugins/showTimeoutDuration/README.md b/src/plugins/showTimeoutDetails/README.md similarity index 100% rename from src/plugins/showTimeoutDuration/README.md rename to src/plugins/showTimeoutDetails/README.md diff --git a/src/plugins/showTimeoutDuration/TimeoutReasonStore.ts b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts similarity index 100% rename from src/plugins/showTimeoutDuration/TimeoutReasonStore.ts rename to src/plugins/showTimeoutDetails/TimeoutReasonStore.ts diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDetails/index.tsx similarity index 100% rename from src/plugins/showTimeoutDuration/index.tsx rename to src/plugins/showTimeoutDetails/index.tsx diff --git a/src/plugins/showTimeoutDuration/styles.css b/src/plugins/showTimeoutDetails/styles.css similarity index 100% rename from src/plugins/showTimeoutDuration/styles.css rename to src/plugins/showTimeoutDetails/styles.css From 8c0d54f13ac55055ad656678f4f8af79de5eda15 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 24 Aug 2024 18:18:08 +1200 Subject: [PATCH 03/14] ShowTimeoutDetails: Make 5% more readable and document it badly --- .../showTimeoutDetails/TimeoutReasonStore.ts | 22 +++++++++++++++---- src/plugins/showTimeoutDetails/index.tsx | 8 ++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts index c693f2c04..8cddcceec 100644 --- a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts +++ b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts @@ -18,7 +18,7 @@ const AuditLogReasons: { export type TimeoutEntry = { reason: string | undefined; - moderator: string | undefined; + moderator: string | undefined; // User ID of moderator, undefined if automod did the timeout automod: boolean | undefined; expires: string | undefined; // used to compare if timeout reason is different loading: boolean; @@ -46,16 +46,24 @@ export const TimeoutReasonStore = proxyLazy(() => { getReason(guildId: string, userId: string) { const member = GuildMemberStore.getMember(guildId, userId); + if (!member?.communicationDisabledUntil) return NoTimeout; - const timeoutExpiry = new Date(member?.communicationDisabledUntil!); - if (timeoutExpiry <= new Date()) return NoTimeout; + if (new Date(member?.communicationDisabledUntil!) <= new Date()) return NoTimeout; + const reason = this.reasonMap.get(`${guildId}-${userId}`); - if (reason && !reason.loading && reason.expires === member?.communicationDisabledUntil) return reason; + // Return if timeout reason entry is found and is up to date, or if it's still loading + if (reason && (reason.loading ? true : reason.expires === member?.communicationDisabledUntil)) return reason; + + // The indicator being visible does not depend on any data here. This just returns that there's no extra information about the timeout. if (!PermissionStore.canWithPartialContext(PermissionsBits.VIEW_AUDIT_LOG, { guildId })) return NoTimeout; + + // Stop requesting data multiple times this.reasonMap.set(`${guildId}-${userId}`, TimeoutLoading); + RestAPI.get({ url: Constants.Endpoints.GUILD_AUDIT_LOG(guildId), query: { + // action_type is intentionally not specified here as we need multiple types of audit log actions. target_id: userId, limit: 100 } @@ -64,8 +72,11 @@ export const TimeoutReasonStore = proxyLazy(() => { if (entry.action_type === AuditLogReasons.AUTO_MODERATION_USER_COMMUNICATION_DISABLED) return true; if (entry.action_type === AuditLogReasons.MEMBER_UPDATE && entry?.changes.some((change: { key: string; }) => change.key === "communication_disabled_until")) return true; }); + if (!entry) return this.reasonMap.set(`${guildId}-${userId}`, NoTimeout); + const isAutomod = entry.action_type === AuditLogReasons.AUTO_MODERATION_USER_COMMUNICATION_DISABLED; + this.reasonMap.set(`${guildId}-${userId}`, { reason: entry.reason, moderator: isAutomod ? undefined : entry.user_id, @@ -73,8 +84,11 @@ export const TimeoutReasonStore = proxyLazy(() => { expires: member?.communicationDisabledUntil, loading: false }); + + // Re-render the timeout indicator components this.emitChange(); }); + return TimeoutLoading; } } diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index bcb27a821..d3323b379 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -77,14 +77,16 @@ function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: T channelId: message.channel_id, messageId: message.id }), - reason.moderator && " ", + reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}**`, true), reason.reason ]; if (!details.some(Boolean)) return null; - return [ + const result = [ isTooltip ? "\n" : : , - ...details + ...details.flatMap(i => [i, " "]) ]; + result.pop(); + return result; } migratePluginSettings("ShowTimeoutDetails", "ShowTimeoutDuration"); From e5f743c922f6b86e9dcd8cbd4e9970598aeaddb1 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 24 Aug 2024 18:27:27 +1200 Subject: [PATCH 04/14] ShowTimeoutDetails: Add tags for old/related names --- src/plugins/showTimeoutDetails/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index d3323b379..286cf38be 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -95,6 +95,7 @@ export default definePlugin({ name: "ShowTimeoutDetails", description: "Shows how much longer a user's timeout will last and why they are timed out, either in the timeout icon tooltip or next to it", authors: [Devs.Ven, Devs.Sqaaakoi], + tags: ["ShowTimeoutDuration", "ShowTimeoutReason"], settings, From c27e01591e331c058fd714401400fa942ebb4186 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sun, 25 Aug 2024 02:36:40 +1200 Subject: [PATCH 05/14] ShowTimeoutDetails: better new line (maybe) --- src/plugins/showTimeoutDetails/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 286cf38be..0036833b2 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -9,6 +9,7 @@ import "./styles.css"; import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; +import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { findComponentLazy } from "@webpack"; @@ -82,7 +83,7 @@ function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: T ]; if (!details.some(Boolean)) return null; const result = [ - isTooltip ? "\n" : : , + isTooltip ?
: : , ...details.flatMap(i => [i, " "]) ]; result.pop(); From 2c33b5ac4d9c763c5c39f0454a312a39a756b651 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Tue, 27 Aug 2024 23:12:49 +1200 Subject: [PATCH 06/14] ShowTimeoutDetails: Fix tiny space between content in tooltip --- src/plugins/showTimeoutDetails/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 0036833b2..76326c54e 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -83,7 +83,7 @@ function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: T ]; if (!details.some(Boolean)) return null; const result = [ - isTooltip ?
: : , + isTooltip ?
: : , ...details.flatMap(i => [i, " "]) ]; result.pop(); From 2099d6ec94e1f4ea795adb6a1afa034b94c345ab Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Tue, 27 Aug 2024 23:34:27 +1200 Subject: [PATCH 07/14] ShowTimeoutDetails: Better AutoMod reasons (bad code?) --- src/plugins/showTimeoutDetails/TimeoutReasonStore.ts | 8 ++++++++ src/plugins/showTimeoutDetails/index.tsx | 12 +++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts index 8cddcceec..05e743765 100644 --- a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts +++ b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts @@ -20,6 +20,8 @@ export type TimeoutEntry = { reason: string | undefined; moderator: string | undefined; // User ID of moderator, undefined if automod did the timeout automod: boolean | undefined; + automodRuleName: string | undefined; + automodChannelId: string | undefined; expires: string | undefined; // used to compare if timeout reason is different loading: boolean; }; @@ -28,6 +30,8 @@ export const NoTimeout: TimeoutEntry = { reason: undefined, moderator: undefined, automod: undefined, + automodRuleName: undefined, + automodChannelId: undefined, expires: undefined, loading: false }; @@ -36,6 +40,8 @@ export const TimeoutLoading: TimeoutEntry = { reason: undefined, moderator: undefined, automod: undefined, + automodRuleName: undefined, + automodChannelId: undefined, expires: undefined, loading: true }; @@ -81,6 +87,8 @@ export const TimeoutReasonStore = proxyLazy(() => { reason: entry.reason, moderator: isAutomod ? undefined : entry.user_id, automod: isAutomod, + automodRuleName: isAutomod ? entry?.options.auto_moderation_rule_name : undefined, + automodChannelId: isAutomod ? entry?.options?.channel_id : undefined, expires: member?.communicationDisabledUntil, loading: false }); diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 76326c54e..35948960c 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -78,7 +78,17 @@ function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: T channelId: message.channel_id, messageId: message.id }), - reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}**`, true), + reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}**` + (() => { + const automodDetails = [ + reason.automodRuleName, + reason.automodChannelId && `<#${reason.automodChannelId}>`, + ].filter(Boolean); + if (automodDetails.length) return ": " + automodDetails.join(" "); + return ""; + })(), true, { + channelId: message.channel_id, + messageId: message.id + }), reason.reason ]; if (!details.some(Boolean)) return null; From 7ceaa5a6f0cf5c765edc21c1cf9353a979fcab93 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Wed, 28 Aug 2024 03:15:31 +1200 Subject: [PATCH 08/14] ShowTimeoutDetails: Make automod display work as intended --- src/plugins/showTimeoutDetails/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 35948960c..86e148a32 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -78,14 +78,14 @@ function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: T channelId: message.channel_id, messageId: message.id }), - reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}**` + (() => { + reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}${(() => { const automodDetails = [ reason.automodRuleName, reason.automodChannelId && `<#${reason.automodChannelId}>`, ].filter(Boolean); if (automodDetails.length) return ": " + automodDetails.join(" "); return ""; - })(), true, { + })()}**`, true, { channelId: message.channel_id, messageId: message.id }), From d17c0ea387dfe4af905da2fd4a479417de19f58a Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 31 Aug 2024 05:48:32 +1200 Subject: [PATCH 09/14] ShowTimeoutDetails: Refactor UI into a popout --- .../components/TimeoutDetailsPopout.tsx | 88 +++++++++++++++++ .../components/TooltipWrapper.tsx | 85 ++++++++++++++++ src/plugins/showTimeoutDetails/index.tsx | 98 +------------------ src/plugins/showTimeoutDetails/styles.css | 18 ++++ 4 files changed, 195 insertions(+), 94 deletions(-) create mode 100644 src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx create mode 100644 src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx diff --git a/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx new file mode 100644 index 000000000..2e8edf16f --- /dev/null +++ b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx @@ -0,0 +1,88 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { SafetyIcon } from "@components/Icons"; +import { classes, Margins } from "@utils/index"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { Button, Dialog, GuildStore, i18n, Parser, PermissionsBits, PermissionStore, Text, Tooltip, UserStore, useState, useStateFromStores } from "@webpack/common"; +import { Message } from "discord-types/general"; + +import { useTimeoutReason } from "../TimeoutReasonStore"; + + +const PopoutClasses = findByPropsLazy("container", "scroller", "list"); +const rowClasses = findByPropsLazy("row", "rowIcon", "rowGuildName"); + +const ChannelIcon = findComponentByCodeLazy("h4.97l-.8 4.84a1 1 0 0 0 1.97.32l.86-5.16H20a1"); +const CustomAutoModRuleIcon = findComponentByCodeLazy("a1 1 0 0 1 1-1h8a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1ZM3 19a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H3Z"); +const MessageIcon = findComponentByCodeLazy('"M12 22a10 10 0 1 0-8.45-4.64c.13.19.11.44-.04.61l-2.06 2.37A1 1 0 0 0 2.2 22H12Z"'); + +const { setCommunicationDisabledUntil } = findByPropsLazy("setCommunicationDisabledUntil"); + +export default function TimeoutDetailsPopout({ closePopout, guildId, userId, message }: { closePopout(): void; guildId: string; userId: string; message: Message; }) { + const user = UserStore.getUser(userId); + const reason = useTimeoutReason(guildId, userId); + const hasModerationPermission = useStateFromStores([PermissionStore], () => PermissionStore.canManageUser(PermissionsBits.MODERATE_MEMBERS, userId, GuildStore.getGuild(guildId))); + const parse = (text: string) => Parser.parse(text, true, { + channelId: message.channel_id, + messageId: message.id + }); + + const [cancelling, setCancelling] = useState(false); + + return + + Timeout details for {user.username} + +
+ {(reason.moderator || reason.automod) &&
+ + {p => } + + {reason.automod ? i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE : parse(`<@${reason.moderator}>`)} +
} + {(reason.automodChannelId) &&
+ + {p => } + + {parse(`<#${reason.automodChannelId}>`)} +
} + {(reason.automodRuleName) &&
+ + {p => } + + {reason.automodRuleName} +
} + {(reason.reason) &&
+ + {p => } + + {reason.reason} +
} + {hasModerationPermission &&
} +
; +} diff --git a/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx new file mode 100644 index 000000000..46e3bfb4a --- /dev/null +++ b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx @@ -0,0 +1,85 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classes } from "@utils/misc"; +import { findByPropsLazy, findComponentLazy } from "@webpack"; +import { ChannelStore, GuildMemberStore, i18n, Popout, Text, Tooltip } from "@webpack/common"; +import { Message } from "discord-types/general"; +import { FunctionComponent, ReactNode } from "react"; + +import { DisplayStyle, settings } from ".."; +import { useTimeoutReason } from "../TimeoutReasonStore"; +import TimeoutDetailsPopout from "./TimeoutDetailsPopout"; + +const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER")); + +const clickableClasses = findByPropsLazy("clickable", "avatar", "username"); + +export default function TooltipWrapper({ message, children, text }: { message: Message; children: FunctionComponent; text: ReactNode; }) { + const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id; + const timeoutReason = useTimeoutReason(guildId, message.author.id); + + if (settings.store.displayStyle === DisplayStyle.Tooltip) return ( + } + > + {popoutProps => { e.stopPropagation(); popoutProps.onClick(e); }} // stop double click to reply/edit + > + {children(props)} + } + + )} + />; + return ( + } + > + {popoutProps =>
{ e.stopPropagation(); popoutProps.onClick(e); }} // stop double click to reply/edit + > + + + {renderTimeout(message, true)} timeout remaining + +
} +
+ ); +} + +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 + }); +} diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 86e148a32..187612bed 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -9,21 +9,13 @@ import "./styles.css"; import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; -import { findComponentLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, i18n, Parser, Text, Tooltip } from "@webpack/common"; -import { Message } from "discord-types/general"; -import { FunctionComponent, ReactNode } from "react"; -import { TimeoutEntry, TimeoutReasonStore, useTimeoutReason } from "./TimeoutReasonStore"; +import TooltipWrapper from "./components/TooltipWrapper"; +import { TimeoutReasonStore } from "./TimeoutReasonStore"; - -const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER")); - -const enum DisplayStyle { +export const enum DisplayStyle { Tooltip = "tooltip", Inline = "ssalggnikool" } @@ -36,70 +28,9 @@ export const settings = definePluginSettings({ { label: "In the Tooltip", value: DisplayStyle.Tooltip }, { label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true }, ], - }, - showReason: { - description: "Should timeout reasons be shown?", - type: OptionType.BOOLEAN, - default: false } }); -function renderTimeout(message: Message, inline: boolean, reason?: TimeoutEntry) { - 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 - })} - {reason && } - ; -} - - -function Reason({ isTooltip, reason, message }: { isTooltip?: boolean, reason: TimeoutEntry; message: Message; }) { - if (reason.loading) return null; - const details = [ - reason.moderator && Parser.parse(`<@${reason.moderator}>`, true, { - channelId: message.channel_id, - messageId: message.id - }), - reason.automod && Parser.parse(`**${i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE}${(() => { - const automodDetails = [ - reason.automodRuleName, - reason.automodChannelId && `<#${reason.automodChannelId}>`, - ].filter(Boolean); - if (automodDetails.length) return ": " + automodDetails.join(" "); - return ""; - })()}**`, true, { - channelId: message.channel_id, - messageId: message.id - }), - reason.reason - ]; - if (!details.some(Boolean)) return null; - const result = [ - isTooltip ?
: : , - ...details.flatMap(i => [i, " "]) - ]; - result.pop(); - return result; -} - migratePluginSettings("ShowTimeoutDetails", "ShowTimeoutDuration"); export default definePlugin({ @@ -124,26 +55,5 @@ export default definePlugin({ TimeoutReasonStore, - TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent; text: ReactNode; }) => { - const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id; - const timeoutReason = useTimeoutReason(guildId, message.author.id); - - if (settings.store.displayStyle === DisplayStyle.Tooltip) return ( - - {children(props)} - - )} - />; - return ( -
- - - {renderTimeout(message, true)} timeout remaining - {settings.store.showReason && } - -
- ); - }, { noop: true }), + TooltipWrapper: ErrorBoundary.wrap(TooltipWrapper, { noop: true }) }); diff --git a/src/plugins/showTimeoutDetails/styles.css b/src/plugins/showTimeoutDetails/styles.css index c2010d268..a7e153195 100644 --- a/src/plugins/showTimeoutDetails/styles.css +++ b/src/plugins/showTimeoutDetails/styles.css @@ -14,3 +14,21 @@ .vc-std-automod :is(svg, .vc-std-wrapper-text) { color: var(--status-warning); } + +.vc-std-tooltip { + max-width: 320px; +} + +.vc-std-popout { + width: unset; + min-width: 180px; + color: var(--text-normal); +} + +.vc-std-popout-button-wrapper { + margin-top: 8px; +} + +.vc-std-popout-button { + padding: 2px 10px; +} From a6058f8285fa16ddafab619e763bbf1e6afb103a Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 31 Aug 2024 06:23:05 +1200 Subject: [PATCH 10/14] ShowTimeoutDetails: Clean up refactored code --- .../showTimeoutDetails/TimeoutReasonStore.ts | 4 +-- .../components/TooltipWrapper.tsx | 33 ++++++++++++------- src/plugins/showTimeoutDetails/styles.css | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts index 05e743765..0741789a5 100644 --- a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts +++ b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts @@ -8,7 +8,6 @@ import { proxyLazy } from "@utils/lazy"; import { findByPropsLazy } from "@webpack"; import { Constants, Flux, FluxDispatcher, GuildMemberStore, PermissionsBits, PermissionStore, RestAPI, useStateFromStores } from "@webpack/common"; -import { settings } from "."; const AuditLogReasons: { MEMBER_UPDATE: number; @@ -107,6 +106,5 @@ export const TimeoutReasonStore = proxyLazy(() => { }); export const useTimeoutReason = (guildId: string, userId: string) => useStateFromStores([TimeoutReasonStore], () => { - if (settings.store.showReason) return TimeoutReasonStore.getReason(guildId, userId); - return NoTimeout; + return TimeoutReasonStore.getReason(guildId, userId); }); diff --git a/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx index 46e3bfb4a..91d3b2b83 100644 --- a/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx +++ b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { classes } from "@utils/misc"; +import { classes, Margins } from "@utils/index"; import { findByPropsLazy, findComponentLazy } from "@webpack"; import { ChannelStore, GuildMemberStore, i18n, Popout, Text, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; @@ -53,6 +53,7 @@ export default function TooltipWrapper({ message, children, text }: { message: M onClick={e => { e.stopPropagation(); popoutProps.onClick(e); }} // stop double click to reply/edit > + {renderTimeout(message, true)} timeout remaining @@ -68,18 +69,26 @@ function renderTimeout(message: Message, inline: boolean) { const member = GuildMemberStore.getMember(guildId, message.author.id); if (!member?.communicationDisabledUntil) return null; - const countdown = () => ( - - ); + const countdown = () => <> + + + + + + ; return inline ? countdown() - : i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({ - username: message.author.username, - countdown - }); + : <> + {i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({ + username: message.author.username, + countdown + })} +
+ Click for more details. + ; } diff --git a/src/plugins/showTimeoutDetails/styles.css b/src/plugins/showTimeoutDetails/styles.css index a7e153195..8888ea0cc 100644 --- a/src/plugins/showTimeoutDetails/styles.css +++ b/src/plugins/showTimeoutDetails/styles.css @@ -16,7 +16,7 @@ } .vc-std-tooltip { - max-width: 320px; + max-width: 220px; } .vc-std-popout { From d0f048f81deb4b341288958dbcace85dda5d1c37 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Tue, 3 Sep 2024 00:30:47 +1200 Subject: [PATCH 11/14] ShowTimeoutDetails: add max-width: 360px to popout --- src/plugins/showTimeoutDetails/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/showTimeoutDetails/styles.css b/src/plugins/showTimeoutDetails/styles.css index 8888ea0cc..b28d23391 100644 --- a/src/plugins/showTimeoutDetails/styles.css +++ b/src/plugins/showTimeoutDetails/styles.css @@ -22,6 +22,7 @@ .vc-std-popout { width: unset; min-width: 180px; + max-width: 360px; color: var(--text-normal); } From cfb4972394aed602e13dd28fe9b1864267299704 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Wed, 4 Sep 2024 05:17:23 +1200 Subject: [PATCH 12/14] ShowTimeoutDetails: Add timeout duration to popout (why wasn't this here?) --- .../components/TimeoutDetailsPopout.tsx | 16 ++++++++++++++-- .../components/TooltipWrapper.tsx | 6 ++---- src/plugins/showTimeoutDetails/index.tsx | 2 ++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx index 2e8edf16f..9a2fbb86b 100644 --- a/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx +++ b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx @@ -7,15 +7,16 @@ import { SafetyIcon } from "@components/Icons"; import { classes, Margins } from "@utils/index"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; -import { Button, Dialog, GuildStore, i18n, Parser, PermissionsBits, PermissionStore, Text, Tooltip, UserStore, useState, useStateFromStores } from "@webpack/common"; +import { Button, Dialog, GuildMemberStore, GuildStore, i18n, Parser, PermissionsBits, PermissionStore, Text, Tooltip, UserStore, useState, useStateFromStores } from "@webpack/common"; import { Message } from "discord-types/general"; +import { CountDown } from ".."; import { useTimeoutReason } from "../TimeoutReasonStore"; - const PopoutClasses = findByPropsLazy("container", "scroller", "list"); const rowClasses = findByPropsLazy("row", "rowIcon", "rowGuildName"); +const TimeoutIcon = findComponentByCodeLazy("M12 23c.08 0 .14-.08.11-.16a2.88 2.88 0 0 1 .29-2.31l2.2-3.85"); const ChannelIcon = findComponentByCodeLazy("h4.97l-.8 4.84a1 1 0 0 0 1.97.32l.86-5.16H20a1"); const CustomAutoModRuleIcon = findComponentByCodeLazy("a1 1 0 0 1 1-1h8a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1ZM3 19a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H3Z"); const MessageIcon = findComponentByCodeLazy('"M12 22a10 10 0 1 0-8.45-4.64c.13.19.11.44-.04.61l-2.06 2.37A1 1 0 0 0 2.2 22H12Z"'); @@ -24,6 +25,7 @@ const { setCommunicationDisabledUntil } = findByPropsLazy("setCommunicationDisab export default function TimeoutDetailsPopout({ closePopout, guildId, userId, message }: { closePopout(): void; guildId: string; userId: string; message: Message; }) { const user = UserStore.getUser(userId); + const member = GuildMemberStore.getMember(guildId, userId); const reason = useTimeoutReason(guildId, userId); const hasModerationPermission = useStateFromStores([PermissionStore], () => PermissionStore.canManageUser(PermissionsBits.MODERATE_MEMBERS, userId, GuildStore.getGuild(guildId))); const parse = (text: string) => Parser.parse(text, true, { @@ -40,6 +42,16 @@ export default function TimeoutDetailsPopout({ closePopout, guildId, userId, mes Timeout details for {user.username}
+
+ + {p => } + + +
{(reason.moderator || reason.automod) &&
{p => } diff --git a/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx index 91d3b2b83..85194d8e6 100644 --- a/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx +++ b/src/plugins/showTimeoutDetails/components/TooltipWrapper.tsx @@ -5,17 +5,15 @@ */ import { classes, Margins } from "@utils/index"; -import { findByPropsLazy, findComponentLazy } from "@webpack"; +import { findByPropsLazy } from "@webpack"; import { ChannelStore, GuildMemberStore, i18n, Popout, Text, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { FunctionComponent, ReactNode } from "react"; -import { DisplayStyle, settings } from ".."; +import { CountDown, DisplayStyle, settings } from ".."; import { useTimeoutReason } from "../TimeoutReasonStore"; import TimeoutDetailsPopout from "./TimeoutDetailsPopout"; -const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER")); - const clickableClasses = findByPropsLazy("clickable", "avatar", "username"); export default function TooltipWrapper({ message, children, text }: { message: Message; children: FunctionComponent; text: ReactNode; }) { diff --git a/src/plugins/showTimeoutDetails/index.tsx b/src/plugins/showTimeoutDetails/index.tsx index 187612bed..5a850ba09 100644 --- a/src/plugins/showTimeoutDetails/index.tsx +++ b/src/plugins/showTimeoutDetails/index.tsx @@ -10,10 +10,12 @@ import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; +import { findComponentLazy } from "@webpack"; import TooltipWrapper from "./components/TooltipWrapper"; import { TimeoutReasonStore } from "./TimeoutReasonStore"; +export const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER")); export const enum DisplayStyle { Tooltip = "tooltip", From 1b9ea1f413639b4f1d96de849edcb47b0b5b2434 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Wed, 4 Sep 2024 05:38:20 +1200 Subject: [PATCH 13/14] ShowTimeoutDetails: Questionable refactor --- .../showTimeoutDetails/TimeoutReasonStore.ts | 2 +- .../components/TimeoutDetailsPopout.tsx | 62 +++++++++++-------- .../components/TimeoutDetailsRow.tsx | 26 ++++++++ 3 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 src/plugins/showTimeoutDetails/components/TimeoutDetailsRow.tsx diff --git a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts index 0741789a5..283b976ef 100644 --- a/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts +++ b/src/plugins/showTimeoutDetails/TimeoutReasonStore.ts @@ -86,7 +86,7 @@ export const TimeoutReasonStore = proxyLazy(() => { reason: entry.reason, moderator: isAutomod ? undefined : entry.user_id, automod: isAutomod, - automodRuleName: isAutomod ? entry?.options.auto_moderation_rule_name : undefined, + automodRuleName: isAutomod ? entry?.options?.auto_moderation_rule_name : undefined, automodChannelId: isAutomod ? entry?.options?.channel_id : undefined, expires: member?.communicationDisabledUntil, loading: false diff --git a/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx index 9a2fbb86b..4067eebce 100644 --- a/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx +++ b/src/plugins/showTimeoutDetails/components/TimeoutDetailsPopout.tsx @@ -7,14 +7,14 @@ import { SafetyIcon } from "@components/Icons"; import { classes, Margins } from "@utils/index"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; -import { Button, Dialog, GuildMemberStore, GuildStore, i18n, Parser, PermissionsBits, PermissionStore, Text, Tooltip, UserStore, useState, useStateFromStores } from "@webpack/common"; +import { Button, Dialog, GuildMemberStore, GuildStore, i18n, Parser, PermissionsBits, PermissionStore, Text, UserStore, useState, useStateFromStores } from "@webpack/common"; import { Message } from "discord-types/general"; import { CountDown } from ".."; import { useTimeoutReason } from "../TimeoutReasonStore"; +import TimeoutDetailsRow from "./TimeoutDetailsRow"; const PopoutClasses = findByPropsLazy("container", "scroller", "list"); -const rowClasses = findByPropsLazy("row", "rowIcon", "rowGuildName"); const TimeoutIcon = findComponentByCodeLazy("M12 23c.08 0 .14-.08.11-.16a2.88 2.88 0 0 1 .29-2.31l2.2-3.85"); const ChannelIcon = findComponentByCodeLazy("h4.97l-.8 4.84a1 1 0 0 0 1.97.32l.86-5.16H20a1"); @@ -42,40 +42,48 @@ export default function TimeoutDetailsPopout({ closePopout, guildId, userId, mes Timeout details for {user.username}
-
- - {p => } - + + -
- {(reason.moderator || reason.automod) &&
- - {p => } - + + + {reason.automod ? i18n.Messages.GUILD_SETTINGS_AUTOMOD_TITLE : parse(`<@${reason.moderator}>`)} -
} - {(reason.automodChannelId) &&
- - {p => } - + + + {parse(`<#${reason.automodChannelId}>`)} -
} - {(reason.automodRuleName) &&
- - {p => } - + + + {reason.automodRuleName} -
} - {(reason.reason) &&
- - {p => } - + + + {reason.reason} -
} + + {hasModerationPermission &&