BetterBanReasons: Add per-reason message delete duration option (#1024)

* Add per-reason message delete duration option

* Update index.tsx

* Update index.tsx

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Yonn 2026-04-28 13:51:34 +02:00 • committed by GitHub
parent ae72ab6a51
commit c2e966e50d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 34 deletions

View file

@ -9,48 +9,82 @@ import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { Heading } from "@components/Heading";
import { DeleteIcon, PlusIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import definePlugin, { OptionType } from "@utils/types";
import { Button, TextInput } from "@webpack/common";
import { Button, Select, TextInput, useState } from "@webpack/common";
const cl = classNameFactory("vc-bbr-");
interface BanReason {
text: string;
deleteSeconds?: number;
}
const DELETE_DURATION_OPTIONS = [
{ label: "Discord default", value: null },
{ label: "Don't delete any", value: 0 },
{ label: "Previous hour", value: 3600 },
{ label: "Previous 6 hours", value: 21600 },
{ label: "Previous 12 hours", value: 43200 },
{ label: "Previous 24 hours", value: 86400 },
{ label: "Previous 3 days", value: 259200 },
{ label: "Previous 7 days", value: 604800 },
];
function normalizeReason(r: BanReason | string): BanReason {
return typeof r === "string" ? { text: r } : r;
}
function getStoredReasons(): BanReason[] {
return (settings.store.reasons as (BanReason | string)[]).map(normalizeReason);
}
function ReasonsComponent() {
const { reasons } = settings.store;
const [reasons, setReasons] = useState<BanReason[]>(getStoredReasons);
const update = (next: BanReason[]) => {
setReasons(next);
settings.store.reasons = next;
};
return (
<section>
<Heading>Reasons</Heading>
{reasons.map((r, i) => (
<div
key={i}
className={cl("reason-wrapper")}
>
<div key={i} className={cl("reason-wrapper")}>
<TextInput
value={r}
onChange={v => {
reasons[i] = v;
settings.store.reasons = reasons;
}}
value={r.text}
onChange={v => update(reasons.map((x, j) => j === i ? { ...x, text: v } : x))}
placeholder="Reason"
/>
<Button
className={cl("remove-button")}
color={Button.Colors.TRANSPARENT}
onClick={() => {
reasons.splice(i, 1);
settings.store.reasons = reasons;
}}
onClick={() => update(reasons.filter((_, j) => j !== i))}
look={Button.Looks.FILLED}
size={Button.Sizes.MIN}
>
<DeleteIcon />
</Button>
<div className={cl("duration-row")}>
<span className="vc-text-base vc-text-sm vc-text-normal vc-text-defaultColor vc-plugins-setting-description">Message Auto-delete duration</span>
<Select
options={DELETE_DURATION_OPTIONS}
select={v => update(reasons.map((x, j) => j === i ? { ...x, deleteSeconds: v === null ? undefined : v } : x))}
isSelected={v => (v ?? null) === (r.deleteSeconds ?? null)}
serialize={v => v == null ? "default" : String(v)}
/>
</div>
</div>
))}
<div className={cl("reason-wrapper")}>
<Button onClick={() => settings.store.reasons.push("")} className={cl("add-button")} size={Button.Sizes.LARGE} color={Button.Colors.TRANSPARENT}>
<Button
onClick={() => update([...reasons, { text: "" }])}
className={cl("add-button")}
size={Button.Sizes.LARGE}
color={Button.Colors.TRANSPARENT}
>
<PlusIcon /> Add another reason
</Button>
</div>
@ -62,7 +96,7 @@ const settings = definePluginSettings({
reasons: {
description: "Your custom reasons",
type: OptionType.COMPONENT,
default: [] as string[],
default: [] as BanReason[],
component: ReasonsComponent,
},
isTextInputDefault: {
@ -75,30 +109,56 @@ export default definePlugin({
name: "BetterBanReasons",
description: "Create custom reasons to use in the Discord ban modal, and/or show a text input by default instead of the options.",
tags: ["Appearance", "Customisation"],
authors: [Devs.Inbestigator],
authors: [Devs.Inbestigator, EquicordDevs.yonn2222],
durationSetter: null as ((v: number) => void) | null,
patches: [
{
find: "#{intl::BAN_REASON_OPTION_SPAM_ACCOUNT}",
replacement: [{
match: /(\[\{name:\i\.\i\.\i\(\i\.\i\.\i\),.+?"other"\}\])/,
replace: "$self.getReasons($1)"
},
{
match: /useState\(null\)(?=.{0,300}targetUserId:)/,
replace: "useState($self.getDefaultState())"
}]
replacement: [
{
match: /(\[\{name:\i\.\i\.\i\(\i\.\i\.\i\),.+?"other"\}\])/,
replace: "$self.getReasons($1)"
},
{
match: /useState\(null\)(?=.{0,300}targetUserId:)/,
replace: "useState($self.getDefaultState())"
},
{
match: /(\[\i,\i\])=(\i)\.useState\((null!=\i\?\i:\i)\)/,
replace: "$1=$self.captureDeleteState($2.useState,$3)"
},
{
match: /\i=\i\.useCallback\((\i)=>\{.{0,10},\i\(null\)(?=\},\[\]\))/,
replace: "$&,$self.onReasonSelect($1)"
}
]
}
],
getReasons(defaults) {
const storedReasons = settings.store.reasons.filter((r: string) => r.trim());
const reasons: string[] = storedReasons.length
? storedReasons
: [];
getReasons(defaults: { name: string; value: string; }[]) {
const stored = getStoredReasons().filter(r => r.text.trim());
return [
...reasons.map(s => ({ name: s, value: s })),
...defaults
...stored.map(r => ({ name: r.text, value: r.text })),
...defaults,
];
},
getDefaultState: () => settings.store.isTextInputDefault ? 1 : 0,
captureDeleteState(hook: (initial: number) => [number, (v: number) => void], initial: number) {
const result = hook(initial);
this.durationSetter = result[1];
return result;
},
onReasonSelect(value: string) {
const reason = getStoredReasons().find(r => r.text === value);
if (reason?.deleteSeconds !== undefined) {
this.durationSetter?.(reason.deleteSeconds);
}
},
settings,
});

View file

@ -8,6 +8,13 @@
.vc-bbr-remove-button {
color: var(--text-muted);
align-self: start;
}
.vc-bbr-duration-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.vc-bbr-remove-button:hover {

View file

@ -1296,6 +1296,10 @@ export const EquicordDevs = Object.freeze({
name: "DKA",
id: 119386840624005121n
},
yonn2222: {
name: "yonn2222",
id: 821835831844012103n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly