/** * The UI element which displays the list of Users who are currently playing within the active World. * @extends {Application} */ class PlayerList extends Application { constructor(options) { super(options); game.users.apps.push(this); /** * An internal toggle for whether to show offline players or hide them * @type {boolean} * @private */ this._showOffline = false; } /* -------------------------------------------- */ /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { id: "players", template: "templates/user/players.html", popOut: false }); } /* -------------------------------------------- */ /* Application Rendering */ /* -------------------------------------------- */ /** * Whether the players list is in a configuration where it is hidden. * @returns {boolean} */ get isHidden() { if ( game.webrtc.mode === AVSettings.AV_MODES.DISABLED ) return false; const { client, verticalDock } = game.webrtc.settings; return verticalDock && client.hidePlayerList && !client.hideDock && !ui.webrtc.hidden; } /* -------------------------------------------- */ /** @override */ render(force, context={}) { this._positionInDOM(); const { renderContext, renderData } = context; if ( renderContext ) { const events = ["createUser", "updateUser", "deleteUser"]; if ( !events.includes(renderContext) ) return this; if ( renderContext === "updateUser" ) { const updateKeys = ["name", "pronouns", "ownership", "ownership.default", "active", "navigation"]; if ( !renderData.some(d => updateKeys.some(k => k in d)) ) return this; } } return super.render(force, context); } /* -------------------------------------------- */ /** @override */ getData(options={}) { // Process user data by adding extra characteristics const users = game.users.filter(u => this._showOffline || u.active).map(user => { const u = user.toObject(false); u.active = user.active; u.isGM = user.isGM; u.isSelf = user.isSelf; u.charname = user.character?.name.split(" ")[0] || ""; u.color = u.active ? u.color.css : "#333333"; u.border = u.active ? user.border.css : "#000000"; u.displayName = this._getDisplayName(u); return u; }).sort((a, b) => { if ( (b.role >= CONST.USER_ROLES.ASSISTANT) && (b.role > a.role) ) return 1; return a.name.localeCompare(b.name, game.i18n.lang); }); // Return the data for rendering return { users, hide: this.isHidden, showOffline: this._showOffline }; } /* -------------------------------------------- */ /** * Prepare a displayed name string for the User which includes their name, pronouns, character, or GM tag. * @returns {string} * @protected */ _getDisplayName(user) { const displayNamePart = [user.name]; if ( user.pronouns ) displayNamePart.push(`(${user.pronouns})`); if ( user.isGM ) displayNamePart.push(`[${game.i18n.localize("USER.GM")}]`); else if ( user.charname ) displayNamePart.push(`[${user.charname}]`); return displayNamePart.join(" "); } /* -------------------------------------------- */ /** * Position this Application in the main DOM appropriately. * @protected */ _positionInDOM() { document.body.classList.toggle("players-hidden", this.isHidden); if ( (game.webrtc.mode === AVSettings.AV_MODES.DISABLED) || this.isHidden || !this.element.length ) return; const element = this.element[0]; const cameraViews = ui.webrtc.element[0]; const uiTop = document.getElementById("ui-top"); const uiLeft = document.getElementById("ui-left"); const { client, verticalDock } = game.webrtc.settings; const inDock = verticalDock && !client.hideDock && !ui.webrtc.hidden; if ( inDock && !cameraViews?.contains(element) ) { cameraViews.appendChild(element); uiTop.classList.remove("offset"); } else if ( !inDock && !uiLeft.contains(element) ) { uiLeft.appendChild(element); uiTop.classList.add("offset"); } } /* -------------------------------------------- */ /* Event Listeners and Handlers /* -------------------------------------------- */ /** @override */ activateListeners(html) { // Toggle online/offline html.find("h3").click(this._onToggleOfflinePlayers.bind(this)); // Context menu const contextOptions = this._getUserContextOptions(); Hooks.call("getUserContextOptions", html, contextOptions); new ContextMenu(html, ".player", contextOptions); } /* -------------------------------------------- */ /** * Return the default context options available for the Players application * @returns {object[]} * @private */ _getUserContextOptions() { return [ { name: game.i18n.localize("PLAYERS.ConfigTitle"), icon: '', condition: li => game.user.isGM || (li[0].dataset.userId === game.user.id), callback: li => { const user = game.users.get(li[0].dataset.userId); user?.sheet.render(true); } }, { name: game.i18n.localize("PLAYERS.ViewAvatar"), icon: '', condition: li => { const user = game.users.get(li[0].dataset.userId); return user.avatar !== CONST.DEFAULT_TOKEN; }, callback: li => { let user = game.users.get(li.data("user-id")); new ImagePopout(user.avatar, { title: user.name, uuid: user.uuid }).render(true); } }, { name: game.i18n.localize("PLAYERS.PullToScene"), icon: '', condition: li => game.user.isGM && (li[0].dataset.userId !== game.user.id), callback: li => game.socket.emit("pullToScene", canvas.scene.id, li.data("user-id")) }, { name: game.i18n.localize("PLAYERS.Kick"), icon: '', condition: li => { const user = game.users.get(li[0].dataset.userId); return game.user.isGM && user.active && !user.isSelf; }, callback: li => { const user = game.users.get(li[0].dataset.userId); return this.#kickUser(user); } }, { name: game.i18n.localize("PLAYERS.Ban"), icon: '', condition: li => { const user = game.users.get(li[0].dataset.userId); return game.user.isGM && !user.isSelf && (user.role !== CONST.USER_ROLES.NONE); }, callback: li => { const user = game.users.get(li[0].dataset.userId); return this.#banUser(user); } }, { name: game.i18n.localize("PLAYERS.UnBan"), icon: '', condition: li => { const user = game.users.get(li[0].dataset.userId); return game.user.isGM && !user.isSelf && (user.role === CONST.USER_ROLES.NONE); }, callback: li => { const user = game.users.get(li[0].dataset.userId); return this.#unbanUser(user); } }, { name: game.i18n.localize("WEBRTC.TooltipShowUser"), icon: '', condition: li => { const userId = li.data("userId"); return game.webrtc.settings.client.users[userId]?.blocked; }, callback: async li => { const userId = li.data("userId"); await game.webrtc.settings.set("client", `users.${userId}.blocked`, false); ui.webrtc.render(); } } ]; } /* -------------------------------------------- */ /** * Toggle display of the Players hud setting for whether to display offline players * @param {Event} event The originating click event * @private */ _onToggleOfflinePlayers(event) { event.preventDefault(); this._showOffline = !this._showOffline; this.render(); } /* -------------------------------------------- */ /** * Temporarily remove a User from the World by banning and then un-banning them. * @param {User} user The User to kick * @returns {Promise} */ async #kickUser(user) { const role = user.role; await user.update({role: CONST.USER_ROLES.NONE}); await user.update({role}, {diff: false}); ui.notifications.info(`${user.name} has been kicked from the World.`); } /* -------------------------------------------- */ /** * Ban a User by changing their role to "NONE". * @param {User} user The User to ban * @returns {Promise} */ async #banUser(user) { if ( user.role === CONST.USER_ROLES.NONE ) return; await user.update({role: CONST.USER_ROLES.NONE}); ui.notifications.info(`${user.name} has been banned from the World.`); } /* -------------------------------------------- */ /** * Unban a User by changing their role to "PLAYER". * @param {User} user The User to unban * @returns {Promise} */ async #unbanUser(user) { if ( user.role !== CONST.USER_ROLES.NONE ) return; await user.update({role: CONST.USER_ROLES.PLAYER}); ui.notifications.info(`${user.name} has been unbanned from the World.`); } }