mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-10-01 23:32:02 -05:00
Support |tempnotify|
After a long time struggling to decide how to implement this, I ended up unifying line parsing. Advantage: No need to call `BattleTextParser.parseLine` every time you want to inspect a line. It's parsed into args by `PS.receive` and everything else gets tokens. Disadvantage: Battle rooms need to reassemble the original line to put in their history buffer (`battle.activityQueue`). The lines are getting parsed twice, anyway, and reassembly is a small price to pay for how readable everything else is now.
This commit is contained in:
@@ -347,7 +347,7 @@ interface RoomOptions {
|
||||
type?: string;
|
||||
location?: PSRoomLocation | null;
|
||||
/** Handled after initialization, outside of the constructor */
|
||||
queue?: string[];
|
||||
queue?: Args[];
|
||||
parentElem?: HTMLElement | null;
|
||||
parentRoomid?: RoomID | null;
|
||||
rightPopup?: boolean;
|
||||
@@ -355,11 +355,23 @@ interface RoomOptions {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
class PSRoom extends PSStreamModel<string | null> implements RoomOptions {
|
||||
interface PSNotificationState {
|
||||
title: string;
|
||||
body?: string;
|
||||
/** Used to identify notifications to be dismissed - '' if you only want to autodismiss */
|
||||
id: string;
|
||||
/** normally: automatically dismiss the notification when viewing the room; set this to require manual dismissing */
|
||||
noAutoDismiss: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message",
|
||||
* and `null` to mean "tell Preact to re-render this room"
|
||||
*/
|
||||
class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
||||
id: RoomID;
|
||||
title = "";
|
||||
type = '';
|
||||
notifying: '' | ' notifying' | ' subtle-notifying' = '';
|
||||
readonly classType: string = '';
|
||||
location: PSRoomLocation = 'left';
|
||||
closable = true;
|
||||
@@ -371,6 +383,12 @@ class PSRoom extends PSStreamModel<string | null> implements RoomOptions {
|
||||
* after sending `/leave`, even before the server responds.
|
||||
*/
|
||||
connected: boolean = false;
|
||||
/**
|
||||
* Can this room even be connected to at all?
|
||||
* `true` = pass messages from the server to subscribers
|
||||
* `false` = throw an error if we receive messages from the server
|
||||
*/
|
||||
readonly canConnect: boolean = false;
|
||||
connectWhenLoggedIn: boolean = false;
|
||||
onParentEvent: ((eventId: 'focus' | 'keydown', e?: Event) => false | void) | null = null;
|
||||
|
||||
@@ -379,6 +397,9 @@ class PSRoom extends PSStreamModel<string | null> implements RoomOptions {
|
||||
parentElem: HTMLElement | null = null;
|
||||
rightPopup = false;
|
||||
|
||||
notifications: PSNotificationState[] = [];
|
||||
isSubtleNotifying = false;
|
||||
|
||||
// for compatibility with RoomOptions
|
||||
[k: string]: unknown;
|
||||
|
||||
@@ -394,17 +415,56 @@ class PSRoom extends PSStreamModel<string | null> implements RoomOptions {
|
||||
if (options.rightPopup) this.rightPopup = true;
|
||||
if (options.connected) this.connected = true;
|
||||
}
|
||||
notify(options: {title: string, body?: string, noAutoDismiss?: boolean, id?: string}) {
|
||||
if (options.noAutoDismiss && !options.id) {
|
||||
throw new Error(`Must specify id for manual dismissing`);
|
||||
}
|
||||
this.notifications.push({
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
id: options.id || '',
|
||||
noAutoDismiss: options.noAutoDismiss || false,
|
||||
});
|
||||
PS.update();
|
||||
}
|
||||
dismissNotification(id: string) {
|
||||
this.notifications = this.notifications.filter(notification => notification.id !== id);
|
||||
PS.update();
|
||||
}
|
||||
autoDismissNotifications() {
|
||||
this.notifications = this.notifications.filter(notification => notification.noAutoDismiss);
|
||||
this.isSubtleNotifying = false;
|
||||
}
|
||||
setDimensions(width: number, height: number) {
|
||||
if (this.width === width && this.height === height) return;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
connect(): void {
|
||||
throw new Error(`This room is not designed to connect to a server room`);
|
||||
}
|
||||
receive(message: string): void {
|
||||
throw new Error(`This room is not designed to receive messages`);
|
||||
receiveLine(args: Args): void {
|
||||
switch (args[0]) {
|
||||
case 'title': {
|
||||
this.title = args[1];
|
||||
PS.update();
|
||||
break;
|
||||
} case 'tempnotify': {
|
||||
const [, id, title, body, toHighlight] = args;
|
||||
this.notify({title, body, id});
|
||||
break;
|
||||
} case 'tempnotifyoff': {
|
||||
const [, id] = args;
|
||||
this.dismissNotification(id);
|
||||
break;
|
||||
} default: {
|
||||
if (this.canConnect) {
|
||||
this.update(args);
|
||||
} else {
|
||||
throw new Error(`This room is not designed to receive messages`);
|
||||
}
|
||||
}}
|
||||
}
|
||||
send(msg: string, direct?: boolean) {
|
||||
const id = this.id === 'lobby' ? '' : this.id;
|
||||
@@ -419,10 +479,10 @@ class PSRoom extends PSStreamModel<string | null> implements RoomOptions {
|
||||
}
|
||||
|
||||
class PlaceholderRoom extends PSRoom {
|
||||
queue = [] as string[];
|
||||
queue = [] as Args[];
|
||||
readonly classType: 'placeholder' = 'placeholder';
|
||||
receive(message: string) {
|
||||
this.queue.push(message);
|
||||
receiveLine(args: Args) {
|
||||
this.queue.push(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,10 +702,12 @@ const PS = new class extends PSModel {
|
||||
console.log('\u2705 ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #007700");
|
||||
let isInit = false;
|
||||
for (const line of msg.split('\n')) {
|
||||
if (line.startsWith('|init|')) {
|
||||
const args = BattleTextParser.parseLine(line);
|
||||
switch (args[0]) {
|
||||
case 'init': {
|
||||
isInit = true;
|
||||
room = PS.rooms[roomid2];
|
||||
const type = line.slice(6);
|
||||
const [, type] = args;
|
||||
if (!room) {
|
||||
this.addRoom({
|
||||
id: roomid2,
|
||||
@@ -660,8 +722,7 @@ const PS = new class extends PSModel {
|
||||
}
|
||||
this.update();
|
||||
continue;
|
||||
}
|
||||
if ((line + '|').startsWith('|deinit|')) {
|
||||
} case 'deinit': {
|
||||
room = PS.rooms[roomid2];
|
||||
if (room) {
|
||||
room.connected = false;
|
||||
@@ -669,21 +730,20 @@ const PS = new class extends PSModel {
|
||||
}
|
||||
this.update();
|
||||
continue;
|
||||
}
|
||||
if ((line + '|').startsWith('|noinit|')) {
|
||||
} case 'noinit': {
|
||||
room = PS.rooms[roomid2];
|
||||
if (room) {
|
||||
room.connected = false;
|
||||
if ((line + '|').startsWith('|noinit|namerequired|')) {
|
||||
if (args[1] === 'namerequired') {
|
||||
room.connectWhenLoggedIn = true;
|
||||
}
|
||||
}
|
||||
this.update();
|
||||
continue;
|
||||
}
|
||||
if (room) room.receive(line);
|
||||
}}
|
||||
if (room) room.receiveLine(args);
|
||||
}
|
||||
if (room) room.update(isInit ? `|initdone` : null);
|
||||
if (room) room.update(isInit ? [`initdone`] : null);
|
||||
}
|
||||
send(fullMsg: string) {
|
||||
const pipeIndex = fullMsg.indexOf('|');
|
||||
@@ -810,8 +870,8 @@ const PS = new class extends PSModel {
|
||||
if (roomid === '') this.mainmenu = newRoom as MainMenuRoom;
|
||||
|
||||
if (options.queue) {
|
||||
for (const line of options.queue) {
|
||||
room.receive(line);
|
||||
for (const args of options.queue) {
|
||||
room.receiveLine(args);
|
||||
}
|
||||
}
|
||||
updated = true;
|
||||
@@ -835,6 +895,7 @@ const PS = new class extends PSModel {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
this.room.autoDismissNotifications();
|
||||
this.update();
|
||||
if (this.room.onParentEvent) this.room.onParentEvent('focus', undefined);
|
||||
return true;
|
||||
@@ -956,8 +1017,8 @@ const PS = new class extends PSModel {
|
||||
this.room = room;
|
||||
}
|
||||
if (options.queue) {
|
||||
for (const line of options.queue) {
|
||||
room.receive(line);
|
||||
for (const args of options.queue) {
|
||||
room.receiveLine(args);
|
||||
}
|
||||
}
|
||||
return room;
|
||||
|
||||
@@ -25,7 +25,7 @@ class BattlesRoom extends PSRoom {
|
||||
if (format === this.format) return this.refresh();
|
||||
this.battles = null;
|
||||
this.format = format;
|
||||
this.update('');
|
||||
this.update(null);
|
||||
this.refresh();
|
||||
}
|
||||
refresh() {
|
||||
@@ -83,14 +83,10 @@ class BattlesPanel extends PSRoomPanel<BattlesRoom> {
|
||||
|
||||
class BattleRoom extends ChatRoom {
|
||||
readonly classType = 'battle';
|
||||
// @ts-ignore assigned in parent constructor
|
||||
pmTarget: null;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengeMenuOpen: false;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengingFormat: null;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengedFormat: null;
|
||||
pmTarget!: null;
|
||||
challengeMenuOpen!: false;
|
||||
challengingFormat!: null;
|
||||
challengedFormat!: null;
|
||||
battle: Battle = null!;
|
||||
/**
|
||||
* @return true to prevent line from being sent to server
|
||||
@@ -103,11 +99,11 @@ class BattleRoom extends ChatRoom {
|
||||
switch (cmd) {
|
||||
case 'play': {
|
||||
this.battle.play();
|
||||
this.update('');
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'pause': {
|
||||
this.battle.pause();
|
||||
this.update('');
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'ffto': case 'fastfowardto': {
|
||||
let turnNum = Number(target);
|
||||
@@ -118,11 +114,11 @@ class BattleRoom extends ChatRoom {
|
||||
turnNum = -1;
|
||||
}
|
||||
if (isNaN(turnNum)) {
|
||||
this.receive(`|error|/ffto - Invalid turn number: ${target}`);
|
||||
this.receiveLine([`error`, `/ffto - Invalid turn number: ${target}`]);
|
||||
return true;
|
||||
}
|
||||
this.battle.fastForwardTo(turnNum);
|
||||
this.update('');
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'switchsides': {
|
||||
this.battle.switchSides();
|
||||
@@ -172,12 +168,12 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
|
||||
battle.play();
|
||||
super.componentDidMount();
|
||||
}
|
||||
receive(line: string) {
|
||||
if (line === `|initdone`) {
|
||||
receiveLine(args: Args) {
|
||||
if (args[0] === `initdone`) {
|
||||
this.props.room.battle.fastForwardTo(-1);
|
||||
return;
|
||||
}
|
||||
this.props.room.battle.add(line);
|
||||
this.props.room.battle.add('|' + args.join('|'));
|
||||
}
|
||||
renderControls() {
|
||||
const battle = this.props.room.battle;
|
||||
|
||||
@@ -9,6 +9,7 @@ class ChatRoom extends PSRoom {
|
||||
readonly classType: 'chat' | 'battle' = 'chat';
|
||||
users: {[userid: string]: string} = {};
|
||||
userCount = 0;
|
||||
readonly canConnect = true;
|
||||
|
||||
// PM-only properties
|
||||
pmTarget: string | null = null;
|
||||
@@ -77,22 +78,22 @@ class ChatRoom extends PSRoom {
|
||||
return true;
|
||||
} case 'reject': {
|
||||
this.challengedFormat = null;
|
||||
this.update('');
|
||||
this.update(null);
|
||||
return false;
|
||||
}}
|
||||
return false;
|
||||
}
|
||||
openChallenge() {
|
||||
if (!this.pmTarget) {
|
||||
this.receive(`|error|Can only be used in a PM.`);
|
||||
this.receiveLine([`error`, `Can only be used in a PM.`]);
|
||||
return;
|
||||
}
|
||||
this.challengeMenuOpen = true;
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
cancelChallenge() {
|
||||
if (!this.pmTarget) {
|
||||
this.receive(`|error|Can only be used in a PM.`);
|
||||
this.receiveLine([`error`, `Can only be used in a PM.`]);
|
||||
return;
|
||||
}
|
||||
if (this.challengingFormat) {
|
||||
@@ -102,7 +103,7 @@ class ChatRoom extends PSRoom {
|
||||
} else {
|
||||
this.challengeMenuOpen = false;
|
||||
}
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
send(line: string, direct?: boolean) {
|
||||
this.updateTarget();
|
||||
@@ -113,9 +114,6 @@ class ChatRoom extends PSRoom {
|
||||
}
|
||||
super.send(line);
|
||||
}
|
||||
receive(line: string) {
|
||||
this.update(line);
|
||||
}
|
||||
setUsers(count: number, usernames: string[]) {
|
||||
this.userCount = count;
|
||||
this.users = {};
|
||||
@@ -123,13 +121,13 @@ class ChatRoom extends PSRoom {
|
||||
const userid = toID(username);
|
||||
this.users[userid] = username;
|
||||
}
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
addUser(username: string) {
|
||||
const userid = toID(username);
|
||||
if (!(userid in this.users)) this.userCount++;
|
||||
this.users[userid] = username;
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
removeUser(username: string, noUpdate?: boolean) {
|
||||
const userid = toID(username);
|
||||
@@ -137,12 +135,12 @@ class ChatRoom extends PSRoom {
|
||||
this.userCount--;
|
||||
delete this.users[userid];
|
||||
}
|
||||
if (!noUpdate) this.update('');
|
||||
if (!noUpdate) this.update(null);
|
||||
}
|
||||
renameUser(username: string, oldUsername: string) {
|
||||
this.removeUser(oldUsername, true);
|
||||
this.addUser(username);
|
||||
this.update('');
|
||||
this.update(null);
|
||||
}
|
||||
destroy() {
|
||||
if (this.pmTarget) this.connected = false;
|
||||
@@ -347,7 +345,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
||||
PS.send(`|/challenge ${room.pmTarget}, ${format}`);
|
||||
room.challengeMenuOpen = false;
|
||||
room.challengingFormat = format;
|
||||
room.update('');
|
||||
room.update(null);
|
||||
};
|
||||
acceptChallenge = (e: Event, format: string, team?: Team) => {
|
||||
const room = this.props.room;
|
||||
@@ -356,7 +354,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
||||
PS.send(`|/utm ${packedTeam}`);
|
||||
this.props.room.send(`/accept`);
|
||||
room.challengedFormat = null;
|
||||
room.update('');
|
||||
room.update(null);
|
||||
};
|
||||
render() {
|
||||
const room = this.props.room;
|
||||
@@ -460,14 +458,9 @@ class ChatLog extends preact.Component<{
|
||||
if (!this.props.noSubscription) {
|
||||
this.log = new BattleLog(this.base! as HTMLDivElement);
|
||||
}
|
||||
this.subscription = this.props.room.subscribe(msg => {
|
||||
if (!msg) return;
|
||||
const tokens = BattleTextParser.parseLine(msg);
|
||||
this.subscription = this.props.room.subscribe(tokens => {
|
||||
if (!tokens) return;
|
||||
switch (tokens[0]) {
|
||||
case 'title':
|
||||
this.props.room.title = tokens[1];
|
||||
PS.update();
|
||||
return;
|
||||
case 'users':
|
||||
const usernames = tokens[1].split(',');
|
||||
const count = parseInt(usernames.shift()!, 10);
|
||||
|
||||
@@ -22,40 +22,46 @@ class MainMenuRoom extends PSRoom {
|
||||
official?: RoomInfo[],
|
||||
pspl?: RoomInfo[],
|
||||
} = {};
|
||||
receive(line: string) {
|
||||
const tokens = BattleTextParser.parseLine(line);
|
||||
switch (tokens[0]) {
|
||||
case 'challstr':
|
||||
receiveLine(args: Args) {
|
||||
const [cmd] = args;
|
||||
switch (cmd) {
|
||||
case 'challstr': {
|
||||
const [, challstr] = args;
|
||||
PSLoginServer.query({
|
||||
act: 'upkeep',
|
||||
challstr: tokens[1],
|
||||
challstr,
|
||||
}, res => {
|
||||
if (!res) return;
|
||||
if (!res.loggedin) return;
|
||||
this.send(`/trn ${res.username},0,${res.assertion}`);
|
||||
});
|
||||
return;
|
||||
case 'updateuser':
|
||||
PS.user.setName(tokens[1], tokens[2] === '1', tokens[3]);
|
||||
} case 'updateuser': {
|
||||
const [, fullName, namedCode, avatar] = args;
|
||||
PS.user.setName(fullName, namedCode === '1', avatar);
|
||||
return;
|
||||
case 'updatechallenges':
|
||||
this.receiveChallenges(tokens[1]);
|
||||
} case 'updatechallenges': {
|
||||
const [, challengesBuf] = args;
|
||||
this.receiveChallenges(challengesBuf);
|
||||
return;
|
||||
case 'queryresponse':
|
||||
this.handleQueryResponse(tokens[1] as ID, JSON.parse(tokens[2]));
|
||||
} case 'queryresponse': {
|
||||
const [, queryId, responseJSON] = args;
|
||||
this.handleQueryResponse(queryId as ID, JSON.parse(responseJSON));
|
||||
return;
|
||||
case 'pm':
|
||||
this.handlePM(tokens[1], tokens[2], tokens[3]);
|
||||
} case 'pm': {
|
||||
const [, user1, user2, message] = args;
|
||||
this.handlePM(user1, user2, message);
|
||||
return;
|
||||
case 'formats':
|
||||
this.parseFormats(tokens);
|
||||
} case 'formats': {
|
||||
this.parseFormats(args);
|
||||
return;
|
||||
case 'popup':
|
||||
alert(tokens[1]);
|
||||
} case 'popup': {
|
||||
const [, message] = args;
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
const lobby = PS.rooms['lobby'];
|
||||
if (lobby) lobby.receive(line);
|
||||
if (lobby) lobby.receiveLine(args);
|
||||
}
|
||||
receiveChallenges(dataBuf: string) {
|
||||
let json;
|
||||
@@ -78,7 +84,7 @@ class MainMenuRoom extends PSRoom {
|
||||
}
|
||||
room.challengedFormat = json.challengesFrom[targetUserid] || null;
|
||||
room.challengingFormat = json.challengeTo?.to === targetUserid ? json.challengeTo.format : null;
|
||||
room.update('');
|
||||
room.update(null);
|
||||
}
|
||||
}
|
||||
parseFormats(formatsList: string[]) {
|
||||
@@ -217,7 +223,7 @@ class MainMenuRoom extends PSRoom {
|
||||
}, true);
|
||||
room = PS.rooms[roomid]!;
|
||||
}
|
||||
room.receive(`|c|${user1}|${message}`);
|
||||
room.receiveLine([`c`, user1, message]);
|
||||
PS.update();
|
||||
}
|
||||
handleQueryResponse(id: ID, response: any) {
|
||||
@@ -231,12 +237,12 @@ class MainMenuRoom extends PSRoom {
|
||||
Object.assign(userdetails, response);
|
||||
}
|
||||
const userRoom = PS.rooms[`user-${userid}`] as UserRoom;
|
||||
if (userRoom) userRoom.update('');
|
||||
if (userRoom) userRoom.update(null);
|
||||
break;
|
||||
case 'rooms':
|
||||
this.roomsCache = response;
|
||||
const roomsRoom = PS.rooms[`rooms`] as RoomsRoom;
|
||||
if (roomsRoom) roomsRoom.update('');
|
||||
if (roomsRoom) roomsRoom.update(null);
|
||||
break;
|
||||
case 'roomlist':
|
||||
const battlesRoom = PS.rooms[`battles`] as BattlesRoom;
|
||||
@@ -248,7 +254,7 @@ class MainMenuRoom extends PSRoom {
|
||||
battles.push(battleTable[battleid]);
|
||||
}
|
||||
battlesRoom.battles = battles;
|
||||
battlesRoom.update('');
|
||||
battlesRoom.update(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class PSHeader extends preact.Component<{style: {}}> {
|
||||
const room = PS.rooms[id]!;
|
||||
const closable = (id === '' || id === 'rooms' ? '' : ' closable');
|
||||
const cur = PS.isVisible(room) ? ' cur' : '';
|
||||
let className = `roomtab button${room.notifying}${closable}${cur}`;
|
||||
const notifying = room.notifications.length ? ' notifying' : room.isSubtleNotifying ? ' subtle-notifying' : '';
|
||||
let className = `roomtab button${notifying}${closable}${cur}`;
|
||||
let icon = null;
|
||||
let title = room.title;
|
||||
let closeButton = null;
|
||||
|
||||
@@ -97,9 +97,9 @@ class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{room: T}>
|
||||
this.props.room.onParentEvent = (id: string, e?: Event) => {
|
||||
if (id === 'focus') this.focus();
|
||||
};
|
||||
this.subscriptions.push(this.props.room.subscribe(message => {
|
||||
if (!message) this.forceUpdate();
|
||||
else this.receive(message);
|
||||
this.subscriptions.push(this.props.room.subscribe(args => {
|
||||
if (!args) this.forceUpdate();
|
||||
else this.receiveLine(args);
|
||||
}));
|
||||
if (this.base) {
|
||||
this.props.room.setDimensions(this.base.offsetWidth, this.base.offsetHeight);
|
||||
@@ -117,7 +117,7 @@ class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{room: T}>
|
||||
}
|
||||
this.subscriptions = [];
|
||||
}
|
||||
receive(message: string) {}
|
||||
receiveLine(args: Args) {}
|
||||
/**
|
||||
* PS has "fake select menus", buttons that act like <select> dropdowns.
|
||||
* This function is used by the popups they open to change the button
|
||||
|
||||
Reference in New Issue
Block a user