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({