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:
Guangcong Luo
2020-03-19 21:20:55 -07:00
parent 71495d7faf
commit d75ef3c6ae
6 changed files with 146 additions and 89 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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