mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Preact: More misc fixes
Moderate: - Fix double messages when being DMed (and otherwise joining a room server-side) - Refactor PSModel to support passing a value with updates, so PSStreamModel is explicitly about tracking an update backlog. - Work on supporting AFD mode (it doesn't seem to work yet) - Fix music still playing after closing a battle - Show username (with capitalization and spaces) in DM titles - Don't let people try to search for battles without choosing a username Minor: - Remove random BOM that made it into some files? - Correct date Preact client started (I mistook the date testclient was created) - Refactor updateLayout
This commit is contained in:
parent
0eeed0d7ce
commit
9f18a13bbf
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="/js/lib/jquery-2.2.4.min.js"></script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
,///////,
|
||||
+#########+ ///////,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ https://psim.us/dev
|
|||
<div id="room-news">
|
||||
<div class="mini-window-body">
|
||||
<div class="newsentry"><h4>New updates</h4><p>The Preact alpha test client is very far along!</p><p>Other than the teambuilder, everything else should be ready.</p><p>—<strong>Zarel</strong> <small class="date">on Apr 9, 2025</small></p></div>
|
||||
<div class="newsentry"><h4>Preact alpha</h4><p>Welcome to the Preact alpha test client!</p><p>Many parts are unimplemented and there may be bugs; we're using this page to test it.</p><p>—<strong>Zarel</strong> <small class="date">on Sep 25, 2015</small></p></div>
|
||||
<div class="newsentry"><h4>Preact alpha</h4><p>Welcome to the Preact alpha test client!</p><p>Many parts are unimplemented and there may be bugs; we're using this page to test it.</p><p>—<strong>Zarel</strong> <small class="date">on Oct 16, 2018</small></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,74 +20,78 @@
|
|||
|
||||
const PSURL = `${document.location.protocol !== 'http:' ? 'https:' : ''}//${Config.routes.client}/`;
|
||||
|
||||
export class PSSubscription {
|
||||
observable: PSModel | PSStreamModel<any>;
|
||||
listener: (value?: any) => void;
|
||||
constructor(observable: PSModel | PSStreamModel<any>, listener: (value?: any) => void) {
|
||||
export class PSSubscription<T = any> {
|
||||
observable: PSModel<T> | PSStreamModel<T>;
|
||||
listener: (value: T) => void;
|
||||
constructor(observable: PSModel<T> | PSStreamModel<T>, listener: (value: T) => void) {
|
||||
this.observable = observable;
|
||||
this.listener = listener;
|
||||
}
|
||||
unsubscribe() {
|
||||
const index = this.observable.subscriptions.indexOf(this);
|
||||
const index = this.observable.subscriptions.indexOf(this as any);
|
||||
if (index >= 0) this.observable.subscriptions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PS Models roughly implement the Observable spec. Not the entire
|
||||
* spec - just the parts we use. PSModel just notifies subscribers of
|
||||
* updates - a simple model for React.
|
||||
* PS Models roughly implement the Observable spec. By default,
|
||||
* PSModel notifies listeners when the model is updated. With a
|
||||
* value, PSModel can also stream data out.
|
||||
*
|
||||
* Note that unlike React's usual paradigm, PS Models are not
|
||||
* immutable.
|
||||
*/
|
||||
export class PSModel {
|
||||
subscriptions = [] as PSSubscription[];
|
||||
subscribe(listener: () => void) {
|
||||
const subscription = new PSSubscription(this, listener);
|
||||
export class PSModel<T = null> {
|
||||
subscriptions: PSSubscription<T>[] = [];
|
||||
subscribe(listener: (value: T) => void) {
|
||||
const subscription = new PSSubscription<T>(this, listener);
|
||||
this.subscriptions.push(subscription);
|
||||
return subscription;
|
||||
}
|
||||
subscribeAndRun(listener: () => void) {
|
||||
subscribeAndRun(listener: (value: T) => void, value?: T) {
|
||||
const subscription = this.subscribe(listener);
|
||||
subscription.listener();
|
||||
subscription.listener(value!);
|
||||
return subscription;
|
||||
}
|
||||
update() {
|
||||
update(this: PSModel): void;
|
||||
update(value: T): void;
|
||||
update(value?: T) {
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.listener();
|
||||
subscription.listener(value!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PS Models roughly implement the Observable spec. PSStreamModel
|
||||
* streams some data out. This is very not-React, which generally
|
||||
* expects the DOM to be a pure function of state. Instead PSModels
|
||||
* which hold state, PSStreamModels give state directly to views,
|
||||
* so that the model doesn't need to hold a redundant copy of state.
|
||||
* @see PSModel
|
||||
*
|
||||
* The main difference is that StreamModel keeps a backlog,
|
||||
* so events generated before something subscribes are not
|
||||
* lost. Nullish values are not kept in the backlog.
|
||||
*/
|
||||
export class PSStreamModel<T = string> {
|
||||
subscriptions = [] as PSSubscription[];
|
||||
updates = [] as T[];
|
||||
subscriptions: PSSubscription<T>[] = [];
|
||||
backlog: NonNullable<T>[] | null = [];
|
||||
subscribe(listener: (value: T) => void) {
|
||||
// TypeScript bug
|
||||
const subscription: PSSubscription = new PSSubscription(this, listener);
|
||||
const subscription: PSSubscription<T> = new PSSubscription<T>(this, listener);
|
||||
this.subscriptions.push(subscription);
|
||||
if (this.updates.length) {
|
||||
for (const update of this.updates) {
|
||||
if (this.backlog) {
|
||||
for (const update of this.backlog) {
|
||||
subscription.listener(update);
|
||||
}
|
||||
this.updates = [];
|
||||
this.backlog = null;
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
subscribeAndRun(listener: (value: T) => void) {
|
||||
subscribeAndRun(listener: (value: T) => void, value: T = null!) {
|
||||
const subscription = this.subscribe(listener);
|
||||
subscription.listener(null);
|
||||
subscription.listener(value);
|
||||
return subscription;
|
||||
}
|
||||
update(value: T) {
|
||||
if (!this.subscriptions.length) {
|
||||
if (!this.subscriptions.length && value !== null && value !== undefined) {
|
||||
// save updates for later
|
||||
this.updates.push(value);
|
||||
(this.backlog ||= []).push(value);
|
||||
}
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.listener(value);
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ import { PSModel, PSStreamModel } from './client-core';
|
|||
import type { PSRoomPanel, PSRouter } from './panels';
|
||||
import type { ChatRoom } from './panel-chat';
|
||||
import type { MainMenuRoom } from './panel-mainmenu';
|
||||
import { toID, type ID } from './battle-dex';
|
||||
import { Dex, toID, type ID } from './battle-dex';
|
||||
import { BattleTextParser, type Args } from './battle-text-parser';
|
||||
|
||||
declare const BattleTextAFD: any;
|
||||
declare const BattleTextNotAFD: any;
|
||||
|
||||
/**********************************************************************
|
||||
* Prefs
|
||||
*********************************************************************/
|
||||
|
|
@ -67,6 +70,8 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
musicvolume = 50;
|
||||
notifvolume = 50;
|
||||
|
||||
afd: boolean | 'sprites' = false;
|
||||
|
||||
// PREFS END HERE
|
||||
|
||||
storageEngine: 'localStorage' | 'iframeLocalStorage' | '' = '';
|
||||
|
|
@ -104,6 +109,7 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
this.update(key);
|
||||
this.save();
|
||||
}
|
||||
|
||||
load(newPrefs: object, noSave?: boolean) {
|
||||
this.fixPrefs(newPrefs);
|
||||
Object.assign(this, PSPrefsDefaults);
|
||||
|
|
@ -111,6 +117,7 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
for (const key in PSPrefsDefaults) {
|
||||
if (key in newPrefs) (this as any)[key] = (newPrefs as any)[key];
|
||||
}
|
||||
this.setAFD();
|
||||
this.update(null);
|
||||
if (!noSave) this.save();
|
||||
}
|
||||
|
|
@ -155,6 +162,38 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
delete newPrefs['dark'];
|
||||
}
|
||||
}
|
||||
|
||||
setAFD(mode?: typeof this['afd']) {
|
||||
if (mode === undefined) {
|
||||
// init
|
||||
if (typeof BattleTextAFD !== 'undefined') {
|
||||
for (const id in BattleTextNotAFD) {
|
||||
if (!BattleTextAFD[id]) {
|
||||
BattleTextAFD[id] = BattleTextNotAFD[id];
|
||||
} else {
|
||||
BattleTextAFD[id] = { ...BattleTextNotAFD[id], ...BattleTextAFD[id] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.server.afd) {
|
||||
mode = true;
|
||||
} else if (this.afd !== undefined) {
|
||||
mode = this.afd;
|
||||
} else {
|
||||
// uncomment on April Fools' Day
|
||||
// mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
Dex.afdMode = mode;
|
||||
|
||||
if (mode === true) {
|
||||
(BattleText as any) = BattleTextAFD;
|
||||
} else {
|
||||
(BattleText as any) = BattleTextNotAFD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
|
|
@ -551,10 +590,8 @@ export interface RoomOptions {
|
|||
location?: PSRoomLocation | null;
|
||||
/**
|
||||
* In case the room received messages before it was ready for them.
|
||||
*
|
||||
* Fed to `room.receiveLine` after initialization, after the constructor is run.
|
||||
*/
|
||||
backlog?: Args[];
|
||||
backlog?: Args[] | null;
|
||||
/**
|
||||
* Popup parent element. If it exists, a popup shows up right above/below that element.
|
||||
*
|
||||
|
|
@ -643,6 +680,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
if (this.location !== 'popup' && this.location !== 'semimodal-popup') this.parentElem = null;
|
||||
if (options.rightPopup) this.rightPopup = true;
|
||||
if (options.connected) this.connected = true;
|
||||
if (options.backlog) this.backlog = options.backlog;
|
||||
this.noURL = options.noURL || false;
|
||||
this.args = options.args || null;
|
||||
}
|
||||
|
|
@ -741,17 +779,9 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
}
|
||||
|
||||
class PlaceholderRoom extends PSRoom {
|
||||
backlog = [] as Args[];
|
||||
override readonly classType = 'placeholder';
|
||||
override receiveLine(args: Args) {
|
||||
try {
|
||||
this.backlog.push(args);
|
||||
} catch (err: any) {
|
||||
console.log(this.backlog.length);
|
||||
console.log(args.length);
|
||||
console.log(this.backlog.slice(0, 1000));
|
||||
throw err;
|
||||
}
|
||||
(this.backlog ||= []).push(args);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -936,7 +966,10 @@ export const PS = new class extends PSModel {
|
|||
}
|
||||
|
||||
this.updateLayout();
|
||||
window.addEventListener('resize', () => this.updateLayout());
|
||||
window.addEventListener('resize', () => {
|
||||
// super.update() skips another updateLayout() call
|
||||
if (this.updateLayout()) super.update();
|
||||
});
|
||||
}
|
||||
|
||||
// Panel layout
|
||||
|
|
@ -987,7 +1020,8 @@ export const PS = new class extends PSModel {
|
|||
maxWidth: 640,
|
||||
};
|
||||
}
|
||||
updateLayout(alreadyUpdating?: boolean) {
|
||||
/** @returns changed */
|
||||
updateLayout(): boolean {
|
||||
const leftPanelWidth = this.calculateLeftPanelWidth();
|
||||
const totalWidth = document.body.offsetWidth;
|
||||
const totalHeight = document.body.offsetHeight;
|
||||
|
|
@ -1007,8 +1041,9 @@ export const PS = new class extends PSModel {
|
|||
|
||||
if (this.leftPanelWidth !== leftPanelWidth) {
|
||||
this.leftPanelWidth = leftPanelWidth;
|
||||
if (!alreadyUpdating) this.update(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
getRoom(elem: HTMLElement | EventTarget | null | undefined, skipClickable?: boolean): PSRoom | null {
|
||||
let curElem: HTMLElement | null = elem as HTMLElement;
|
||||
|
|
@ -1039,8 +1074,8 @@ export const PS = new class extends PSModel {
|
|||
PS.moveRoom(fromRoom, toLocation, onHome, toIndex);
|
||||
PS.update();
|
||||
}
|
||||
override update(layoutAlreadyUpdated?: boolean) {
|
||||
if (!layoutAlreadyUpdated) this.updateLayout(true);
|
||||
override update() {
|
||||
this.updateLayout();
|
||||
super.update();
|
||||
}
|
||||
receive(msg: string) {
|
||||
|
|
@ -1234,11 +1269,6 @@ export const PS = new class extends PSModel {
|
|||
if (this.room === room) this.room = newRoom;
|
||||
if (roomid === '') this.mainmenu = newRoom as MainMenuRoom;
|
||||
|
||||
if (options.backlog) {
|
||||
for (const args of options.backlog) {
|
||||
room.receiveLine(args);
|
||||
}
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
if (updated) this.update();
|
||||
|
|
|
|||
|
|
@ -41,15 +41,13 @@ export class ChatRoom extends PSRoom {
|
|||
/** n.b. this will be null outside of battle rooms */
|
||||
battle: Battle | null = null;
|
||||
log: BattleLog | null = null;
|
||||
/** during initialization, room could get messages before it has a log */
|
||||
backlog: Args[] | null = null;
|
||||
|
||||
constructor(options: RoomOptions) {
|
||||
super(options);
|
||||
if (options.args?.pmTarget) this.pmTarget = options.args.pmTarget as string;
|
||||
if (options.args?.challengeMenuOpen) this.challengeMenuOpen = true;
|
||||
if (options.args?.initialSlash) this.initialSlash = true;
|
||||
this.updateTarget();
|
||||
this.updateTarget(this.pmTarget);
|
||||
this.connect();
|
||||
}
|
||||
override connect() {
|
||||
|
|
@ -60,23 +58,45 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
}
|
||||
override receiveLine(args: Args) {
|
||||
if (!this.log) (this.backlog ||= []).push(args);
|
||||
switch (args[0]) {
|
||||
case 'users':
|
||||
const usernames = args[1].split(',');
|
||||
const count = parseInt(usernames.shift()!, 10);
|
||||
this.setUsers(count, usernames);
|
||||
return;
|
||||
case 'join': case 'j': case 'J':
|
||||
this.addUser(args[1]);
|
||||
break;
|
||||
case 'leave': case 'l': case 'L':
|
||||
this.removeUser(args[1]);
|
||||
break;
|
||||
case 'name': case 'n': case 'N':
|
||||
this.renameUser(args[1], args[2]);
|
||||
break;
|
||||
case 'c':
|
||||
if (`${args[2]} `.startsWith('/challenge ')) {
|
||||
this.updateChallenge(args[1], args[2].slice(11));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
super.receiveLine(args);
|
||||
}
|
||||
updateTarget() {
|
||||
updateTarget(name?: string | null) {
|
||||
if (this.id === 'dm-') {
|
||||
this.pmTarget = PS.user.userid;
|
||||
this.setUsers(1, [` ${PS.user.userid}`]);
|
||||
this.pmTarget = PS.user.name;
|
||||
this.setUsers(1, [` ${PS.user.name}`]);
|
||||
this.title = `Console`;
|
||||
} else if (this.id.startsWith('dm-')) {
|
||||
const id = this.id.slice(3);
|
||||
this.pmTarget = id;
|
||||
if (!name || toID(name) !== id) name = this.pmTarget || id;
|
||||
this.pmTarget = name;
|
||||
if (!PS.user.userid) {
|
||||
this.setUsers(1, [` ${id}`]);
|
||||
this.setUsers(1, [` ${name}`]);
|
||||
} else {
|
||||
this.setUsers(2, [` ${id}`, ` ${PS.user.userid}`]);
|
||||
this.setUsers(2, [` ${name}`, ` ${PS.user.name}`]);
|
||||
}
|
||||
this.title = `[DM] ${this.pmTarget}`;
|
||||
this.title = `[DM] ${name}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -212,6 +232,12 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
override destroy() {
|
||||
if (this.pmTarget) this.connected = false;
|
||||
if (this.battle) {
|
||||
// since battle is defined here, we might as well deallocate it here
|
||||
this.battle.destroy();
|
||||
} else {
|
||||
this.log?.destroy();
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -536,21 +562,12 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
|||
export class ChatUserList extends preact.Component<{
|
||||
room: ChatRoom, left?: number, top?: number, minimized?: boolean,
|
||||
}> {
|
||||
subscription: PSSubscription | null = null;
|
||||
override state = {
|
||||
expanded: false,
|
||||
};
|
||||
toggleExpanded = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
};
|
||||
override componentDidMount() {
|
||||
this.subscription = this.props.room.subscribe(msg => {
|
||||
if (!msg) this.forceUpdate();
|
||||
});
|
||||
}
|
||||
override componentWillUnmount() {
|
||||
if (this.subscription) this.subscription.unsubscribe();
|
||||
}
|
||||
render() {
|
||||
const room = this.props.room;
|
||||
let userList = Object.entries(room.users) as [ID, string][];
|
||||
|
|
@ -612,33 +629,11 @@ export class ChatLog extends preact.Component<{
|
|||
room.log.add(args);
|
||||
}
|
||||
}
|
||||
this.subscription = room.subscribe(tokens => {
|
||||
if (!tokens) return;
|
||||
this.props.room.log!.add(tokens);
|
||||
});
|
||||
}
|
||||
this.subscription = room.subscribe(tokens => {
|
||||
if (!tokens) return;
|
||||
switch (tokens[0]) {
|
||||
case 'users':
|
||||
const usernames = tokens[1].split(',');
|
||||
const count = parseInt(usernames.shift()!, 10);
|
||||
this.props.room.setUsers(count, usernames);
|
||||
return;
|
||||
case 'join': case 'j': case 'J':
|
||||
this.props.room.addUser(tokens[1]);
|
||||
break;
|
||||
case 'leave': case 'l': case 'L':
|
||||
this.props.room.removeUser(tokens[1]);
|
||||
break;
|
||||
case 'name': case 'n': case 'N':
|
||||
this.props.room.renameUser(tokens[1], tokens[2]);
|
||||
break;
|
||||
case 'c':
|
||||
if (`${tokens[2]} `.startsWith('/challenge ')) {
|
||||
this.props.room.updateChallenge(tokens[1], tokens[2].slice(11));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!this.props.noSubscription) this.props.room.log!.add(tokens);
|
||||
});
|
||||
this.setControlsJSX(this.props.children);
|
||||
}
|
||||
override componentWillUnmount() {
|
||||
|
|
|
|||
|
|
@ -310,6 +310,8 @@ export class MainMenuRoom extends PSRoom {
|
|||
args: { pmTarget },
|
||||
}, true);
|
||||
room = PS.rooms[roomid] as ChatRoom;
|
||||
} else {
|
||||
room.updateTarget(pmTarget);
|
||||
}
|
||||
room.receiveLine([`c`, user1, message]);
|
||||
PS.update();
|
||||
|
|
@ -388,6 +390,12 @@ class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
|
|||
this.base?.querySelector<HTMLButtonElement>('.formatselect')?.focus();
|
||||
}
|
||||
submitSearch = (ev: Event, format: string, team?: Team) => {
|
||||
if (!PS.user.named) {
|
||||
PS.join('login' as RoomID, {
|
||||
parentElem: this.base!.querySelector<HTMLElement>('.big.button'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
PS.mainmenu.startSearch(format, team);
|
||||
};
|
||||
handleDragStart = (e: DragEvent) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user