From 33f78c413f6e67e424897023777300d084aab1bc Mon Sep 17 00:00:00 2001 From: Aurastic <33085835+ISenseAura@users.noreply.github.com> Date: Wed, 7 May 2025 14:40:03 +0530 Subject: [PATCH] Preact: Add highlight, receivepopup etc (#2406) --- play.pokemonshowdown.com/src/battle-log.ts | 6 +- play.pokemonshowdown.com/src/client-main.ts | 112 +++++++++++++++++++- play.pokemonshowdown.com/src/panel-chat.tsx | 87 +++++++++++---- 3 files changed, 181 insertions(+), 24 deletions(-) diff --git a/play.pokemonshowdown.com/src/battle-log.ts b/play.pokemonshowdown.com/src/battle-log.ts index d8d0bd523..d835341a9 100644 --- a/play.pokemonshowdown.com/src/battle-log.ts +++ b/play.pokemonshowdown.com/src/battle-log.ts @@ -53,7 +53,7 @@ export class BattleLog { * * 1 = player 2: "Red sent out Pikachu!" "Eevee used Tackle!" */ perspective: -1 | 0 | 1 = -1; - getHighlight: ((message: string, name: string) => boolean) | null = null; + getHighlight: ((line: Args) => boolean) | null = null; constructor(elem: HTMLDivElement, scene?: BattleScene | null, innerElem?: HTMLDivElement) { this.elem = elem; @@ -177,7 +177,7 @@ export class BattleLog { } timestampHtml = `[${components.map(x => x < 10 ? `0${x}` : x).join(':')}] `; } - const isHighlighted = window.app?.rooms?.[battle!.roomid].getHighlight(message) || this.getHighlight?.(message, name); + const isHighlighted = window.app?.rooms?.[battle!.roomid].getHighlight(message) || this.getHighlight?.(args); [divClass, divHTML, noNotify] = this.parseChatMessage(message, name, timestampHtml, isHighlighted); if (!noNotify && isHighlighted) { const notifyTitle = "Mentioned by " + name + " in " + (battle?.roomid || ''); @@ -266,6 +266,7 @@ export class BattleLog { case 'unlink': { // |unlink| is deprecated in favor of |hidelines| + if (window.PS.prefs.nounlink) return; const user = toID(args[2]) || toID(args[1]); this.unlinkChatFrom(user); if (args[2]) { @@ -276,6 +277,7 @@ export class BattleLog { } case 'hidelines': { + if (window.PS.prefs.nounlink) return; const user = toID(args[2]); this.unlinkChatFrom(user); if (args[1] !== 'unlink') { diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 6ec696f38..729ec4060 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -12,7 +12,7 @@ import { PSConnection, PSLoginServer } from './client-connection'; import { PSModel, PSStreamModel } from './client-core'; import type { PSRoomPanel, PSRouter } from './panels'; -import type { ChatRoom } from './panel-chat'; +import { ChatRoom } from './panel-chat'; import type { MainMenuRoom } from './panel-mainmenu'; import { Dex, toID, type ID } from './battle-dex'; import { BattleTextParser, type Args } from './battle-text-parser'; @@ -78,6 +78,7 @@ class PSPrefs extends PSStreamModel { hidelinks: false, hideinterstice: true, }; + nounlink: boolean | null = null; /* Battle preferences */ ignorenicks: boolean | null = null; @@ -118,6 +119,9 @@ class PSPrefs extends PSStreamModel { afd: boolean | 'sprites' = false; + highlights: Record | null = null; + logtimes: Record | null = null; + // PREFS END HERE storageEngine: 'localStorage' | 'iframeLocalStorage' | '' = ''; @@ -830,6 +834,14 @@ export class PSRoom extends PSStreamModel implements RoomOptions { PS.update(); } autoDismissNotifications() { + let room = PS.rooms[this.id] as ChatRoom; + if (room.lastMessageTime) { + // Mark chat messages as read to avoid double-notifying on reload + let lastMessageDates = PS.prefs.logtimes || {}; + if (!lastMessageDates[PS.server.id]) lastMessageDates[PS.server.id] = {}; + lastMessageDates[PS.server.id][room.id] = room.lastMessageTime || 0; + PS.prefs.set('logtimes', lastMessageDates); + } this.notifications = this.notifications.filter(notification => notification.noAutoDismiss); this.isSubtleNotifying = false; } @@ -926,6 +938,9 @@ export class PSRoom extends PSStreamModel implements RoomOptions { this.send(target); PS.leave(this.id); }, + 'receivepopup'(target) { + PS.alert(target); + }, 'inopener,inparent'(target) { // do this command in the popup opener let room = this.getParent(); @@ -1187,10 +1202,103 @@ export class PSRoom extends PSStreamModel implements RoomOptions { } this.add("||All PM windows cleared and closed."); }, + 'unpackhidden'() { + PS.prefs.set('nounlink', true); + this.add('||Locked/banned users\' chat messages: ON'); + }, + 'packhidden'() { + PS.prefs.set('nounlink', false); + this.add('||Locked/banned users\' chat messages: HIDDEN'); + }, + 'hl,highlight'(target) { + let highlights = PS.prefs.highlights || {}; + if (target.includes(' ')) { + let targets = target.split(' '); + let subCmd = targets[0]; + targets = targets.slice(1).join(' ').match(/([^,]+?({\d*,\d*})?)+/g) as string[]; + // trim the targets to be safe + for (let i = 0, len = targets.length; i < len; i++) { + targets[i] = targets[i].replace(/\n/g, '').trim(); + } + switch (subCmd) { + case 'add': case 'roomadd': { + let key = subCmd === 'roomadd' ? (PS.server.id + '#' + this.id) : 'global'; + let highlightList = highlights[key] || []; + for (let i = 0, len = targets.length; i < len; i++) { + if (!targets[i]) continue; + if (/[\\^$*+?()|{}[\]]/.test(targets[i])) { + // Catch any errors thrown by newly added regular expressions so they don't break the entire highlight list + try { + new RegExp(targets[i]); + } catch (e: any) { + return this.add(`|error|${(e.message.substr(0, 28) === 'Invalid regular expression: ' ? e.message : 'Invalid regular expression: /' + targets[i] + '/: ' + e.message)}`); + } + } + if (highlightList.includes(targets[i])) { + return this.add(`|error|${targets[i]} is already on your highlights list.`); + } + } + highlights[key] = highlightList.concat(targets); + this.add(`||Now highlighting on ${(key === 'global' ? "(everywhere): " : "(in " + key + "): ")} ${highlights[key].join(', ')}`); + // We update the regex + ChatRoom.updateHighlightRegExp(highlights); + break; + } + case 'delete': case 'roomdelete': { + let key = subCmd === 'roomdelete' ? (PS.server.id + '#' + this.id) : 'global'; + let highlightList = highlights[key] || []; + let newHls: string[] = []; + for (let i = 0, len = highlightList.length; i < len; i++) { + if (!targets.includes(highlightList[i])) { + newHls.push(highlightList[i]); + } + } + highlights[key] = newHls; + this.add(`||Now highlighting on ${(key === 'global' ? "(everywhere): " : "(in " + key + "): ")} ${highlights[key].join(', ')}`); + // We update the regex + ChatRoom.updateHighlightRegExp(highlights); + break; + } + default: + // Wrong command + this.add('|error|Invalid /highlight command.'); + this.handleSend('/help highlight'); // show help + return false; + } + PS.prefs.set('highlights', highlights); + } else { + if (['clear', 'roomclear', 'clearall'].includes(target)) { + let key = (target === 'roomclear' ? (PS.server.id + '#' + this.id) : (target === 'clearall' ? '' : 'global')); + if (key) { + highlights[key] = []; + this.add(`||All highlights (${(key === 'global' ? "everywhere" : "in " + key)}) cleared.`); + ChatRoom.updateHighlightRegExp(highlights); + } else { + PS.prefs.set('highlights', null); + this.add("||All highlights (in all rooms and globally) cleared."); + ChatRoom.updateHighlightRegExp({}); + } + } else if (['show', 'list', 'roomshow', 'roomlist'].includes(target)) { + // Shows a list of the current highlighting words + let key = target.startsWith('room') ? (PS.server.id + '#' + this.id) : 'global'; + if (highlights[key] && highlights[key].length > 0) { + this.add(`||Current highlight list ${(key === 'global' ? "(everywhere): " : "(in " + key + "): ")}${highlights[key].join(", ")}`); + } else { + this.add(`||Your highlight list${(key === 'global' ? '' : ' in ' + key)} is empty.`); + } + } else { + // Wrong command + this.add('|error|Invalid /highlight command.'); + this.handleSend('/help highlight'); // show help + return false; + } + } + return false; + }, 'senddirect'(target) { this.sendDirect(target); }, - 'help'(target) { + 'h,help'(target) { switch (toID(target)) { case 'chal': case 'chall': diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx index cbb38bdf3..99cac3f6f 100644 --- a/play.pokemonshowdown.com/src/panel-chat.tsx +++ b/play.pokemonshowdown.com/src/panel-chat.tsx @@ -47,11 +47,13 @@ export class ChatRoom extends PSRoom { log: BattleLog | null = null; tour: ChatTournament | null = null; lastMessage: Args | null = null; + lastMessageTime: number | null = null; joinLeave: { join: string[], leave: string[], messageId: string } | null = null; /** in order from least to most recent */ userActivity: string[] = []; timeOffset = 0; + static highlightRegExp: Record | null = null; constructor(options: RoomOptions) { super(options); @@ -179,21 +181,20 @@ export class ChatRoom extends PSRoom { this.title = `[DM] ${nameWithGroup.trim()}`; } } - handleHighlight = (message: string, name: string) => { - if (!PS.prefs.noselfhighlight && PS.user.nameRegExp?.test(message)) { - this.notify({ - title: `Mentioned by ${name} in ${this.id}`, - body: `"${message}"`, - id: 'highlight', - }); - return true; + static getHighlight(message: string, roomid: string) { + let highlights = PS.prefs.highlights || {}; + if (Array.isArray(highlights)) { + highlights = { global: highlights }; + // Migrate from the old highlight system + PS.prefs.set('highlights', highlights); + } + if (!PS.prefs.noselfhighlight && PS.user.nameRegExp) { + if (PS.user.nameRegExp?.test(message)) return true; } - /* - // TODO! if (!this.highlightRegExp) { try { - //this.updateHighlightRegExp(highlights); - } catch (e) { + this.updateHighlightRegExp(highlights); + } catch { // If the expression above is not a regexp, we'll get here. // Don't throw an exception because that would prevent the chat // message from showing up, or, when the lobby is initialising, @@ -201,14 +202,60 @@ export class ChatRoom extends PSRoom { return false; } } - var id = PS.server.id + '#' + this.id; - var globalHighlightsRegExp = this.highlightRegExp['global']; - var roomHighlightsRegExp = this.highlightRegExp[id]; - - return (((globalHighlightsRegExp && - globalHighlightsRegExp.test(message)) || - (roomHighlightsRegExp && roomHighlightsRegExp.test(message)))); - */ + const id = PS.server.id + '#' + roomid; + const globalHighlightsRegExp = this.highlightRegExp?.['global']; + const roomHighlightsRegExp = this.highlightRegExp?.[id]; + return (((globalHighlightsRegExp?.test(message)) || (roomHighlightsRegExp?.test(message)))); + } + static updateHighlightRegExp(highlights: Record) { + // Enforce boundary for match sides, if a letter on match side is + // a word character. For example, regular expression "a" matches + // "a", but not "abc", while regular expression "!" matches + // "!" and "!abc". + this.highlightRegExp = {}; + for (let i in highlights) { + if (!highlights[i].length) { + this.highlightRegExp[i] = null; + continue; + } + this.highlightRegExp[i] = new RegExp('(?:\\b|(?!\\w))(?:' + highlights[i].join('|') + ')(?:\\b|(?!\\w))', 'i'); + } + } + handleHighlight = (args: Args) => { + let name; + let message; + let msgTime = 0; + if (args[0] === 'c:') { + msgTime = parseInt(args[1]); + name = args[2]; + message = args[3]; + } else { + name = args[1]; + message = args[2]; + } + let lastMessageDates = Dex.prefs('logtimes') || (PS.prefs.set('logtimes', {}), Dex.prefs('logtimes')); + if (!lastMessageDates[PS.server.id]) lastMessageDates[PS.server.id] = {}; + let lastMessageDate = lastMessageDates[PS.server.id][this.id] || 0; + // because the time offset to the server can vary slightly, subtract it to not have it affect comparisons between dates + let serverMsgTime = msgTime - (this.timeOffset || 0); + let mayNotify = serverMsgTime > lastMessageDate && name !== PS.user.userid; + if (PS.isVisible(this)) { + this.lastMessageTime = null; + lastMessageDates[PS.server.id][this.id] = serverMsgTime; + PS.prefs.set('logtimes', lastMessageDates); + } else { + // To be saved on focus + let lastMessageTime = this.lastMessageTime || 0; + if (lastMessageTime < serverMsgTime) this.lastMessageTime = serverMsgTime; + } + if (ChatRoom.getHighlight(message, this.id)) { + if (mayNotify) this.notify({ + title: `Mentioned by ${name} in ${this.id}`, + body: `"${message}"`, + id: 'highlight', + }); + return true; + } return false; }; override clientCommands = this.parseClientCommands({