From 2d2a39387a7879e5faae0f82bd60b0fd55a1a5a2 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 22 Jul 2026 07:09:57 +0000 Subject: [PATCH] Preact: Redesign user moderation buttons Supersedes #2662 --- play.pokemonshowdown.com/src/client-main.ts | 6 +- .../src/panel-mainmenu.tsx | 2 +- play.pokemonshowdown.com/src/panel-popups.tsx | 313 +++++++----------- play.pokemonshowdown.com/style/client2.css | 25 +- 4 files changed, 146 insertions(+), 200 deletions(-) diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 0422020cb..c1f8aa8e6 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -2655,7 +2655,7 @@ export const PS = new class extends PSModel { otherButtons?: preact.ComponentChildren, parentElem?: HTMLElement, } = {}) { opts.cancelButton ??= 'Cancel'; - return new Promise(resolve => { + return new Promise(resolve => { this.join(`popup-${this.popups.length}` as RoomID, { args: { message, okValue: true, cancelValue: false, callback: resolve, ...opts, parentElem: null }, parentElem: opts.parentElem, @@ -2664,14 +2664,14 @@ export const PS = new class extends PSModel { } prompt(message: string, opts: { defaultValue?: string, okButton?: string, cancelButton?: string, type?: 'text' | 'password' | 'number' | 'numeric', - otherButtons?: preact.ComponentChildren, parentElem?: HTMLElement | null, + label?: string, otherButtons?: preact.ComponentChildren, parentElem?: HTMLElement | null, } = {}): Promise { opts.cancelButton ??= 'Cancel'; return new Promise(resolve => { this.join(`popup-${this.popups.length}` as RoomID, { args: { message, value: opts.defaultValue || '', - okValue: true, cancelValue: false, callback: resolve, ...opts, parentElem: null, + okValue: true, cancelValue: null, callback: resolve, ...opts, parentElem: null, }, parentElem: opts.parentElem, }); diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index f7ff7f5ae..ca1196dd4 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -1063,7 +1063,7 @@ export class TeamForm extends preact.Component<{ -

:
+

:
diff --git a/play.pokemonshowdown.com/src/panel-popups.tsx b/play.pokemonshowdown.com/src/panel-popups.tsx index 9fccd7767..f2f29ac7d 100644 --- a/play.pokemonshowdown.com/src/panel-popups.tsx +++ b/play.pokemonshowdown.com/src/panel-popups.tsx @@ -332,133 +332,68 @@ class UserPanel extends PSRoomPanel { } } +const USER_PUNISHMENTS = { + mute: { label: 'Mute', duration: '7 minutes', scope: 'room' }, + hourmute: { label: 'Hourmute', duration: '1 hour', scope: 'room' }, + ban: { label: 'Ban', duration: '2 days', scope: 'room' }, + weekban: { label: 'Weekban', duration: '1 week', scope: 'room' }, + lock: { label: 'Lock', duration: '2 days', scope: 'global' }, + weeklock: { label: 'Weeklock', duration: '1 week', scope: 'global' }, + namelock: { label: 'Namelock', duration: '2 days', scope: 'global' }, +} as const; +type UserPunishment = keyof typeof USER_PUNISHMENTS; + class UserOptionsPanel extends PSRoomPanel { static readonly id = 'useroptions'; static readonly routes = ['useroptions-*']; static readonly location = 'popup'; static readonly noURL = true; - declare state: { - showMuteInput?: boolean, - showBanInput?: boolean, - showLockInput?: boolean, - showConfirm?: boolean, - requestSent?: boolean, - data?: Record, - }; getTargets() { const [, targetUser, targetRoomid] = PSUtils.splitFirst(this.props.room.id, '-', 2); - let targetRoom = (PS.rooms[targetRoomid] || null) as ChatRoom | null; + let targetRoom = (targetRoomid ? PS.rooms[targetRoomid] : null) as ChatRoom | null || null; if (targetRoom?.type !== 'chat') targetRoom = targetRoom?.getParent() as ChatRoom; if (targetRoom?.type !== 'chat') targetRoom = targetRoom?.getParent() as ChatRoom; if (targetRoom?.type !== 'chat') targetRoom = null; + if (targetRoom?.pmTarget) targetRoom = null; return { targetUser: targetUser as ID, targetRoomid: targetRoomid as RoomID, targetRoom }; } - handleMute = (ev: Event) => { - this.setState({ showMuteInput: true, showBanInput: false, showLockInput: false }); + handleCommand = (ev: Event) => { ev.preventDefault(); ev.stopImmediatePropagation(); + const command = (ev.currentTarget as HTMLButtonElement).getAttribute('data-cmdpreview'); + if (!command) return PS.alert("Room not found", { parentElem: ev.currentTarget as HTMLElement }); + this.send(command); }; - handleBan = (ev: Event) => { - this.setState({ showBanInput: true, showMuteInput: false, showLockInput: false }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - handleLock = (ev: Event) => { - this.setState({ showLockInput: true, showMuteInput: false, showBanInput: false }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - - handleCancel = (ev: Event) => { - this.setState({ showBanInput: false, showMuteInput: false, showLockInput: false, showConfirm: false }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - - handleConfirm = (ev: Event) => { - const data = this.state.data; - if (!data) return; + send(command: string) { const { targetUser, targetRoom } = this.getTargets(); - - let cmd = ''; - if (data.action === "Mute") { - cmd += data.duration === "1 hour" ? "/hourmute " : "/mute "; - } else if (data.action === "Ban") { - cmd += data.duration === "1 week" ? "/weekban " : "/ban "; - } else if (data.action === "Lock") { - cmd += data.duration === "1 week" ? "/weeklock " : "/lock "; - } else if (data.action === "Namelock") { - cmd += "/namelock "; + if (targetRoom) { + targetRoom.send(command); } else { - return; + PS.join(`dm-${targetUser}` as RoomID); + PS.rooms[`dm-${targetUser}`]!.send(command); } - cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`; - targetRoom?.send(cmd); this.close(); - }; + } + + openPunishment = (ev: Event) => { + const button = ev.currentTarget as HTMLButtonElement; + const punishment = button.value as UserPunishment; + ev.preventDefault(); + ev.stopImmediatePropagation(); + if (!(punishment in USER_PUNISHMENTS)) return; - handleAddFriend = (ev: Event) => { const { targetUser, targetRoom } = this.getTargets(); - targetRoom?.send(`/friend add ${targetUser}`); - this.setState({ requestSent: true }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - - handleIgnore = () => { - const { targetUser, targetRoom } = this.getTargets(); - targetRoom?.send(`/ignore ${targetUser}`); - this.close(); - }; - - handleUnignore = () => { - const { targetUser, targetRoom } = this.getTargets(); - targetRoom?.send(`/unignore ${targetUser}`); - this.close(); - }; - - muteUser = (ev: Event) => { - this.setState({ showMuteInput: false }); - const hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr"; - const reason = this.base?.querySelector("input[name=mutereason]")?.value; - const data = { - action: 'Mute', - reason, - duration: hrMute ? "1 hour" : "7 minutes", - }; - this.setState({ data, showConfirm: true }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - - banUser = (ev: Event) => { - this.setState({ showBanInput: false }); - const weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk"; - const reason = this.base?.querySelector("input[name=banreason]")?.value; - const data = { - action: 'Ban', - reason, - duration: weekBan ? "1 week" : "2 days", - }; - this.setState({ data, showConfirm: true }); - ev.preventDefault(); - ev.stopImmediatePropagation(); - }; - - lockUser = (ev: Event) => { - this.setState({ showLockInput: false }); - const weekLock = (ev.currentTarget as HTMLButtonElement).value === "1wk"; - const isNamelock = (ev.currentTarget as HTMLButtonElement).value === "nmlk"; - const reason = this.base?.querySelector("input[name=lockreason]")?.value; - const data = { - action: isNamelock ? 'Namelock' : 'Lock', - reason, - duration: weekLock ? "1 week" : "2 days", - }; - this.setState({ data, showConfirm: true }); - ev.preventDefault(); - ev.stopImmediatePropagation(); + const action = USER_PUNISHMENTS[punishment]; + const scope = action.scope === 'room' ? ` in ${targetRoom?.title || '???'}` : ''; + PS.prompt( + `${action.label} ${targetUser}${scope} for ${action.duration}?`, + { okButton: action.label, parentElem: button, label: "Reason (optional):" } + ).then(reason => { + if (reason === null) return; + reason = reason.trim(); + this.send(`/${punishment} ${targetUser}${reason ? `, ${reason}` : ''}`); + }); }; isIgnoringUser = (userid: string) => { @@ -469,33 +404,27 @@ class UserOptionsPanel extends PSRoomPanel { override render() { const room = this.props.room; - const banPerms = ["@", "#", "~"]; - const mutePerms = ["%", ...banPerms]; const { targetUser, targetRoom } = this.getTargets(); - const userRoomGroup = targetRoom?.users[PS.user.userid].charAt(0) || ''; - const canMute = mutePerms.includes(userRoomGroup); - const canBan = banPerms.includes(userRoomGroup); - const canLock = mutePerms.includes(PS.user.group); - const isVisible = (actionName: string) => { - if (actionName === 'mute') { - return canMute && !this.state.showLockInput && !this.state.showBanInput && !this.state.showConfirm; - } - if (actionName === 'ban') { - return canBan && !this.state.showLockInput && !this.state.showMuteInput && !this.state.showConfirm; - } - if (actionName === 'lock') { - return canLock && !this.state.showBanInput && !this.state.showMuteInput && !this.state.showConfirm; - } - }; + const userRoomGroup = targetRoom?.users[PS.user.userid]?.charAt(0); + const roomGroupOrder = PS.server.getGroup(userRoomGroup).order; + const globalGroupOrder = PS.server.getGroup(PS.user.group).order; + const canMute = !!targetRoom && roomGroupOrder <= PS.server.getGroup('%').order; + const canBan = !!targetRoom && roomGroupOrder <= PS.server.getGroup('@').order; + const canLock = globalGroupOrder <= PS.server.getGroup('%').order; - return
+ return
+

+ +

{this.isIgnoringUser(targetUser) ? ( - ) : ( - )} @@ -505,80 +434,76 @@ class UserOptionsPanel extends PSRoomPanel { Report

-

- {this.state.requestSent ? ( - - ) : ( - - )} -

- {(canMute || canBan || canLock) &&
} - {this.state.showConfirm &&

- - {this.state.data?.action} {targetUser} {} - {!this.state.data?.action.endsWith('ock') ? <>from {targetRoom?.title} : ''} for {this.state.data?.duration}? - -

- {} - + +

} +

+

-

} -

- {isVisible('mute') && (this.state.showMuteInput ? ( -

- {}
- {} - {} - -
- ) : ( -
} + {canLock &&
+ Global moderation +

+ - ))} {} - {isVisible('ban') && (this.state.showBanInput ? ( -

-
- {} - {} - -
- ) : ( - - ))} {} - {isVisible('lock') && (this.state.showLockInput ? ( -
-
- {} - {} - {} - -
- ) : ( - - ))} -

+

+

+ +

+
} ; } } @@ -1934,9 +1859,9 @@ class PopupPanel extends PSRoomPanel { style="white-space:pre-wrap;word-wrap:break-word" dangerouslySetInnerHTML={{ __html: this.parseMessage(message as string || '') }} >

} - {!!type &&

} + />

}