Preact minor updates batch 30

- Fix starting tournaments with custom name
- Fix error when joining private rooms
- Improve colors for command syntax highlighting
- Improve performance by not updating background rooms
- Rename a bunch of room visibility functions for clarity
- Default language to English
- Add timestamps to disconnect/reconnect messages
- Fix server teams not getting fully downloaded
This commit is contained in:
Guangcong Luo
2026-05-25 12:33:42 +00:00
parent 75fddbbe29
commit fe3d7cd4e0
11 changed files with 74 additions and 46 deletions

View File

@@ -122,6 +122,13 @@ export class BattleLog {
});
this.addNode(el);
}
static renderTimestamp(timestamp: number | null, showTimestamps?: 'minutes' | 'seconds' | null) {
if (!showTimestamps) return '';
const date = timestamp && !isNaN(timestamp) ? new Date(timestamp * 1000) : new Date();
const components = [date.getHours(), date.getMinutes()];
if (showTimestamps === 'seconds') components.push(date.getSeconds());
return `<small class="gray">[${components.map(x => x < 10 ? `0${x}` : x).join(':')}] </small>`;
}
add(args: Args, kwArgs?: KWArgs, preempt?: boolean, showTimestamps?: 'minutes' | 'seconds') {
if (kwArgs?.silent) return;
const battle = this.scene?.battle;
@@ -169,15 +176,7 @@ export class BattleLog {
}
const ignoreList = window.app?.ignore || window.PS?.prefs?.ignore;
if (ignoreList?.[toUserid(name)] && ' +^\u2605\u2606'.includes(rank)) return;
let timestampHtml = '';
if (showTimestamps) {
const date = timestamp && !isNaN(timestamp) ? new Date(timestamp * 1000) : new Date();
const components = [date.getHours(), date.getMinutes()];
if (showTimestamps === 'seconds') {
components.push(date.getSeconds());
}
timestampHtml = `<small class="gray">[${components.map(x => x < 10 ? `0${x}` : x).join(':')}] </small>`;
}
const timestampHtml = BattleLog.renderTimestamp(timestamp, showTimestamps);
const isHighlighted = window.app?.rooms?.[battle!.roomid].getHighlight(message) || this.getHighlight?.(args);
[divClass, divHTML, noNotify] = this.parseChatMessage(message, name, timestampHtml, isHighlighted);
if (!noNotify && isHighlighted) {

View File

@@ -531,7 +531,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
}
localTeam.uploaded = {
teamid: team.teamid,
notLoaded: false,
notLoaded: this.unloadedPackedTeam(team.team) === localTeam.packedTeam,
private: team.private,
};
delete teams[localTeam.teamid];
@@ -554,22 +554,22 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
localTeam.teamid = team.teamid;
localTeam.uploaded = {
teamid: team.teamid,
notLoaded: false,
notLoaded: this.unloadedPackedTeam(team.team) === localTeam.packedTeam,
private: team.private,
};
break;
}
}
if (!matched) {
const mons = team.team.split(',').map((m: string) => ({ species: m, moves: [] }));
const newTeam: Team = {
name: team.name,
format: team.format,
folder: '',
packedTeam: Teams.pack(mons),
packedTeam: this.unloadedPackedTeam(team.team),
iconCache: null,
isBox: false,
key: this.getKey(team.name),
teamid: team.teamid,
uploaded: {
teamid: team.teamid,
notLoaded: true,
@@ -581,6 +581,10 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
}
});
}
unloadedPackedTeam(uploadedTeam: string) {
const mons = uploadedTeam.split(',').map((m: string) => ({ species: m, moves: [] }));
return Teams.pack(mons);
}
loadTeam(team: Team | undefined | null, ifNeeded: true): void | Promise<void>;
loadTeam(team: Team | undefined | null): Promise<void>;
loadTeam(team: Team | undefined | null, ifNeeded?: boolean): void | Promise<void> {
@@ -1059,7 +1063,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
}
notify(options: { title: string, body?: string, noAutoDismiss?: boolean, id?: string }) {
let desktopNotification: Notification | null = null;
const roomIsFocused = document.hasFocus?.() && PS.isVisible(this);
const roomIsFocused = document.hasFocus?.() && PS.isVisiblePanel(this);
if (roomIsFocused && !options.noAutoDismiss) return;
if (!roomIsFocused) {
PS.playNotificationSound();
@@ -1092,7 +1096,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
PS.update();
}
subtleNotify() {
if (PS.isVisible(this)) return;
if (PS.isVisiblePanel(this)) return;
const room = PS.rooms[this.id] as ChatRoom;
const lastSeenTimestamp = PS.prefs.logtimes?.[PS.server.id]?.[this.id] || 0;
const lastMessageTime = +(room.lastMessage?.[1] || 0);
@@ -1245,7 +1249,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID;
const targetRoom = roomid ? PS.rooms[roomid] : this;
if (!targetRoom) return this.errorReply(`Room '${roomid}' not found.`);
if (PS.isNormalRoom(targetRoom)) {
if (PS.isPanel(targetRoom)) {
this.errorReply(`'${roomid}' is already maximized.`);
} else if (!PS.isPopup(targetRoom)) {
PS.moveRoom(targetRoom, 'left', false, 0);
@@ -2180,7 +2184,7 @@ export const PS = new class extends PSModel {
if (!PS.user.initializing) {
room.receiveLine(['error', args[2]]);
}
} else if (args[1] === 'nonexistent') {
} else if (args[1] === 'nonexistent' || args[1] === 'joinfailed') {
// sometimes we assume a room is a chatroom when it's not
// when that happens, just ignore this error
if (room.type === 'chat' || room.type === 'battle') room.receiveLine(args);
@@ -2222,7 +2226,17 @@ export const PS = new class extends PSModel {
}
this.connection.send(`${roomid || ''}|${msg}`);
}
isVisible(room: PSRoom) {
isVisible(room: PSRoom): boolean {
if (PS.isPanel(room)) {
return !this.leftPanelWidth ? room === this.panel : room === this.leftPanel || room === this.rightPanel;
}
if (room.location === 'mini-window') {
return !this.leftPanelWidth ? this.mainmenu === this.panel : this.mainmenu === this.leftPanel;
}
// some kind of popup
return true;
}
isVisiblePanel(room: PSRoom) {
if (!this.leftPanelWidth) {
// one panel visible
return room === this.panel || room === this.room;
@@ -2357,10 +2371,10 @@ export const PS = new class extends PSModel {
return true;
}
this.closePopupsAbove(room, true);
if (!this.isVisible(room)) {
if (!this.isVisiblePanel(room)) {
room.focusNextUpdate = true;
}
if (PS.isNormalRoom(room)) {
if (PS.isPanel(room)) {
if (room.location === 'right') {
this.rightPanel = room;
} else {
@@ -2583,16 +2597,6 @@ export const PS = new class extends PSModel {
PS.focusRoom(PS.leftPanel.id);
}
}
roomVisible(room: PSRoom): boolean {
if (PS.isNormalRoom(room)) {
return !this.leftPanelWidth ? room === this.panel : room === this.leftPanel || room === this.rightPanel;
}
if (room.location === 'mini-window') {
return !this.leftPanelWidth ? this.mainmenu === this.panel : this.mainmenu === this.leftPanel;
}
// some kind of popup
return true;
}
renameRoom(room: PSRoom, id: RoomID) {
// should never happen
if (this.rooms[id]) this.removeRoom(this.rooms[id]);
@@ -2617,7 +2621,8 @@ export const PS = new class extends PSModel {
if (!room) return false;
return room.location === 'popup' || room.location === 'modal-popup';
}
isNormalRoom(room: PSRoom | undefined | null) {
/** this isn't just !isPopup. you forgot about mini windows again. */
isPanel(room: PSRoom | undefined | null) {
if (!room) return false;
return room.location === 'left' || room.location === 'right';
}
@@ -2641,7 +2646,7 @@ export const PS = new class extends PSModel {
return;
}
background ??= !this.roomVisible(room);
background ??= !this.isVisible(room);
if (room.location === 'mini-window') {
const miniRoomIndex = this.miniRoomList.indexOf(room.id);

View File

@@ -53,6 +53,7 @@ interface TournamentTableBracketData {
scores: number[];
}
type TournamentInfo = {
/** nickname will override this */
format?: string,
teambuilderFormat?: string,
generator?: string,
@@ -148,7 +149,8 @@ export class ChatTournament extends PSModel {
} else if (args[0] === 'tournament') {
switch (cmd) {
case 'create': {
this.info.format = args[2];
this.info.format = args[5] || args[2];
this.info.teambuilderFormat = args[2];
this.info.generator = args[3];
const formatName = BattleLog.formatName(args[2]);
const type = args[3];

View File

@@ -107,12 +107,18 @@ export class ChatRoom extends PSRoom {
return;
case 'noinit':
if (this.battle) {
if (this.battle && args[1] === 'joinfailed') {
this.receiveLine(['bigerror', args[2]]);
this.receiveLine(['html',
`<div class="broadcast-red pad"><p class="buttonbar"><button class="button" data-cmd="/close"><strong>Close</strong></button></p></div>`,
]);
} else if (this.battle) {
// check the Replays database
(this as any as BattleRoom).loadReplay();
} else {
const message = args[2] ? BattleLog.escapeHTML(args[2]) : `Chatroom "${BattleLog.escapeHTML(this.title)}" not found`;
this.receiveLine(['html',
`<div class="broadcast-red pad"><h3>Chatroom "${BattleLog.escapeHTML(this.title)}" not found</h3><p class="buttonbar"><button class="button" data-cmd="/close"><strong>Close</strong></button></p></div>`,
`<div class="broadcast-red pad"><h3>${message}</h3><p class="buttonbar"><button class="button" data-cmd="/close"><strong>Close</strong></button></p></div>`,
]);
}
return;
@@ -199,12 +205,13 @@ export class ChatRoom extends PSRoom {
if (time < cutOffTime) cutOffStart = i;
}
if (lines[i].startsWith('|raw|<div class="infobox"> You joined ')) {
reconnectMessage = `|raw|<div class="infobox">You reconnected to ${lines[i].slice(38)}`;
const timestamp = BattleLog.renderTimestamp(Date.now() / 1000, PS.prefs.timestamps?.chatrooms);
reconnectMessage = `|raw|<div class="infobox">${timestamp}You reconnected to ${lines[i].slice(38)}`;
cutOffEnd = i;
if (!lines[i - 1]) cutOffEnd = i - 1;
}
}
console.log("Reconnection log splice:");
console.log(`Reconnection log splice: (cutoff: ${cutOffTime})`);
console.log([
...lines.slice(0, cutOffStart),
'====================',
@@ -215,7 +222,8 @@ export class ChatRoom extends PSRoom {
lines = lines.slice(cutOffStart, cutOffEnd);
if (lines.length) {
this.receiveLine([`raw`, `<div class="infobox">You disconnected.</div>`]);
const timestamp = BattleLog.renderTimestamp(cutOffTime, PS.prefs.timestamps?.chatrooms);
this.receiveLine([`raw`, `<div class="infobox">${timestamp}You disconnected.</div>`]);
for (const line of lines) this.receiveLine(BattleTextParser.parseLine(line));
this.receiveLine(BattleTextParser.parseLine(reconnectMessage));
}
@@ -307,7 +315,7 @@ export class ChatRoom extends PSRoom {
const 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
const time = serverTime - (this.timeOffset || 0);
if (PS.isVisible(this)) {
if (PS.isVisiblePanel(this)) {
this.lastMessageTime = null;
lastMessageDates[PS.server.id][this.id] = time;
PS.prefs.set('logtimes', lastMessageDates);

View File

@@ -458,6 +458,7 @@ export class MainMenuRoom extends PSRoom {
case 'teamupload':
if (PS.teams.uploading) {
const team = PS.teams.uploading;
team.teamid = response.teamid;
team.uploaded = {
teamid: response.teamid,
notLoaded: false,

View File

@@ -666,7 +666,7 @@ class OptionsPanel extends PSRoomPanel {
{this.state.showStatusInput ? (
<p>
<input name="statustext" />
<input class="textbox" name="statustext" />
<button class="button" onClick={this.editStatus}><i class="fa fa-pencil" aria-hidden></i></button>
</p>
) : (
@@ -758,9 +758,9 @@ class OptionsPanel extends PSRoomPanel {
<p>
<label class="optlabel">
Language: {}
<select name="language" onChange={this.handleOnChange} class="button" value={serverSettings.language}>
<option value="german">Deutsch</option>
<select name="language" onChange={this.handleOnChange} class="button" value={serverSettings.language || 'english'}>
<option value="english">English</option>
<option value="german">Deutsch</option>
<option value="spanish">Español</option>
<option value="french">Français</option>
<option value="italian">Italiano</option>

View File

@@ -191,7 +191,7 @@ class RoomsPanel extends PSRoomPanel {
return [...hidden, ["Search results", results]];
}
override render() {
if (this.hidden && PS.isVisible(this.props.room)) this.hidden = false;
if (this.hidden && PS.isVisiblePanel(this.props.room)) this.hidden = false;
if (this.hidden) {
return <PSPanelWrapper room={this.props.room}>{null}</PSPanelWrapper>;
}

View File

@@ -108,7 +108,7 @@ export class PSHeader extends preact.Component {
const room = PS.rooms[id];
if (!room) return null;
const closable = (id === '' || id === 'rooms' ? '' : ' closable');
const cur = PS.isVisible(room) ? ' cur' : '';
const cur = PS.isVisiblePanel(room) ? ' cur' : '';
let notifying = room.isSubtleNotifying ? ' subtle-notifying' : '';
let hoverTitle = '';
let notifications = room.notifications;

View File

@@ -188,6 +188,7 @@ PS.router = new PSRouter();
export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ room: T }> {
subscriptions: PSSubscription[] = [];
wasVisible = true; // remember, shouldComponentUpdate isn't called on first render
subscribeTo<M>(
model: PSModel<M> | PSStreamModel<M>, callback: (value: M) => void = () => { this.forceUpdate(); }
): PSSubscription {
@@ -205,6 +206,12 @@ export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ r
}));
this.componentDidUpdate();
}
override shouldComponentUpdate() {
const wasVisible = this.wasVisible;
const visible = PS.isVisible(this.props.room);
this.wasVisible = visible;
return visible || wasVisible;
}
justUpdatedDimensions = false;
updateDimensions() {
const justUpdated = this.justUpdatedDimensions;
@@ -940,7 +947,7 @@ export class PSView extends preact.Component {
let rooms = [] as preact.VNode[];
for (const roomid in PS.rooms) {
const room = PS.rooms[roomid]!;
if (PS.isNormalRoom(room)) {
if (PS.isPanel(room)) {
rooms.push(this.renderRoom(room));
}
}

View File

@@ -420,6 +420,12 @@ pre.textbox {
.textbox tt {
color: #3388CC;
}
.textbox tt.message-cmd {
color: #1a5f97;
}
.dark .textbox tt.message-cmd {
color: #86b7df;
}
.textbox:hover {
border-color: #474747;
box-shadow: inset 0px 1px 2px #D2D2D2, 1px 1px 0 rgba(255,255,255,.6);

View File

@@ -5,7 +5,7 @@ License: GPLv2
*/
@import url(./battle-log.css?v11);
@import url(./battle-log.css?v12);
.battle {
position: absolute;