diff --git a/src/panel-mainmenu.tsx b/src/panel-mainmenu.tsx
index f5de49428..356e14f46 100644
--- a/src/panel-mainmenu.tsx
+++ b/src/panel-mainmenu.tsx
@@ -5,7 +5,7 @@
* @license AGPLv3
*/
-type RoomInfo = {title: string, desc: string, userCount: number, subRooms?: string[]};
+type RoomInfo = {title: string, desc?: string, userCount?: number, subRooms?: string[]};
class MainMenuRoom extends PSRoom {
readonly classType: string = 'mainmenu';
diff --git a/src/panel-rooms.tsx b/src/panel-rooms.tsx
index b706cac62..a2808dacf 100644
--- a/src/panel-rooms.tsx
+++ b/src/panel-rooms.tsx
@@ -15,6 +15,8 @@ class RoomsRoom extends PSRoom {
class RoomsPanel extends PSRoomPanel {
hidden = false;
+ search = '';
+ lastKeyCode = 0;
componentDidMount() {
super.componentDidMount();
this.subscriptions.push(PS.user.subscribe(() => {
@@ -28,12 +30,107 @@ class RoomsPanel extends PSRoomPanel {
this.forceUpdate();
PS.update();
};
+ changeSearch = (e: Event) => {
+ const target = (e.currentTarget as HTMLInputElement);
+ if (target.selectionStart !== target.selectionEnd) return;
+ this.search = target.value;
+ this.forceUpdate();
+ };
+ keyDownSearch = (e: KeyboardEvent) => {
+ this.lastKeyCode = e.keyCode;
+ if (e.keyCode === 13) {
+ const target = (e.currentTarget as HTMLInputElement);
+ let value = target.value;
+ const arrowIndex = value.indexOf(' \u21d2 ');
+ if (arrowIndex >= 0) value = value.slice(arrowIndex + 3);
+ if (!/^[a-z0-9-]$/.test(value)) value = toId(value);
+
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ target.value = '';
+
+ PS.join(value as RoomID);
+ }
+ };
+ runSearch() {
+ const searchid = toId(this.search);
+ let exactMatch = false;
+
+ const rooms = PS.mainmenu.roomsCache;
+ let roomList = [...(rooms.official || []), ...(rooms.pspl || []), ...(rooms.chat || [])];
+ for (const room of roomList) {
+ if (!room.subRooms) continue;
+ for (const title of room.subRooms) {
+ roomList.push({
+ title,
+ desc: `Subroom of ${room.title}`,
+ });
+ }
+ }
+
+ let start = roomList.filter(room => {
+ const titleid = toId(room.title);
+ if (titleid === searchid) exactMatch = true;
+ return titleid.startsWith(searchid) ||
+ toId(room.title.replace(/^The /, '')).startsWith(searchid);
+ });
+ roomList = roomList.filter(room => !start.includes(room));
+
+ let abbr = roomList.filter(room =>
+ toId(room.title.toLowerCase().replace(/\b([a-z0-9])[a-z0-9]*\b/g, '$1')).startsWith(searchid) ||
+ room.title.replace(/[^A-Z0-9]+/g, '').toLowerCase().startsWith(searchid)
+ );
+
+ const hidden = !exactMatch ? [{title: this.search, desc: "(Private room?)"}] : [];
+
+ const autoFill = this.lastKeyCode !== 127 && this.lastKeyCode >= 32;
+ if (autoFill) {
+ const firstTitle = (start[0] || abbr[0] || hidden[0]).title;
+ let firstTitleOffset = 0;
+ while (
+ searchid !== toId(firstTitle.slice(0, firstTitleOffset)) &&
+ firstTitleOffset < firstTitle.length // should never happen, but sanity against infinite loop
+ ) {
+ firstTitleOffset++;
+ }
+ let autoFillValue = firstTitle.slice(firstTitleOffset);
+ if (!autoFillValue && toId(firstTitle) !== searchid) {
+ autoFillValue = ' \u21d2 ' + firstTitle;
+ }
+ const oldSearch = this.search;
+ const searchElem = this.base!.querySelector('input[type=search]') as HTMLInputElement;
+ searchElem.value = oldSearch + autoFillValue;
+ searchElem.setSelectionRange(oldSearch.length, oldSearch.length + autoFillValue.length);
+ }
+
+ return {start, abbr, hidden};
+ }
+ focus() {
+ (this.base!.querySelector('input[type=search]') as HTMLInputElement).focus();
+ }
render() {
if (this.hidden && PS.isVisible(this.props.room)) this.hidden = false;
if (this.hidden) {
return Connecting...
}
- {this.renderRoomList("Official chat rooms", rooms.official)}
- {this.renderRoomList("PSPL winner", rooms.pspl)}
- {this.renderRoomList("Chat rooms", rooms.chat)}
+ {roomList}