From 1572b07cb4e1af8966aee3a2e9fad264ffe92584 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Sun, 6 Apr 2025 09:01:43 -0700 Subject: [PATCH] Preact: Misc fixes - Don't clear PM contents when rearranging - Don't show "Chat self" on others' usercards while not logged in - Direct usercard URLs to Preact client - Fix mini window collapse/expand - Fix dark mode mini window and focused mini window styles - Set titles for more readable history - Stub PS.alert / PS.prompt - Document RoomOptions - Infer parentRoomid from parentElem - Fix room focus issues - Fix browser back/forward history - Keep default format up to date --- play.pokemonshowdown.com/src/client-main.ts | 56 +++++++++++++---- play.pokemonshowdown.com/src/panel-battle.tsx | 1 + .../src/panel-mainmenu.tsx | 38 ++++++------ .../src/panel-teambuilder.tsx | 2 +- play.pokemonshowdown.com/src/panel-topbar.tsx | 7 ++- play.pokemonshowdown.com/src/panels.tsx | 52 +++++++++------- play.pokemonshowdown.com/style/client2.css | 60 +++++++++++++------ 7 files changed, 144 insertions(+), 72 deletions(-) diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 2606c1e7c..d998b46a5 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -134,7 +134,7 @@ class PSPrefs extends PSStreamModel { } } else if (isChrome64) { newPrefs['nogif'] = true; - alert('Your version of Chrome has a bug that makes animated GIFs freeze games sometimes, so certain animations have been disabled. Only some people have the problem, so you can experiment and enable them in the Options menu setting "Disable GIFs for Chrome 64 bug".'); + PS.alert('Your version of Chrome has a bug that makes animated GIFs freeze games sometimes, so certain animations have been disabled. Only some people have the problem, so you can experiment and enable them in the Options menu setting "Disable GIFs for Chrome 64 bug".'); } const colorSchemeQuerySupported = window.matchMedia?.('(prefers-color-scheme: dark)').media !== 'not all'; @@ -241,7 +241,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> { this.byKey[team.key] = team; } unpackOldBuffer(buffer: string) { - alert(`Your team storage format is too old for PS. You'll need to upgrade it at https://${Config.routes.client}/recoverteams.html`); + PS.alert(`Your team storage format is too old for PS. You'll need to upgrade it at https://${Config.routes.client}/recoverteams.html`); this.list = []; } packAll(teams: Team[]) { @@ -431,7 +431,7 @@ class PSUser extends PSStreamModel { PS.send('|/logout'); PS.connection?.disconnect(); - alert("You have been logged out and disconnected.\n\nIf you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command."); + PS.alert("You have been logged out and disconnected.\n\nIf you wanted to change your name while staying connected, use the 'Change Name' button or the '/nick' command."); this.name = ""; this.group = ''; this.userid = "" as ID; @@ -539,12 +539,24 @@ type PSRoomLocation = 'left' | 'right' | 'popup' | 'mini-window' | 'modal-popup' export interface RoomOptions { id: RoomID; title?: string; + /** @see {PS.roomTypes} */ type?: string; location?: PSRoomLocation | null; - /** Handled after initialization, outside of the constructor */ + /** Fed to `room.receiveLine` after initialization, outside of the constructor */ queue?: Args[]; + /** + * Popup parent element. If it exists, a popup shows up right above/below that element. + * + * No effect on non-popup panels. + */ parentElem?: HTMLElement | null; + /** + * Popup's parent room. Inferred from `parentElem`. Closes any popup that isn't this popup. + * + * No effect on non-popup panels. + */ parentRoomid?: RoomID | null; + /** Opens the popup to the right of its parent, instead of the default above/below (for userlists) */ rightPopup?: boolean; connected?: boolean; [k: string]: unknown; @@ -667,6 +679,9 @@ export class PSRoom extends PSStreamModel implements RoomOptions { } } } + /** + * Handles OUTGOING messages, like `/logout`. + */ handleMessage(line: string) { if (!line.startsWith('/') || line.startsWith('//')) return false; const spaceIndex = line.indexOf(' '); @@ -950,14 +965,18 @@ export const PS = new class extends PSModel { if (!alreadyUpdating) this.update(true); } } - getRoom(elem: HTMLElement) { - let curElem: HTMLElement | null = elem; + getRoom(elem: HTMLElement | EventTarget | null | undefined): PSRoom | null { + let curElem: HTMLElement | null = elem as HTMLElement; while (curElem) { if (curElem.id.startsWith('room-')) { - return PS.rooms[curElem.id.slice(5)]; + return PS.rooms[curElem.id.slice(5)] || null; + } + if (curElem.getAttribute('data-roomid')) { + return PS.rooms[curElem.getAttribute('data-roomid') as RoomID] || null; } curElem = curElem.parentElement; } + return null; } dragOnto(fromRoom: PSRoom, toLocation: 'left' | 'right' | 'mini-window', toIndex: number) { // one day you will be able to rearrange mainmenu and rooms, but not today @@ -1039,7 +1058,7 @@ export const PS = new class extends PSModel { const msg = fullMsg.slice(pipeIndex + 1); console.log('\u25b6\ufe0f ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #776677"); if (!this.connection) { - alert(`You are not connected and cannot send ${msg}.`); + PS.alert(`You are not connected and cannot send ${msg}.`); return; } this.connection.send(fullMsg); @@ -1165,6 +1184,9 @@ export const PS = new class extends PSModel { const room = this.rooms[roomid]; if (!room) return false; if (this.room === room) return true; + while (this.popups.length && PS.room !== room) { + this.leave(this.popups.pop()!); + } if (room.location === 'left') { this.leftPanel = this.panel = room; while (this.popups.length) this.leave(this.popups.pop()!); @@ -1176,10 +1198,6 @@ export const PS = new class extends PSModel { } else { // popup or mini-window if (room.location === 'mini-window') { this.leftPanel = this.panel = PS.mainmenu; - } else { - while (this.popups.length && this.popups[this.popups.length - 1] !== roomid) { - this.leave(this.popups.pop()!); - } } this.room = room; } @@ -1230,6 +1248,17 @@ export const PS = new class extends PSModel { } return buf; } + alert(message: string) { + alert(message); + } + prompt(message: string, defaultValue?: string, opts?: { + okButton?: string, type?: 'text' | 'password' | 'number', + }): Promise { + return new Promise(resolve => { + const input = prompt(message, defaultValue); + resolve(input); + }); + } getPMRoom(userid: ID): ChatRoom { const myUserid = PS.user.userid; const roomid = `pm-${[userid, myUserid].sort().join('-')}` as RoomID; @@ -1255,6 +1284,7 @@ export const PS = new class extends PSModel { } } + options.parentRoomid ??= this.getRoom(options.parentElem)?.id; if (this.rooms[options.id]) { for (let i = 0; i < this.popups.length; i++) { const popup = this.rooms[this.popups[i]]!; @@ -1387,8 +1417,8 @@ export const PS = new class extends PSModel { if (location === 'left') this.leftPanel = this.panel = room; if (location === 'right') this.rightPanel = this.panel = room; if (location === 'mini-window') this.leftPanel = this.panel = this.mainmenu; + this.room = room; } - this.room = room; } removeRoom(room: PSRoom) { room.destroy(); diff --git a/play.pokemonshowdown.com/src/panel-battle.tsx b/play.pokemonshowdown.com/src/panel-battle.tsx index fcac4b47d..2fe33a09d 100644 --- a/play.pokemonshowdown.com/src/panel-battle.tsx +++ b/play.pokemonshowdown.com/src/panel-battle.tsx @@ -55,6 +55,7 @@ class BattlesPanel extends PSRoomPanel { static readonly Model = BattlesRoom; static readonly location = 'right'; static readonly icon = ; + static readonly title = 'Battles'; refresh = () => { this.props.room.refresh(); }; diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 93a74fb41..b244d9fba 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -74,7 +74,7 @@ export class MainMenuRoom extends PSRoom { return; } case 'popup': { const [, message] = args; - alert(message.replace(/\|\|/g, '\n')); + PS.alert(message.replace(/\|\|/g, '\n')); return; } } @@ -321,12 +321,13 @@ class MainMenuPanel extends PSRoomPanel { this.base!.querySelector('button.big')!.focus(); } submit = (e: Event) => { - alert('todo: implement'); + PS.alert('todo: implement'); }; handleDragStart = (e: DragEvent) => { - const roomid = (e.currentTarget as HTMLElement).getAttribute('data-roomid') as RoomID; - const foreground = (PS.leftPanel.id === roomid || PS.rightPanel?.id === roomid); - PS.dragging = { type: 'room', roomid, foreground }; + const room = PS.getRoom(e.currentTarget); + if (!room) return; + const foreground = (PS.leftPanel.id === room.id || PS.rightPanel?.id === room.id); + PS.dragging = { type: 'room', roomid: room.id, foreground }; }; handleDragEnter = (e: DragEvent) => { // console.log('dragenter ' + e.dataTransfer!.dropEffect); @@ -361,7 +362,7 @@ class MainMenuPanel extends PSRoomPanel { if (((e.target as any)?.parentNode as HTMLInputElement)?.name === 'closeRoom') { return; } - const room = PS.rooms[(e.currentTarget as any).getAttribute('data-roomid') as RoomID]; + const room = PS.getRoom(e.currentTarget); if (room) { room.minimized = !room.minimized; this.forceUpdate(); @@ -370,17 +371,18 @@ class MainMenuPanel extends PSRoomPanel { renderMiniRooms() { return PS.miniRoomList.map(roomid => { const room = PS.rooms[roomid]!; - return
-
-

- - - {room.title} -

- {this.renderMiniRoom(room)} -
+ return
+

+ + + {room.title} +

+ {this.renderMiniRoom(room)}
; }); } @@ -465,7 +467,7 @@ class MainMenuPanel extends PSRoomPanel { export class FormatDropdown extends preact.Component<{ format?: string, onChange?: JSX.EventHandler }> { declare base?: HTMLButtonElement; - format = '[Gen 7] Random Battle'; + format = `[Gen ${Dex.gen}] Random Battle`; change = (e: Event) => { if (!this.base) return; this.format = this.base.value; diff --git a/play.pokemonshowdown.com/src/panel-teambuilder.tsx b/play.pokemonshowdown.com/src/panel-teambuilder.tsx index 36ecdde03..7499d1a68 100644 --- a/play.pokemonshowdown.com/src/panel-teambuilder.tsx +++ b/play.pokemonshowdown.com/src/panel-teambuilder.tsx @@ -53,7 +53,7 @@ class TeambuilderRoom extends PSRoom { } // unrecognized command - alert(`Unrecognized command: ${line}`); + PS.alert(`Unrecognized command: ${line}`); return true; } diff --git a/play.pokemonshowdown.com/src/panel-topbar.tsx b/play.pokemonshowdown.com/src/panel-topbar.tsx index 8fed0391f..4ff184a43 100644 --- a/play.pokemonshowdown.com/src/panel-topbar.tsx +++ b/play.pokemonshowdown.com/src/panel-topbar.tsx @@ -305,11 +305,16 @@ class UserPanel extends PSRoomPanel { const buttonbar = []; if (!hideInteraction) { - buttonbar.push(isSelf || !PS.user.named ? ( + buttonbar.push(isSelf ? (

{}

+ ) : !PS.user.named ? ( +

+ {} + +

) : (

{} diff --git a/play.pokemonshowdown.com/src/panels.tsx b/play.pokemonshowdown.com/src/panels.tsx index 787388068..18fc1c471 100644 --- a/play.pokemonshowdown.com/src/panels.tsx +++ b/play.pokemonshowdown.com/src/panels.tsx @@ -57,6 +57,28 @@ export class PSRouter { return url as RoomID; } + updatePanelState(): { roomid: RoomID, changed: boolean } { + const room = PS.room; + let roomid = room.id; + const panelState = (PS.leftPanelWidth && PS.room === PS.panel ? + PS.leftPanel.id + '..' + PS.rightPanel!.id : + PS.room.id); + // don't generate history when focusing things on the home page + if (roomid === 'news' && PS.room.location === 'mini-window') roomid = '' as RoomID; + if (roomid === 'rooms') roomid = '' as RoomID; + if (roomid === this.roomid && panelState === this.panelState) { + return { roomid, changed: false }; + } + + if (roomid === '' || roomid === 'rooms') { + document.title = 'Showdown!'; + } else { + document.title = room.title + ' - Showdown!'; + } + this.roomid = roomid; + this.panelState = panelState; + return { roomid, changed: true }; + } subscribeHash() { if (location.hash) { const currentRoomid = location.hash.slice(1); @@ -67,8 +89,8 @@ export class PSRouter { } } PS.subscribeAndRun(() => { - const roomid = PS.room.id; - location.hash = roomid ? '#' + roomid : ''; + const { roomid, changed } = this.updatePanelState(); + if (changed) location.hash = roomid ? `#${roomid}` : ''; }); window.addEventListener('hashchange', e => { const possibleRoomid = location.hash.slice(1); @@ -90,21 +112,12 @@ export class PSRouter { } if (!window.history) return; PS.subscribeAndRun(() => { - const room = PS.room; - const roomid = room.id; - const panelState = (PS.leftPanelWidth ? - PS.leftPanel.id + '..' + PS.rightPanel!.id : - roomid); - if (roomid === this.roomid && panelState === this.panelState) { - return; - } - if (panelState === this.panelState) { - history.pushState(panelState, room.title, '/' + roomid); + const { roomid, changed } = this.updatePanelState(); + if (changed) { + history.pushState(this.panelState, '', `/${roomid}`); } else { - history.replaceState(panelState, room.title, '/' + roomid); + history.replaceState(this.panelState, '', `/${roomid}`); } - this.roomid = roomid; - this.panelState = panelState; }); window.addEventListener('popstate', e => { const possibleRoomid = location.pathname.slice(1); @@ -114,9 +127,11 @@ export class PSRouter { } if (typeof e.state === 'string') { const [leftRoomid, rightRoomid] = e.state.split('..') as RoomID[]; - PS.join(leftRoomid, 'left'); if (rightRoomid) { + PS.join(leftRoomid, 'left'); PS.join(rightRoomid, 'right'); + } else { + PS.join(leftRoomid); } } if (roomid !== null) { @@ -187,13 +202,11 @@ export function PSPanelWrapper(props: { }) { const room = props.room; if (room.location === 'mini-window') { - const minimized = room.minimized ? { display: 'none' } : {}; if (room.id === 'news') { - return

{props.children}
; + return
{props.children}
; } return
{props.children}
; @@ -236,7 +249,6 @@ export class PSMain extends preact.Component { PS.addRoom({ id: roomid, parentElem: elem, - parentRoomid: PSMain.containingRoomid(elem), rightPopup: elem.className === 'userbutton username', username: name, }); diff --git a/play.pokemonshowdown.com/style/client2.css b/play.pokemonshowdown.com/style/client2.css index 87020ef20..a96bccfab 100644 --- a/play.pokemonshowdown.com/style/client2.css +++ b/play.pokemonshowdown.com/style/client2.css @@ -274,24 +274,24 @@ span.header-username:hover { color: #333333; } .minimizebutton:hover, -.pm-window h3:hover .minimizebutton { +.mini-window h3:hover .minimizebutton { color: #333333; } .dark .minimizebutton:hover, -.dark .pm-window h3:hover .minimizebutton { +.dark .mini-window h3:hover .minimizebutton { color: #CCCCCC; } -.pm-window h3 .closebutton:hover + .minimizebutton { +.mini-window h3 .closebutton:hover + .minimizebutton { color: #999999 !important; } -.pm-window h3 span.header-username:hover + .minimizebutton { +.mini-window h3 span.header-username:hover + .minimizebutton { color: #999999 !important; } .closebutton:active, .minimizebutton:active { color: #661111; } -.pm-minimized .minimizebutton .fa-minus-circle:before { +.collapsed .minimizebutton .fa-minus-circle:before { /** replace the minus with a plus when PM is minimized */ content: "\f055"; } @@ -501,6 +501,7 @@ p.or:after { position: absolute; left: 325px; top: 40px; + min-height: 2px; } .mainmenu { padding: 0 0 51px 0; @@ -545,13 +546,9 @@ p.or:after { color: #CCDDFF; } -.pmbox { - width: 270px; - margin: 0 auto; - min-height: 2px; -} .mini-window { margin: 0 -12px 12px -10px; + width: 292px; } .tiny-layout .mini-window { margin: 0 0 12px 0; @@ -570,21 +567,28 @@ p.or:after { .mini-window h3:hover { color: #333333; } -.mini-window h3.pm-minimized { +.mini-window.collapsed h3 { border-radius: 5px; } .mini-window h3.pm-notifying { border-color: #AA8866; background: #E3C3A3; } +.dark .mini-window h3.pm-notifying { + background: #417589; + color: #BBBBBB; +} .mini-window h3.pm-notifying:hover { border-color: #604020; background: #CFAF8F; } +.dark .mini-window h3.pm-notifying:hover { + background: #417589; +} .mini-window h3 .closebutton, .mini-window h3 .minimizebutton { float: right; - margin: -3px -3px; + margin: -2px -3px; width: 22px; height: 22px; } @@ -618,6 +622,10 @@ p.or:after { border-bottom: 0; overflow: auto; -webkit-overflow-scrolling: touch; + word-wrap: break-word; +} +.pm-buttonbar { + padding-bottom: 3px; } .pm-log-add { color: black; @@ -626,13 +634,13 @@ p.or:after { border-bottom-right-radius: 5px; background: rgba(242,247,250,.85); } -.mini-window.collapsed .pm-log, -.mini-window.collapsed .pm-log-add { +.mini-window.collapsed .mini-window-contents, +.mini-window.collapsed .mini-window-body { display: none; } .mini-window.focused h3, -.mini-window.focused .pm-log, -.mini-window.focused .pm-log-add { +.mini-window.focused .mini-window-contents, +.mini-window.focused .mini-window-body { border-color: #555555; } .mini-window.focused h3, @@ -640,6 +648,19 @@ p.or:after { background: #f8f8f8; color: #222222; } +.dark .mini-window h3 { + background: rgba(50,50,50,.8); + color: #AAA; +} +.dark .mini-window h3:hover { + color: #CCC; +} +.dark .mini-window.focused h3, +.dark .mini-window.focused h3:hover { + background: #222; + color: #EEE; +} + .challenge { margin: 0 5px; background: #fcd2b3; @@ -2008,6 +2029,7 @@ a.ilink.yours { } .dark .ps-room.ps-room-light, +.dark .ps-room-light, .dark .tournament-box, .dark .tournament-status, .dark .tournament-toggle, @@ -2016,7 +2038,7 @@ a.ilink.yours { color: #DDD; } -.dark .pm-log { +.dark .pm-log, .dark .mini-window-body { background: rgba(0,0,0,.85); color: #DDD; } @@ -2121,8 +2143,8 @@ a.ilink.yours { background: rgba(0,0,0,.2); } .dark .mini-window.focused h3, -.dark .mini-window.focused .pm-log, -.dark .mini-window.focused .pm-log-add { +.dark .mini-window.focused .mini-window-body, +.dark .mini-window.focused .mini-window-contents { border-color: #CCC; }