pokemon-showdown-client/play.pokemonshowdown.com/js/client-rooms.js
Guangcong Luo a10821ab8b
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
Update to ESLint 9 (#2326)
This finally removes the tslint dependency and switches to eslint.

There are a lot of other changes here, too, to bring the codebase up to
server standards. TSLint never had much in the way of indentation
enforcement.

Not very happy about eslint splitting itself up over 6 dependencies,
or its documentation over three websites, nor how poorly documented the
new flat config is, but I mean, eslint's gonna eslint. Customizing
would be even harder if we tried to use Biome or something. They mostly
seem to go full Prettier.

Also here are some changes to our style rules. In particular:

- Curly brackets (for objects etc) now have spaces inside them. Sorry
  for the huge change. ESLint doesn't support our old style, and most
  projects use Prettier style, so we might as well match them in this way.
  See https://github.com/eslint-stylistic/eslint-stylistic/issues/415

- String + number concatenation is no longer allowed (except in ES3
  code). We otherwise now consistently use template strings for this.
2025-02-25 20:05:32 -08:00

333 lines
13 KiB
JavaScript

(function ($) {
this.RoomsRoom = Room.extend({
minWidth: 320,
maxWidth: 1024,
type: 'rooms',
title: 'Rooms',
events: {
'change select[name=sections]': 'refresh'
},
isSideRoom: true,
initialize: function () {
this.focusedSection = '';
this.$el.addClass('ps-room-light').addClass('scrollable');
var buf = '<div class="pad"><button class="button" style="float:right;font-size:10pt;margin-top:3px" name="closeHide"><i class="fa fa-caret-right"></i> Hide</button>';
buf += '<div class="roomlisttop"></div><p><select name="sections" class="button"><option value="all">(All rooms)</option></select></p>';
buf += '<div class="roomlist"><p><em style="font-size:20pt">Loading...</em></p></div><div class="roomlist"></div>';
buf += '<p><button name="toggleMoreRooms" class="button">Show more rooms</button><p>';
buf += '<p><button name="joinRoomPopup" class="button">Join other room</button></p></div>';
this.$el.html(buf);
app.on('response:rooms', this.update, this);
var settings = Dex.prefs('serversettings');
if (settings) app.send('/updatesettings ' + JSON.stringify(settings));
app.send('/cmd rooms');
app.user.on('change:named', this.updateUser, this);
this.update();
this.chatroomInterval = setInterval(function () {
if (app.curSideRoom && app.curSideRoom.id === 'rooms') {
app.send('/cmd rooms');
}
}, 20 * 1000);
},
initSectionSelection: function () {
var buf = ['<option value="">(All rooms)</option>'];
var sectionTitles = app.roomsData.sectionTitles;
if (!sectionTitles) {
this.$('select[name=sections]').parent().hide();
return;
}
for (var i = 0; i < sectionTitles.length; i++) {
var sectionName = BattleLog.escapeHTML(sectionTitles[i]);
buf.push('<option value="' + sectionName + '">' + sectionName + '</option>');
}
this.$('select[name=sections]').html(buf.join(''));
},
updateUser: function () {
this.update();
},
focus: function () {
if (new Date().getTime() - this.lastUpdate > 20 * 1000) {
app.send('/cmd rooms');
this.lastUpdate = new Date().getTime();
}
var prevPos = this.$el.scrollTop();
if (!this.$('select:focus').length) {
this.$('button[name=joinRoomPopup]').focus();
}
this.$el.scrollTop(prevPos);
},
joinRoomPopup: function () {
app.addPopupPrompt("Room name:", "Join room", function (room) {
var routeLength = (Config.routes.client + '/').length;
if (room.substr(0, 7) === 'http://') room = room.slice(7);
if (room.substr(0, 8) === 'https://') room = room.slice(8);
if (room.substr(0, routeLength) === Config.routes.client + '/') room = room.slice(routeLength);
if (room.substr(0, 8) === 'psim.us/') room = room.slice(8);
if (room.substr(0, document.location.hostname.length + 1) === document.location.hostname + '/') room = room.slice(document.location.hostname.length + 1);
room = toRoomid(room);
if (!room) return;
app.tryJoinRoom(room);
});
},
toggleMoreRooms: function () {
this.showMoreRooms = !this.showMoreRooms;
this.updateRoomList();
this.$el.find('button[name=toggleMoreRooms]').text(
this.showMoreRooms ? 'Hide more rooms' : 'Show more rooms'
);
},
update: function (rooms) {
if (rooms) {
this.lastUpdate = new Date().getTime();
app.roomsData = rooms;
}
if (!app.roomsData) return;
this.initSectionSelection();
this.updateRoomList();
if (!app.roomsFirstOpen && window.location.host !== 'demo.psim.us') {
if (Config.roomsFirstOpenScript) {
Config.roomsFirstOpenScript();
}
app.roomsFirstOpen = 2;
}
},
renderRoomBtn: function (roomData) {
var id = toID(roomData.title);
var buf = '<div><a href="' + app.root + id + '" class="blocklink"><small style="float:right">(' + Number(roomData.userCount) + ' users)</small><strong><i class="fa fa-comment-o"></i> ' + BattleLog.escapeHTML(roomData.title) + '<br /></strong><small>' + BattleLog.escapeHTML(roomData.desc || '') + '</small></a>';
if (roomData.subRooms && roomData.subRooms.length) {
buf += '<div class="subrooms"><i class="fa fa-level-up fa-rotate-90"></i> Subrooms:';
for (var i = 0; i < roomData.subRooms.length; i++) {
buf += ' <a class="blocklink" href="' + app.root + toID(roomData.subRooms[i]) + '"><i class="fa fa-comment-o"></i> <strong>' + BattleLog.escapeHTML(roomData.subRooms[i]) + '</strong></a>';
}
buf += '</div>';
}
buf += '</div>';
return buf;
},
compareRooms: function (roomA, roomB) {
return roomB.userCount - roomA.userCount;
},
updateRoomList: function () {
var rooms = app.roomsData;
if (rooms.userCount) {
var userCount = Number(rooms.userCount);
var battleCount = Number(rooms.battleCount);
var leftSide = '<button class="button" name="finduser" title="Find an online user"><span class="pixelated usercount" title="Meloetta is PS\'s mascot! The Aria forme is about using its voice, and represents our chatrooms." ></span><strong>' + userCount + '</strong> ' + (userCount === 1 ? 'user' : 'users') + ' online</button> ';
var rightSide = '<button class="button" name="roomlist" title="Watch an active battle"><span class="pixelated battlecount" title="Meloetta is PS\'s mascot! The Pirouette forme is Fighting-type, and represents our battles." ></span><strong>' + battleCount + '</strong> active ' + (battleCount === 1 ? 'battle' : 'battles') + '</button>';
this.$('.roomlisttop').html('<div class="roomcounters">' + leftSide + '</td><td>' + rightSide + '</div>');
}
if (rooms.pspl) {
for (var i = 0; i < rooms.pspl.length; i++) {
rooms.pspl[i].spotlight = "Spotlight rooms";
}
rooms.chat = rooms.pspl.concat(rooms.chat);
rooms.pspl = null;
}
if (rooms.official) {
for (var i = 0; i < rooms.official.length; i++) {
rooms.official[i].section = "Official";
}
rooms.chat = rooms.official.concat(rooms.chat);
rooms.official = null;
}
var allRooms = rooms.chat;
if (this.focusedSection) {
var sectionFilter = this.focusedSection;
allRooms = allRooms.filter(function (roomData) {
return (roomData.section || 'Other') === sectionFilter;
});
}
var spotlightLabel = '';
var spotlightRooms = [];
var officialRooms = [];
var otherRooms = [];
var hiddenRooms = [];
for (var i = 0; i < allRooms.length; i++) {
var roomData = allRooms[i];
if (roomData.spotlight) {
spotlightRooms.push(roomData);
spotlightLabel = roomData.spotlight;
} else if (roomData.section === 'Official') {
officialRooms.push(roomData);
} else if (roomData.privacy === 'hidden') {
hiddenRooms.push(roomData);
} else {
otherRooms.push(roomData);
}
}
this.$('.roomlist').first().html(
(officialRooms.length ?
'<h2 class="rooms-officialchatrooms">Official chat rooms</h2>' + officialRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
) +
(spotlightRooms.length ?
'<h2 class="rooms-psplchatrooms">' + BattleLog.escapeHTML(spotlightLabel) + '</h2>' + spotlightRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
)
);
this.$('.roomlist').last().html(
(otherRooms.length ?
'<h2 class="rooms-chatrooms">Chat rooms</h2>' + otherRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
) +
(hiddenRooms.length && this.showMoreRooms ?
'<h2 class="rooms-chatrooms">Hidden rooms</h2>' + hiddenRooms.sort(this.compareRooms).map(this.renderRoomBtn).join("") : ''
)
);
},
roomlist: function () {
app.joinRoom('battles');
},
closeHide: function () {
app.sideRoom = app.curSideRoom = null;
clearInterval(this.chatroomInterval);
this.chatroomInterval = null;
this.close();
},
finduser: function () {
if (app.isDisconnected) {
app.addPopupMessage("You are offline.");
return;
}
app.addPopupPrompt("Username", "Open", function (target) {
if (!target) return;
if (toID(target) === 'zarel') {
app.addPopup(Popup, { htmlMessage: "Zarel is very busy; please don't contact him this way. If you're looking for help, try <a href=\"/help\">joining the Help room</a>?" });
return;
}
app.addPopup(UserPopup, { name: target });
});
},
refresh: function () {
var section = this.$('select[name=sections]').val();
this.focusedSection = section;
this.updateRoomList();
}
});
this.BattlesRoom = Room.extend({
minWidth: 320,
maxWidth: 1024,
type: 'battles',
title: 'Battles',
isSideRoom: true,
events: {
'change select[name=elofilter]': 'refresh',
'submit .search': 'submitSearch'
},
initialize: function () {
this.$el.addClass('ps-room-light').addClass('scrollable');
var buf = '<div class="pad"><button class="button" style="float:right;font-size:10pt;margin-top:3px" name="close"><i class="fa fa-times"></i> Close</button><div class="roomlist"><p><button class="button" name="refresh"><i class="fa fa-refresh"></i> Refresh</button> <span style="' + Dex.getPokemonIcon('meloetta-pirouette') + ';display:inline-block;vertical-align:middle" class="picon" title="Meloetta is PS\'s mascot! The Pirouette forme is Fighting-type, and represents our battles."></span></p>';
buf += '<p><label class="label">Format:</label><button class="select formatselect" name="selectFormat">(All formats)</button></p>';
buf += '<label>Minimum Elo: <select name="elofilter" class="button"><option value="none">None</option><option value="1000">1000</option><option value="1100">1100</option><option value="1300">1300</option><option value="1500">1500</option><option value="1700">1700</option><option value="1900">1900</option></select></label>';
buf += '<p><form class="search"><input type="text" name="prefixsearch" class="textbox" value="' + BattleLog.escapeHTML(this.usernamePrefix) + '" placeholder="username prefix"/><button type="submit" class="button">Search</button></form></p>';
buf += '<div class="list"><p>Loading...</p></div>';
buf += '</div></div>';
this.$el.html(buf);
this.$list = this.$('.list');
this.$refreshButton = this.$('button[name=refresh]');
this.format = '';
app.on('response:roomlist', this.update, this);
app.send('/cmd roomlist');
this.update();
},
selectFormat: function (format, button) {
if (!window.BattleFormats) {
return;
}
var self = this;
app.addPopup(FormatPopup, { format: format, sourceEl: button, selectType: 'watch', onselect: function (newFormat) {
self.changeFormat(newFormat);
} });
},
changeFormat: function (format) {
this.format = format;
this.data = null;
this.update();
this.refresh();
},
focus: function (e) {
if (e && $(e.target).is('input')) return;
if (e && $(e.target).closest('select, a').length) return;
if (new Date().getTime() - this.lastUpdate > 60 * 1000) {
this.refresh();
}
var prevPos = this.$el.scrollTop();
this.$('button[name=refresh]').focus();
this.$el.scrollTop(prevPos);
},
rejoin: function () {
this.refresh();
},
renderRoomBtn: function (id, roomData, matches) {
var format = (matches[1] || '');
var formatBuf = '';
if (roomData.minElo) {
formatBuf += '<small style="float:right">(' + (typeof roomData.minElo === 'number' ? 'rated: ' : '') + BattleLog.escapeHTML('' + roomData.minElo) + ')</small>';
}
formatBuf += (format ? '<small>[' + BattleLog.escapeFormat(format) + ']</small><br />' : '');
var roomDesc = formatBuf + '<em class="p1">' + BattleLog.escapeHTML(roomData.p1) + '</em> <small class="vs">vs.</small> <em class="p2">' + BattleLog.escapeHTML(roomData.p2) + '</em>';
if (!roomData.p1) {
matches = id.match(/[^0-9]([0-9]*)$/);
roomDesc = formatBuf + 'empty room ' + matches[1];
} else if (!roomData.p2) {
roomDesc = formatBuf + '<em class="p1">' + BattleLog.escapeHTML(roomData.p1) + '</em>';
}
return '<div><a href="' + app.root + id + '" class="blocklink">' + roomDesc + '</a></div>';
},
submitSearch: function (e) {
e.preventDefault();
this.refresh();
},
update: function (data) {
if (!data && !this.data) {
if (app.isDisconnected) {
this.$list.html('<p>You are offline.</p>');
} else {
this.$list.html('<p>Loading...</p>');
}
return;
}
this.$('button[name=refresh]')[0].disabled = false;
// Synchronize stored room data with incoming data
if (data) this.data = data;
var rooms = this.data.rooms;
var buf = [];
for (var id in rooms) {
var roomData = rooms[id];
var matches = ChatRoom.parseBattleID(id);
// bogus room ID could be used to inject JavaScript
if (!matches || this.format && matches[1] !== this.format) {
continue;
}
buf.push(this.renderRoomBtn(id, roomData, matches));
}
if (!buf.length) return this.$list.html('<p>No ' + BattleLog.escapeFormat(this.format) + ' battles are going on right now.</p>');
return this.$list.html('<p>' + buf.length + (buf.length === 100 ? '+' : '') + ' ' + BattleLog.escapeFormat(this.format) + ' ' + (buf.length === 1 ? 'battle' : 'battles') + '</p>' + buf.join(""));
},
refresh: function () {
var usernamePrefix = this.$('input[name=prefixsearch]').val();
var elofilter = this.$('select[name=elofilter]').val();
var searchParams = [this.format, elofilter, toID(usernamePrefix)];
app.send('/cmd roomlist ' + searchParams.join(','));
this.lastUpdate = new Date().getTime();
// Prevent further refreshes until we get a response.
this.$refreshButton[0].disabled = true;
}
});
}).call(this, jQuery);