|
@@ -23,10 +23,11 @@ import tpl_muc_password_form from "templates/muc_password_form.js";
|
|
|
import tpl_muc_sidebar from "templates/muc_sidebar.js";
|
|
|
import tpl_room_panel from "templates/room_panel.html";
|
|
|
import tpl_spinner from "templates/spinner.html";
|
|
|
+import { ChatBoxView } from "./converse-chatview";
|
|
|
import { Model } from '@converse/skeletor/src/model.js';
|
|
|
import { View } from '@converse/skeletor/src/view.js';
|
|
|
import { __ } from '@converse/headless/i18n';
|
|
|
-import { api, converse } from "@converse/headless/converse-core";
|
|
|
+import { _converse, api, converse } from "@converse/headless/converse-core";
|
|
|
import { debounce, isString, isUndefined } from "lodash-es";
|
|
|
import { render } from "lit-html";
|
|
|
|
|
@@ -53,1421 +54,1426 @@ const COMMAND_TO_AFFILIATION = {
|
|
|
'revoke': 'none'
|
|
|
}
|
|
|
|
|
|
-converse.plugins.add('converse-muc-views', {
|
|
|
- /* Dependencies are other plugins which might be
|
|
|
- * overridden or relied upon, and therefore need to be loaded before
|
|
|
- * this plugin. They are "optional" because they might not be
|
|
|
- * available, in which case any overrides applicable to them will be
|
|
|
- * ignored.
|
|
|
- *
|
|
|
- * NB: These plugins need to have already been loaded via require.js.
|
|
|
- *
|
|
|
- * It's possible to make these dependencies "non-optional".
|
|
|
- * If the setting "strict_plugin_dependencies" is set to true,
|
|
|
- * an error will be raised if the plugin is not found.
|
|
|
- */
|
|
|
- dependencies: ["converse-autocomplete", "converse-modal", "converse-controlbox", "converse-chatview"],
|
|
|
|
|
|
- overrides: {
|
|
|
- ControlBoxView: {
|
|
|
- renderControlBoxPane () {
|
|
|
- this.__super__.renderControlBoxPane.apply(this, arguments);
|
|
|
- if (api.settings.get('allow_muc')) {
|
|
|
- this.renderRoomsPanel();
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+/**
|
|
|
+ * NativeView which renders a groupchat, based upon
|
|
|
+ * { @link _converse.ChatBoxView } for normal one-on-one chat boxes.
|
|
|
+ * @class
|
|
|
+ * @namespace _converse.ChatRoomView
|
|
|
+ * @memberOf _converse
|
|
|
+ */
|
|
|
+export const ChatRoomView = ChatBoxView.extend({
|
|
|
+ length: 300,
|
|
|
+ tagName: 'div',
|
|
|
+ className: 'chatbox chatroom hidden',
|
|
|
+ is_chatroom: true,
|
|
|
+ events: {
|
|
|
+ 'click .chatbox-navback': 'showControlBox',
|
|
|
+ 'click .hide-occupants': 'hideOccupants',
|
|
|
+ 'click .new-msgs-indicator': 'viewUnreadMessages',
|
|
|
+ // Arrow functions don't work here because you can't bind a different `this` param to them.
|
|
|
+ 'click .occupant-nick': function (ev) {this.insertIntoTextArea(ev.target.textContent) },
|
|
|
+ 'click .send-button': 'onFormSubmitted',
|
|
|
+ 'dragover .chat-textarea': 'onDragOver',
|
|
|
+ 'drop .chat-textarea': 'onDrop',
|
|
|
+ 'input .chat-textarea': 'inputChanged',
|
|
|
+ 'keydown .chat-textarea': 'onKeyDown',
|
|
|
+ 'keyup .chat-textarea': 'onKeyUp',
|
|
|
+ 'mousedown .dragresize-occupants-left': 'onStartResizeOccupants',
|
|
|
+ 'paste .chat-textarea': 'onPaste',
|
|
|
+ 'submit .muc-nickname-form': 'submitNickname',
|
|
|
},
|
|
|
|
|
|
- initialize () {
|
|
|
- const { _converse } = this;
|
|
|
-
|
|
|
- api.promises.add(['roomsPanelRendered']);
|
|
|
-
|
|
|
- // Configuration values for this plugin
|
|
|
- // ====================================
|
|
|
- // Refer to docs/source/configuration.rst for explanations of these
|
|
|
- // configuration settings.
|
|
|
- api.settings.extend({
|
|
|
- 'auto_list_rooms': false,
|
|
|
- 'cache_muc_messages': true,
|
|
|
- 'locked_muc_nickname': false,
|
|
|
- 'modtools_disable_query': [],
|
|
|
- 'modtools_disable_assign': false,
|
|
|
- 'muc_disable_slash_commands': false,
|
|
|
- 'muc_mention_autocomplete_filter': 'contains',
|
|
|
- 'muc_mention_autocomplete_min_chars': 0,
|
|
|
- 'muc_mention_autocomplete_show_avatar': true,
|
|
|
- 'muc_roomid_policy': null,
|
|
|
- 'muc_roomid_policy_hint': null,
|
|
|
- 'roomconfig_whitelist': [],
|
|
|
- 'show_retraction_warning': true,
|
|
|
- 'visible_toolbar_buttons': {
|
|
|
- 'toggle_occupants': true
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
+ async initialize () {
|
|
|
+ this.initDebounced();
|
|
|
+
|
|
|
+ this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
|
|
|
+ this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
|
|
|
+ this.listenTo(this.model, 'change:hidden_occupants', this.renderToolbar);
|
|
|
+ this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
|
|
+ this.listenTo(this.model, 'destroy', this.hide);
|
|
|
+ this.listenTo(this.model, 'show', this.show);
|
|
|
+ this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
|
|
|
+ this.listenTo(this.model.features, 'change:open', this.renderHeading);
|
|
|
+ this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown);
|
|
|
+ this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
|
|
+
|
|
|
+ // Bind so that we can pass it to addEventListener and removeEventListener
|
|
|
+ this.onMouseMove = this.onMouseMove.bind(this);
|
|
|
+ this.onMouseUp = this.onMouseUp.bind(this);
|
|
|
+
|
|
|
+ await this.render();
|
|
|
+
|
|
|
+ // Need to be registered after render has been called.
|
|
|
+ this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
|
|
+ this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
|
|
+ this.listenTo(this.model.messages, 'change', this.renderChatHistory);
|
|
|
+ this.listenTo(this.model.messages, 'remove', this.renderChatHistory);
|
|
|
+ this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
|
|
|
+ this.listenTo(this.model.notifications, 'change', this.renderNotifications);
|
|
|
+
|
|
|
+ this.model.occupants.forEach(o => this.onOccupantAdded(o));
|
|
|
+ this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
|
|
|
+ this.listenTo(this.model.occupants, 'change', this.renderChatHistory);
|
|
|
+ this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
|
|
|
+ this.listenTo(this.model.occupants, 'change:role', this.onOccupantRoleChanged);
|
|
|
+ this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
|
|
|
+ this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
|
|
|
+
|
|
|
+ this.createSidebarView();
|
|
|
+ await this.updateAfterMessagesFetched();
|
|
|
+
|
|
|
+ // Register later due to await
|
|
|
+ const user_settings = await _converse.api.user.settings.getModel();
|
|
|
+ this.listenTo(user_settings, 'change:mucs_with_hidden_subject', this.renderHeading);
|
|
|
+
|
|
|
+ this.onConnectionStatusChanged();
|
|
|
+ this.model.maybeShow();
|
|
|
|
|
|
- const viewWithRoomsPanel = {
|
|
|
- renderRoomsPanel () {
|
|
|
- if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
|
|
|
- return this.roomspanel;
|
|
|
- }
|
|
|
- const id = `converse.roomspanel${_converse.bare_jid}`;
|
|
|
+ /**
|
|
|
+ * Triggered once a { @link _converse.ChatRoomView } has been opened
|
|
|
+ * @event _converse#chatRoomViewInitialized
|
|
|
+ * @type { _converse.ChatRoomView }
|
|
|
+ * @example _converse.api.listen.on('chatRoomViewInitialized', view => { ... });
|
|
|
+ */
|
|
|
+ api.trigger('chatRoomViewInitialized', this);
|
|
|
+ },
|
|
|
|
|
|
- this.roomspanel = new _converse.RoomsPanel({
|
|
|
- 'model': new (_converse.RoomsPanelModel.extend({
|
|
|
- id,
|
|
|
- 'browserStorage': _converse.createStore(id)
|
|
|
- }))()
|
|
|
- });
|
|
|
- this.roomspanel.model.fetch();
|
|
|
- this.el.querySelector('.controlbox-pane').insertAdjacentElement(
|
|
|
- 'beforeEnd', this.roomspanel.render().el);
|
|
|
+ async render () {
|
|
|
+ this.el.setAttribute('id', this.model.get('box_id'));
|
|
|
+ render(tpl_chatroom({
|
|
|
+ 'markScrolled': ev => this.markScrolled(ev),
|
|
|
+ 'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
|
|
|
+ 'show_send_button': _converse.show_send_button,
|
|
|
+ }), this.el);
|
|
|
+
|
|
|
+ this.notifications = this.el.querySelector('.chat-content__notifications');
|
|
|
+ this.content = this.el.querySelector('.chat-content');
|
|
|
+ this.msgs_container = this.el.querySelector('.chat-content__messages');
|
|
|
+ this.help_container = this.el.querySelector('.chat-content__help');
|
|
|
+
|
|
|
+ this.renderBottomPanel();
|
|
|
+ if (!api.settings.get('muc_show_logs_before_join') &&
|
|
|
+ this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
|
|
|
+ this.showSpinner();
|
|
|
+ }
|
|
|
+ // Render header as late as possible since it's async and we
|
|
|
+ // want the rest of the DOM elements to be available ASAP.
|
|
|
+ // Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
|
|
|
+ await this.renderHeading();
|
|
|
+ !this.model.get('hidden') && this.show();
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Triggered once the section of the { @link _converse.ControlBoxView }
|
|
|
- * which shows gropuchats has been rendered.
|
|
|
- * @event _converse#roomsPanelRendered
|
|
|
- * @example _converse.api.listen.on('roomsPanelRendered', () => { ... });
|
|
|
- */
|
|
|
- api.trigger('roomsPanelRendered');
|
|
|
- return this.roomspanel;
|
|
|
- },
|
|
|
+ getNotifications () {
|
|
|
+ const actors_per_state = this.model.notifications.toJSON();
|
|
|
+ const states = api.settings.get('muc_show_join_leave') ?
|
|
|
+ [...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES, ...converse.MUC_ROLE_CHANGES] :
|
|
|
+ converse.CHAT_STATES;
|
|
|
|
|
|
- getRoomsPanel () {
|
|
|
- if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
|
|
|
- return this.roomspanel;
|
|
|
+ return states.reduce((result, state) => {
|
|
|
+ const existing_actors = actors_per_state[state];
|
|
|
+ if (!(existing_actors?.length)) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ const actors = existing_actors.map(a => this.model.getOccupant(a)?.getDisplayName() || a);
|
|
|
+ if (actors.length === 1) {
|
|
|
+ if (state === 'composing') {
|
|
|
+ return `${result}${__('%1$s is typing', actors[0])}\n`;
|
|
|
+ } else if (state === 'paused') {
|
|
|
+ return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
|
|
|
+ } else if (state === _converse.GONE) {
|
|
|
+ return `${result}${__('%1$s has gone away', actors[0])}\n`;
|
|
|
+ } else if (state === 'entered') {
|
|
|
+ return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
|
|
|
+ } else if (state === 'exited') {
|
|
|
+ return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
|
|
|
+ } else if (state === 'op') {
|
|
|
+ return `${result}${__("%1$s is now a moderator", actors[0])}\n`;
|
|
|
+ } else if (state === 'deop') {
|
|
|
+ return `${result}${__("%1$s is no longer a moderator", actors[0])}\n`;
|
|
|
+ } else if (state === 'voice') {
|
|
|
+ return `${result}${__("%1$s has been given a voice", actors[0])}\n`;
|
|
|
+ } else if (state === 'mute') {
|
|
|
+ return `${result}${__("%1$s has been muted", actors[0])}\n`;
|
|
|
+ }
|
|
|
+ } else if (actors.length > 1) {
|
|
|
+ let actors_str;
|
|
|
+ if (actors.length > 3) {
|
|
|
+ actors_str = `${Array.from(actors).slice(0, 2).join(', ')} and others`;
|
|
|
} else {
|
|
|
- return this.renderRoomsPanel();
|
|
|
+ const last_actor = actors.pop();
|
|
|
+ actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (state === 'composing') {
|
|
|
+ return `${result}${__('%1$s are typing', actors_str)}\n`;
|
|
|
+ } else if (state === 'paused') {
|
|
|
+ return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
|
|
|
+ } else if (state === _converse.GONE) {
|
|
|
+ return `${result}${__('%1$s have gone away', actors_str)}\n`;
|
|
|
+ } else if (state === 'entered') {
|
|
|
+ return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
|
|
|
+ } else if (state === 'exited') {
|
|
|
+ return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
|
|
|
+ } else if (state === 'op') {
|
|
|
+ return `${result}${__("%1$s are now moderators", actors[0])}\n`;
|
|
|
+ } else if (state === 'deop') {
|
|
|
+ return `${result}${__("%1$s are no longer moderators", actors[0])}\n`;
|
|
|
+ } else if (state === 'voice') {
|
|
|
+ return `${result}${__("%1$s have been given voices", actors[0])}\n`;
|
|
|
+ } else if (state === 'mute') {
|
|
|
+ return `${result}${__("%1$s have been muted", actors[0])}\n`;
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
+ return result;
|
|
|
+ }, '');
|
|
|
+ },
|
|
|
|
|
|
- if (_converse.ControlBoxView) {
|
|
|
- Object.assign(_converse.ControlBoxView.prototype, viewWithRoomsPanel);
|
|
|
- }
|
|
|
+ getHelpMessages () {
|
|
|
+ const setting = api.settings.get("muc_disable_slash_commands");
|
|
|
+ const disabled_commands = Array.isArray(setting) ? setting : [];
|
|
|
+ return [
|
|
|
+ `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
|
|
|
+ `<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
|
|
|
+ `<strong>/clear</strong>: ${__('Clear the chat area')}`,
|
|
|
+ `<strong>/close</strong>: ${__('Close this groupchat')}`,
|
|
|
+ `<strong>/deop</strong>: ${__('Change user role to participant')}`,
|
|
|
+ `<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
|
|
|
+ `<strong>/help</strong>: ${__('Show this menu')}`,
|
|
|
+ `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
|
|
|
+ `<strong>/me</strong>: ${__('Write in 3rd person')}`,
|
|
|
+ `<strong>/member</strong>: ${__('Grant membership to a user')}`,
|
|
|
+ `<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
|
|
|
+ `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
|
|
|
+ `<strong>/nick</strong>: ${__('Change your nickname')}`,
|
|
|
+ `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
|
|
|
+ `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
|
|
|
+ `<strong>/register</strong>: ${__("Register your nickname")}`,
|
|
|
+ `<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
|
|
|
+ `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
|
|
|
+ `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
|
|
|
+ `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
|
|
|
+ ].filter(line => disabled_commands.every(c => (!line.startsWith(c+'<', 9))))
|
|
|
+ .filter(line => this.getAllowedCommands().some(c => line.startsWith(c+'<', 9)));
|
|
|
+ },
|
|
|
|
|
|
+ /**
|
|
|
+ * Renders the MUC heading if any relevant attributes have changed.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#renderHeading
|
|
|
+ * @param { _converse.ChatRoom } [item]
|
|
|
+ */
|
|
|
+ async renderHeading () {
|
|
|
+ const tpl = await this.generateHeadingTemplate();
|
|
|
+ render(tpl, this.el.querySelector('.chat-head-chatroom'));
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * NativeView which renders a groupchat, based upon
|
|
|
- * { @link _converse.ChatBoxView } for normal one-on-one chat boxes.
|
|
|
- * @class
|
|
|
- * @namespace _converse.ChatRoomView
|
|
|
- * @memberOf _converse
|
|
|
- */
|
|
|
- _converse.ChatRoomView = _converse.ChatBoxView.extend({
|
|
|
- length: 300,
|
|
|
- tagName: 'div',
|
|
|
- className: 'chatbox chatroom hidden',
|
|
|
- is_chatroom: true,
|
|
|
- events: {
|
|
|
- 'click .chatbox-navback': 'showControlBox',
|
|
|
- 'click .hide-occupants': 'hideOccupants',
|
|
|
- 'click .new-msgs-indicator': 'viewUnreadMessages',
|
|
|
- // Arrow functions don't work here because you can't bind a different `this` param to them.
|
|
|
- 'click .occupant-nick': function (ev) {this.insertIntoTextArea(ev.target.textContent) },
|
|
|
- 'click .send-button': 'onFormSubmitted',
|
|
|
- 'dragover .chat-textarea': 'onDragOver',
|
|
|
- 'drop .chat-textarea': 'onDrop',
|
|
|
- 'input .chat-textarea': 'inputChanged',
|
|
|
- 'keydown .chat-textarea': 'onKeyDown',
|
|
|
- 'keyup .chat-textarea': 'onKeyUp',
|
|
|
- 'mousedown .dragresize-occupants-left': 'onStartResizeOccupants',
|
|
|
- 'paste .chat-textarea': 'onPaste',
|
|
|
- 'submit .muc-nickname-form': 'submitNickname',
|
|
|
- },
|
|
|
|
|
|
- async initialize () {
|
|
|
- this.initDebounced();
|
|
|
-
|
|
|
- this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
|
|
|
- this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
|
|
|
- this.listenTo(this.model, 'change:hidden_occupants', this.renderToolbar);
|
|
|
- this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
|
|
|
- this.listenTo(this.model, 'destroy', this.hide);
|
|
|
- this.listenTo(this.model, 'show', this.show);
|
|
|
- this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
|
|
|
- this.listenTo(this.model.features, 'change:open', this.renderHeading);
|
|
|
- this.listenTo(this.model.messages, 'rendered', this.maybeScrollDown);
|
|
|
- this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
|
|
|
-
|
|
|
- // Bind so that we can pass it to addEventListener and removeEventListener
|
|
|
- this.onMouseMove = this.onMouseMove.bind(this);
|
|
|
- this.onMouseUp = this.onMouseUp.bind(this);
|
|
|
-
|
|
|
- await this.render();
|
|
|
-
|
|
|
- // Need to be registered after render has been called.
|
|
|
- this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
|
|
- this.listenTo(this.model.messages, 'add', this.onMessageAdded);
|
|
|
- this.listenTo(this.model.messages, 'change', this.renderChatHistory);
|
|
|
- this.listenTo(this.model.messages, 'remove', this.renderChatHistory);
|
|
|
- this.listenTo(this.model.messages, 'reset', this.renderChatHistory);
|
|
|
- this.listenTo(this.model.notifications, 'change', this.renderNotifications);
|
|
|
-
|
|
|
- this.model.occupants.forEach(o => this.onOccupantAdded(o));
|
|
|
- this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
|
|
|
- this.listenTo(this.model.occupants, 'change', this.renderChatHistory);
|
|
|
- this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
|
|
|
- this.listenTo(this.model.occupants, 'change:role', this.onOccupantRoleChanged);
|
|
|
- this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
|
|
|
- this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
|
|
|
-
|
|
|
- this.createSidebarView();
|
|
|
- await this.updateAfterMessagesFetched();
|
|
|
-
|
|
|
- // Register later due to await
|
|
|
- const user_settings = await _converse.api.user.settings.getModel();
|
|
|
- this.listenTo(user_settings, 'change:mucs_with_hidden_subject', this.renderHeading);
|
|
|
-
|
|
|
- this.onConnectionStatusChanged();
|
|
|
- this.model.maybeShow();
|
|
|
+ renderBottomPanel () {
|
|
|
+ const container = this.el.querySelector('.bottom-panel');
|
|
|
+ const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
|
|
|
+ const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
|
|
|
+ container.innerHTML = tpl_chatroom_bottom_panel({__, can_edit, entered});
|
|
|
+ if (entered && can_edit) {
|
|
|
+ this.renderMessageForm();
|
|
|
+ this.initMentionAutoComplete();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Triggered once a { @link _converse.ChatRoomView } has been opened
|
|
|
- * @event _converse#chatRoomViewInitialized
|
|
|
- * @type { _converse.ChatRoomView }
|
|
|
- * @example _converse.api.listen.on('chatRoomViewInitialized', view => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatRoomViewInitialized', this);
|
|
|
- },
|
|
|
+ createSidebarView () {
|
|
|
+ this.model.occupants.chatroomview = this;
|
|
|
+ this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
|
|
|
+ const container_el = this.el.querySelector('.chatroom-body');
|
|
|
+ const occupants_width = this.model.get('occupants_width');
|
|
|
+ if (this.sidebar_view && occupants_width !== undefined) {
|
|
|
+ this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
|
|
|
+ }
|
|
|
+ container_el.insertAdjacentElement('beforeend', this.sidebar_view.el);
|
|
|
+ },
|
|
|
|
|
|
- async render () {
|
|
|
- this.el.setAttribute('id', this.model.get('box_id'));
|
|
|
- render(tpl_chatroom({
|
|
|
- 'markScrolled': ev => this.markScrolled(ev),
|
|
|
- 'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
|
|
|
- 'show_send_button': _converse.show_send_button,
|
|
|
- }), this.el);
|
|
|
-
|
|
|
- this.notifications = this.el.querySelector('.chat-content__notifications');
|
|
|
- this.content = this.el.querySelector('.chat-content');
|
|
|
- this.msgs_container = this.el.querySelector('.chat-content__messages');
|
|
|
- this.help_container = this.el.querySelector('.chat-content__help');
|
|
|
-
|
|
|
- this.renderBottomPanel();
|
|
|
- if (!api.settings.get('muc_show_logs_before_join') &&
|
|
|
- this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
|
|
|
- this.showSpinner();
|
|
|
- }
|
|
|
- // Render header as late as possible since it's async and we
|
|
|
- // want the rest of the DOM elements to be available ASAP.
|
|
|
- // Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
|
|
|
- await this.renderHeading();
|
|
|
- !this.model.get('hidden') && this.show();
|
|
|
- },
|
|
|
+ onStartResizeOccupants (ev) {
|
|
|
+ this.resizing = true;
|
|
|
+ this.el.addEventListener('mousemove', this.onMouseMove);
|
|
|
+ this.el.addEventListener('mouseup', this.onMouseUp);
|
|
|
|
|
|
- getNotifications () {
|
|
|
- const actors_per_state = this.model.notifications.toJSON();
|
|
|
- const states = api.settings.get('muc_show_join_leave') ?
|
|
|
- [...converse.CHAT_STATES, ...converse.MUC_TRAFFIC_STATES, ...converse.MUC_ROLE_CHANGES] :
|
|
|
- converse.CHAT_STATES;
|
|
|
+ const style = window.getComputedStyle(this.sidebar_view.el);
|
|
|
+ this.width = parseInt(style.width.replace(/px$/, ''), 10);
|
|
|
+ this.prev_pageX = ev.pageX;
|
|
|
+ },
|
|
|
|
|
|
- return states.reduce((result, state) => {
|
|
|
- const existing_actors = actors_per_state[state];
|
|
|
- if (!(existing_actors?.length)) {
|
|
|
- return result;
|
|
|
- }
|
|
|
- const actors = existing_actors.map(a => this.model.getOccupant(a)?.getDisplayName() || a);
|
|
|
- if (actors.length === 1) {
|
|
|
- if (state === 'composing') {
|
|
|
- return `${result}${__('%1$s is typing', actors[0])}\n`;
|
|
|
- } else if (state === 'paused') {
|
|
|
- return `${result}${__('%1$s has stopped typing', actors[0])}\n`;
|
|
|
- } else if (state === _converse.GONE) {
|
|
|
- return `${result}${__('%1$s has gone away', actors[0])}\n`;
|
|
|
- } else if (state === 'entered') {
|
|
|
- return `${result}${__('%1$s has entered the groupchat', actors[0])}\n`;
|
|
|
- } else if (state === 'exited') {
|
|
|
- return `${result}${__('%1$s has left the groupchat', actors[0])}\n`;
|
|
|
- } else if (state === 'op') {
|
|
|
- return `${result}${__("%1$s is now a moderator", actors[0])}\n`;
|
|
|
- } else if (state === 'deop') {
|
|
|
- return `${result}${__("%1$s is no longer a moderator", actors[0])}\n`;
|
|
|
- } else if (state === 'voice') {
|
|
|
- return `${result}${__("%1$s has been given a voice", actors[0])}\n`;
|
|
|
- } else if (state === 'mute') {
|
|
|
- return `${result}${__("%1$s has been muted", actors[0])}\n`;
|
|
|
- }
|
|
|
- } else if (actors.length > 1) {
|
|
|
- let actors_str;
|
|
|
- if (actors.length > 3) {
|
|
|
- actors_str = `${Array.from(actors).slice(0, 2).join(', ')} and others`;
|
|
|
- } else {
|
|
|
- const last_actor = actors.pop();
|
|
|
- actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
|
|
|
- }
|
|
|
+ onMouseMove (ev) {
|
|
|
+ if (this.resizing) {
|
|
|
+ ev.preventDefault();
|
|
|
+ const delta = this.prev_pageX - ev.pageX;
|
|
|
+ this.resizeSidebarView(delta, ev.pageX);
|
|
|
+ this.prev_pageX = ev.pageX;
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- if (state === 'composing') {
|
|
|
- return `${result}${__('%1$s are typing', actors_str)}\n`;
|
|
|
- } else if (state === 'paused') {
|
|
|
- return `${result}${__('%1$s have stopped typing', actors_str)}\n`;
|
|
|
- } else if (state === _converse.GONE) {
|
|
|
- return `${result}${__('%1$s have gone away', actors_str)}\n`;
|
|
|
- } else if (state === 'entered') {
|
|
|
- return `${result}${__('%1$s have entered the groupchat', actors_str)}\n`;
|
|
|
- } else if (state === 'exited') {
|
|
|
- return `${result}${__('%1$s have left the groupchat', actors_str)}\n`;
|
|
|
- } else if (state === 'op') {
|
|
|
- return `${result}${__("%1$s are now moderators", actors[0])}\n`;
|
|
|
- } else if (state === 'deop') {
|
|
|
- return `${result}${__("%1$s are no longer moderators", actors[0])}\n`;
|
|
|
- } else if (state === 'voice') {
|
|
|
- return `${result}${__("%1$s have been given voices", actors[0])}\n`;
|
|
|
- } else if (state === 'mute') {
|
|
|
- return `${result}${__("%1$s have been muted", actors[0])}\n`;
|
|
|
- }
|
|
|
- }
|
|
|
- return result;
|
|
|
- }, '');
|
|
|
- },
|
|
|
+ onMouseUp (ev) {
|
|
|
+ if (this.resizing) {
|
|
|
+ ev.preventDefault();
|
|
|
+ this.resizing = false;
|
|
|
+ this.el.removeEventListener('mousemove', this.onMouseMove);
|
|
|
+ this.el.removeEventListener('mouseup', this.onMouseUp);
|
|
|
+ const element_position = this.sidebar_view.el.getBoundingClientRect();
|
|
|
+ const occupants_width = this.calculateSidebarWidth(element_position, 0);
|
|
|
+ const attrs = {occupants_width};
|
|
|
+ _converse.connection.connected ? this.model.save(attrs) : this.model.set(attrs);
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- getHelpMessages () {
|
|
|
- const setting = api.settings.get("muc_disable_slash_commands");
|
|
|
- const disabled_commands = Array.isArray(setting) ? setting : [];
|
|
|
- return [
|
|
|
- `<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
|
|
|
- `<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
|
|
|
- `<strong>/clear</strong>: ${__('Clear the chat area')}`,
|
|
|
- `<strong>/close</strong>: ${__('Close this groupchat')}`,
|
|
|
- `<strong>/deop</strong>: ${__('Change user role to participant')}`,
|
|
|
- `<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
|
|
|
- `<strong>/help</strong>: ${__('Show this menu')}`,
|
|
|
- `<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
|
|
|
- `<strong>/me</strong>: ${__('Write in 3rd person')}`,
|
|
|
- `<strong>/member</strong>: ${__('Grant membership to a user')}`,
|
|
|
- `<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
|
|
|
- `<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
|
|
|
- `<strong>/nick</strong>: ${__('Change your nickname')}`,
|
|
|
- `<strong>/op</strong>: ${__('Grant moderator role to user')}`,
|
|
|
- `<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
|
|
|
- `<strong>/register</strong>: ${__("Register your nickname")}`,
|
|
|
- `<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
|
|
|
- `<strong>/subject</strong>: ${__('Set groupchat subject')}`,
|
|
|
- `<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
|
|
|
- `<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
|
|
|
- ].filter(line => disabled_commands.every(c => (!line.startsWith(c+'<', 9))))
|
|
|
- .filter(line => this.getAllowedCommands().some(c => line.startsWith(c+'<', 9)));
|
|
|
- },
|
|
|
+ resizeSidebarView (delta, current_mouse_position) {
|
|
|
+ const element_position = this.sidebar_view.el.getBoundingClientRect();
|
|
|
+ if (this.is_minimum) {
|
|
|
+ this.is_minimum = element_position.left < current_mouse_position;
|
|
|
+ } else if (this.is_maximum) {
|
|
|
+ this.is_maximum = element_position.left > current_mouse_position;
|
|
|
+ } else {
|
|
|
+ const occupants_width = this.calculateSidebarWidth(element_position, delta);
|
|
|
+ this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Renders the MUC heading if any relevant attributes have changed.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#renderHeading
|
|
|
- * @param { _converse.ChatRoom } [item]
|
|
|
- */
|
|
|
- async renderHeading () {
|
|
|
- const tpl = await this.generateHeadingTemplate();
|
|
|
- render(tpl, this.el.querySelector('.chat-head-chatroom'));
|
|
|
- },
|
|
|
+ calculateSidebarWidth(element_position, delta) {
|
|
|
+ let occupants_width = element_position.width + delta;
|
|
|
+ const room_width = this.el.clientWidth;
|
|
|
+ // keeping display in boundaries
|
|
|
+ if (occupants_width < (room_width * 0.20)) {
|
|
|
+ // set pixel to 20% width
|
|
|
+ occupants_width = (room_width * 0.20);
|
|
|
+ this.is_minimum = true;
|
|
|
+ } else if (occupants_width > (room_width * 0.75)) {
|
|
|
+ // set pixel to 75% width
|
|
|
+ occupants_width = (room_width * 0.75);
|
|
|
+ this.is_maximum = true;
|
|
|
+ } else if ((room_width - occupants_width) < 250) {
|
|
|
+ // resize occupants if chat-area becomes smaller than 250px (min-width property set in css)
|
|
|
+ occupants_width = room_width - 250;
|
|
|
+ this.is_maximum = true;
|
|
|
+ } else {
|
|
|
+ this.is_maximum = false;
|
|
|
+ this.is_minimum = false;
|
|
|
+ }
|
|
|
+ return occupants_width;
|
|
|
+ },
|
|
|
|
|
|
+ getAutoCompleteList () {
|
|
|
+ return this.model.getAllKnownNicknames().map(nick => ({'label': nick, 'value': `@${nick}`}));
|
|
|
+ },
|
|
|
|
|
|
- renderBottomPanel () {
|
|
|
- const container = this.el.querySelector('.bottom-panel');
|
|
|
- const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
|
|
|
- const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
|
|
|
- container.innerHTML = tpl_chatroom_bottom_panel({__, can_edit, entered});
|
|
|
- if (entered && can_edit) {
|
|
|
- this.renderMessageForm();
|
|
|
- this.initMentionAutoComplete();
|
|
|
- }
|
|
|
- },
|
|
|
+ getAutoCompleteListItem(text, input) {
|
|
|
+ input = input.trim();
|
|
|
+ const element = document.createElement("li");
|
|
|
+ element.setAttribute("aria-selected", "false");
|
|
|
|
|
|
- createSidebarView () {
|
|
|
- this.model.occupants.chatroomview = this;
|
|
|
- this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
|
|
|
- const container_el = this.el.querySelector('.chatroom-body');
|
|
|
- const occupants_width = this.model.get('occupants_width');
|
|
|
- if (this.sidebar_view && occupants_width !== undefined) {
|
|
|
- this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
|
|
|
- }
|
|
|
- container_el.insertAdjacentElement('beforeend', this.sidebar_view.el);
|
|
|
- },
|
|
|
+ if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
|
|
|
+ const img = document.createElement("img");
|
|
|
+ let dataUri = "data:" + _converse.DEFAULT_IMAGE_TYPE + ";base64," + _converse.DEFAULT_IMAGE;
|
|
|
|
|
|
- onStartResizeOccupants (ev) {
|
|
|
- this.resizing = true;
|
|
|
- this.el.addEventListener('mousemove', this.onMouseMove);
|
|
|
- this.el.addEventListener('mouseup', this.onMouseUp);
|
|
|
+ if (_converse.vcards) {
|
|
|
+ const vcard = _converse.vcards.findWhere({'nickname': text});
|
|
|
+ if (vcard) dataUri = "data:" + vcard.get('image_type') + ";base64," + vcard.get('image');
|
|
|
+ }
|
|
|
|
|
|
- const style = window.getComputedStyle(this.sidebar_view.el);
|
|
|
- this.width = parseInt(style.width.replace(/px$/, ''), 10);
|
|
|
- this.prev_pageX = ev.pageX;
|
|
|
- },
|
|
|
+ img.setAttribute("src", dataUri);
|
|
|
+ img.setAttribute("width", "22");
|
|
|
+ img.setAttribute("class", "avatar avatar-autocomplete");
|
|
|
+ element.appendChild(img);
|
|
|
+ }
|
|
|
|
|
|
- onMouseMove (ev) {
|
|
|
- if (this.resizing) {
|
|
|
- ev.preventDefault();
|
|
|
- const delta = this.prev_pageX - ev.pageX;
|
|
|
- this.resizeSidebarView(delta, ev.pageX);
|
|
|
- this.prev_pageX = ev.pageX;
|
|
|
- }
|
|
|
- },
|
|
|
+ const regex = new RegExp("(" + input + ")", "ig");
|
|
|
+ const parts = input ? text.split(regex) : [text];
|
|
|
|
|
|
- onMouseUp (ev) {
|
|
|
- if (this.resizing) {
|
|
|
- ev.preventDefault();
|
|
|
- this.resizing = false;
|
|
|
- this.el.removeEventListener('mousemove', this.onMouseMove);
|
|
|
- this.el.removeEventListener('mouseup', this.onMouseUp);
|
|
|
- const element_position = this.sidebar_view.el.getBoundingClientRect();
|
|
|
- const occupants_width = this.calculateSidebarWidth(element_position, 0);
|
|
|
- const attrs = {occupants_width};
|
|
|
- _converse.connection.connected ? this.model.save(attrs) : this.model.set(attrs);
|
|
|
- }
|
|
|
- },
|
|
|
+ parts.forEach(txt => {
|
|
|
+ if (input && txt.match(regex)) {
|
|
|
+ const match = document.createElement("mark");
|
|
|
+ match.textContent = txt;
|
|
|
+ element.appendChild(match);
|
|
|
+ } else {
|
|
|
+ element.appendChild(document.createTextNode(txt));
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- resizeSidebarView (delta, current_mouse_position) {
|
|
|
- const element_position = this.sidebar_view.el.getBoundingClientRect();
|
|
|
- if (this.is_minimum) {
|
|
|
- this.is_minimum = element_position.left < current_mouse_position;
|
|
|
- } else if (this.is_maximum) {
|
|
|
- this.is_maximum = element_position.left > current_mouse_position;
|
|
|
- } else {
|
|
|
- const occupants_width = this.calculateSidebarWidth(element_position, delta);
|
|
|
- this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
|
|
|
- }
|
|
|
- },
|
|
|
+ return element;
|
|
|
+ },
|
|
|
|
|
|
- calculateSidebarWidth(element_position, delta) {
|
|
|
- let occupants_width = element_position.width + delta;
|
|
|
- const room_width = this.el.clientWidth;
|
|
|
- // keeping display in boundaries
|
|
|
- if (occupants_width < (room_width * 0.20)) {
|
|
|
- // set pixel to 20% width
|
|
|
- occupants_width = (room_width * 0.20);
|
|
|
- this.is_minimum = true;
|
|
|
- } else if (occupants_width > (room_width * 0.75)) {
|
|
|
- // set pixel to 75% width
|
|
|
- occupants_width = (room_width * 0.75);
|
|
|
- this.is_maximum = true;
|
|
|
- } else if ((room_width - occupants_width) < 250) {
|
|
|
- // resize occupants if chat-area becomes smaller than 250px (min-width property set in css)
|
|
|
- occupants_width = room_width - 250;
|
|
|
- this.is_maximum = true;
|
|
|
- } else {
|
|
|
- this.is_maximum = false;
|
|
|
- this.is_minimum = false;
|
|
|
- }
|
|
|
- return occupants_width;
|
|
|
- },
|
|
|
+ initMentionAutoComplete () {
|
|
|
+ this.mention_auto_complete = new _converse.AutoComplete(this.el, {
|
|
|
+ 'auto_first': true,
|
|
|
+ 'auto_evaluate': false,
|
|
|
+ 'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
|
|
|
+ 'match_current_word': true,
|
|
|
+ 'list': () => this.getAutoCompleteList(),
|
|
|
+ 'filter': api.settings.get('muc_mention_autocomplete_filter') == 'contains' ?
|
|
|
+ _converse.FILTER_CONTAINS :
|
|
|
+ _converse.FILTER_STARTSWITH,
|
|
|
+ 'ac_triggers': ["Tab", "@"],
|
|
|
+ 'include_triggers': [],
|
|
|
+ 'item': this.getAutoCompleteListItem
|
|
|
+ });
|
|
|
+ this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
|
|
|
+ },
|
|
|
|
|
|
- getAutoCompleteList () {
|
|
|
- return this.model.getAllKnownNicknames().map(nick => ({'label': nick, 'value': `@${nick}`}));
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Get the nickname value from the form and then join the groupchat with it.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#submitNickname
|
|
|
+ * @param { Event }
|
|
|
+ */
|
|
|
+ submitNickname (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ const nick = ev.target.nick.value.trim();
|
|
|
+ nick && this.model.join(nick);
|
|
|
+ },
|
|
|
|
|
|
- getAutoCompleteListItem(text, input) {
|
|
|
- input = input.trim();
|
|
|
- const element = document.createElement("li");
|
|
|
- element.setAttribute("aria-selected", "false");
|
|
|
+ onKeyDown (ev) {
|
|
|
+ if (this.mention_auto_complete.onKeyDown(ev)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
|
|
|
+ },
|
|
|
|
|
|
- if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
|
|
|
- const img = document.createElement("img");
|
|
|
- let dataUri = "data:" + _converse.DEFAULT_IMAGE_TYPE + ";base64," + _converse.DEFAULT_IMAGE;
|
|
|
+ onKeyUp (ev) {
|
|
|
+ this.mention_auto_complete.evaluate(ev);
|
|
|
+ return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
|
|
|
+ },
|
|
|
|
|
|
- if (_converse.vcards) {
|
|
|
- const vcard = _converse.vcards.findWhere({'nickname': text});
|
|
|
- if (vcard) dataUri = "data:" + vcard.get('image_type') + ";base64," + vcard.get('image');
|
|
|
- }
|
|
|
+ async onMessageRetractButtonClicked (message) {
|
|
|
+ const retraction_warning =
|
|
|
+ __("Be aware that other XMPP/Jabber clients (and servers) may "+
|
|
|
+ "not yet support retractions and that this message may not "+
|
|
|
+ "be removed everywhere.");
|
|
|
|
|
|
- img.setAttribute("src", dataUri);
|
|
|
- img.setAttribute("width", "22");
|
|
|
- img.setAttribute("class", "avatar avatar-autocomplete");
|
|
|
- element.appendChild(img);
|
|
|
- }
|
|
|
+ if (message.mayBeRetracted()) {
|
|
|
+ const messages = [__('Are you sure you want to retract this message?')];
|
|
|
+ if (api.settings.get('show_retraction_warning')) {
|
|
|
+ messages[1] = retraction_warning;
|
|
|
+ }
|
|
|
+ !!(await api.confirm(__('Confirm'), messages)) && this.model.retractOwnMessage(message);
|
|
|
+ } else if (await message.mayBeModerated()) {
|
|
|
+ if (message.get('sender') === 'me') {
|
|
|
+ let messages = [__('Are you sure you want to retract this message?')];
|
|
|
+ if (api.settings.get('show_retraction_warning')) {
|
|
|
+ messages = [messages[0], retraction_warning, messages[1]]
|
|
|
+ }
|
|
|
+ !!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage(message);
|
|
|
+ } else {
|
|
|
+ let messages = [
|
|
|
+ __('You are about to retract this message.'),
|
|
|
+ __('You may optionally include a message, explaining the reason for the retraction.')
|
|
|
+ ];
|
|
|
+ if (api.settings.get('show_retraction_warning')) {
|
|
|
+ messages = [messages[0], retraction_warning, messages[1]]
|
|
|
+ }
|
|
|
+ const reason = await api.prompt(
|
|
|
+ __('Message Retraction'),
|
|
|
+ messages,
|
|
|
+ __('Optional reason')
|
|
|
+ );
|
|
|
+ (reason !== false) && this.retractOtherMessage(message, reason);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const err_msg = __(`Sorry, you're not allowed to retract this message`);
|
|
|
+ api.alert('error', __('Error'), err_msg);
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- const regex = new RegExp("(" + input + ")", "ig");
|
|
|
- const parts = input ? text.split(regex) : [text];
|
|
|
+ /**
|
|
|
+ * Retract someone else's message in this groupchat.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#retractOtherMessage
|
|
|
+ * @param { _converse.Message } message - The message which we're retracting.
|
|
|
+ * @param { string } [reason] - The reason for retracting the message.
|
|
|
+ */
|
|
|
+ async retractOtherMessage (message, reason) {
|
|
|
+ const result = await this.model.retractOtherMessage(message, reason);
|
|
|
+ if (result === null) {
|
|
|
+ const err_msg = __(`A timeout occurred while trying to retract the message`);
|
|
|
+ api.alert('error', __('Error'), err_msg);
|
|
|
+ log(err_msg, Strophe.LogLevel.WARN);
|
|
|
+ } else if (u.isErrorStanza(result)) {
|
|
|
+ const err_msg = __(`Sorry, you're not allowed to retract this message.`);
|
|
|
+ api.alert('error', __('Error'), err_msg);
|
|
|
+ log(err_msg, Strophe.LogLevel.WARN);
|
|
|
+ log(result, Strophe.LogLevel.WARN);
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- parts.forEach(txt => {
|
|
|
- if (input && txt.match(regex)) {
|
|
|
- const match = document.createElement("mark");
|
|
|
- match.textContent = txt;
|
|
|
- element.appendChild(match);
|
|
|
- } else {
|
|
|
- element.appendChild(document.createTextNode(txt));
|
|
|
- }
|
|
|
- });
|
|
|
+ showModeratorToolsModal (affiliation) {
|
|
|
+ if (!this.verifyRoles(['moderator'])) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (isUndefined(this.model.modtools_modal)) {
|
|
|
+ const model = new Model({'affiliation': affiliation});
|
|
|
+ this.modtools_modal = new ModeratorToolsModal({model, _converse, 'chatroomview': this});
|
|
|
+ } else {
|
|
|
+ this.modtools_modal.set('affiliation', affiliation);
|
|
|
+ }
|
|
|
+ this.modtools_modal.show();
|
|
|
+ },
|
|
|
|
|
|
- return element;
|
|
|
- },
|
|
|
+ showRoomDetailsModal (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ if (this.model.room_details_modal === undefined) {
|
|
|
+ this.model.room_details_modal = new RoomDetailsModal({'model': this.model});
|
|
|
+ }
|
|
|
+ this.model.room_details_modal.show(ev);
|
|
|
+ },
|
|
|
|
|
|
- initMentionAutoComplete () {
|
|
|
- this.mention_auto_complete = new _converse.AutoComplete(this.el, {
|
|
|
- 'auto_first': true,
|
|
|
- 'auto_evaluate': false,
|
|
|
- 'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
|
|
|
- 'match_current_word': true,
|
|
|
- 'list': () => this.getAutoCompleteList(),
|
|
|
- 'filter': api.settings.get('muc_mention_autocomplete_filter') == 'contains' ?
|
|
|
- _converse.FILTER_CONTAINS :
|
|
|
- _converse.FILTER_STARTSWITH,
|
|
|
- 'ac_triggers': ["Tab", "@"],
|
|
|
- 'include_triggers': [],
|
|
|
- 'item': this.getAutoCompleteListItem
|
|
|
- });
|
|
|
- this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
|
|
|
- },
|
|
|
+ showChatStateNotification (message) {
|
|
|
+ if (message.get('sender') === 'me') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return _converse.ChatBoxView.prototype.showChatStateNotification.apply(this, arguments);
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Get the nickname value from the form and then join the groupchat with it.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#submitNickname
|
|
|
- * @param { Event }
|
|
|
- */
|
|
|
- submitNickname (ev) {
|
|
|
- ev.preventDefault();
|
|
|
- const nick = ev.target.nick.value.trim();
|
|
|
- nick && this.model.join(nick);
|
|
|
- },
|
|
|
+ onOccupantAffiliationChanged (occupant) {
|
|
|
+ if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
+ this.renderHeading();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- onKeyDown (ev) {
|
|
|
- if (this.mention_auto_complete.onKeyDown(ev)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
|
|
|
- },
|
|
|
+ onOccupantRoleChanged (occupant) {
|
|
|
+ if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
+ this.renderBottomPanel();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- onKeyUp (ev) {
|
|
|
- this.mention_auto_complete.evaluate(ev);
|
|
|
- return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Returns a list of objects which represent buttons for the groupchat header.
|
|
|
+ * @emits _converse#getHeadingButtons
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#getHeadingButtons
|
|
|
+ */
|
|
|
+ getHeadingButtons (subject_hidden) {
|
|
|
+ const buttons = [];
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Details'),
|
|
|
+ 'i18n_title': __('Show more information about this groupchat'),
|
|
|
+ 'handler': ev => this.showRoomDetailsModal(ev),
|
|
|
+ 'a_class': 'show-room-details-modal',
|
|
|
+ 'icon_class': 'fa-info-circle',
|
|
|
+ 'name': 'details'
|
|
|
+ });
|
|
|
|
|
|
- async onMessageRetractButtonClicked (message) {
|
|
|
- const retraction_warning =
|
|
|
- __("Be aware that other XMPP/Jabber clients (and servers) may "+
|
|
|
- "not yet support retractions and that this message may not "+
|
|
|
- "be removed everywhere.");
|
|
|
+ if (this.model.getOwnAffiliation() === 'owner') {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Configure'),
|
|
|
+ 'i18n_title': __('Configure this groupchat'),
|
|
|
+ 'handler': ev => this.getAndRenderConfigurationForm(ev),
|
|
|
+ 'a_class': 'configure-chatroom-button',
|
|
|
+ 'icon_class': 'fa-wrench',
|
|
|
+ 'name': 'configure'
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- if (message.mayBeRetracted()) {
|
|
|
- const messages = [__('Are you sure you want to retract this message?')];
|
|
|
- if (api.settings.get('show_retraction_warning')) {
|
|
|
- messages[1] = retraction_warning;
|
|
|
- }
|
|
|
- !!(await api.confirm(__('Confirm'), messages)) && this.model.retractOwnMessage(message);
|
|
|
- } else if (await message.mayBeModerated()) {
|
|
|
- if (message.get('sender') === 'me') {
|
|
|
- let messages = [__('Are you sure you want to retract this message?')];
|
|
|
- if (api.settings.get('show_retraction_warning')) {
|
|
|
- messages = [messages[0], retraction_warning, messages[1]]
|
|
|
- }
|
|
|
- !!(await api.confirm(__('Confirm'), messages)) && this.retractOtherMessage(message);
|
|
|
- } else {
|
|
|
- let messages = [
|
|
|
- __('You are about to retract this message.'),
|
|
|
- __('You may optionally include a message, explaining the reason for the retraction.')
|
|
|
- ];
|
|
|
- if (api.settings.get('show_retraction_warning')) {
|
|
|
- messages = [messages[0], retraction_warning, messages[1]]
|
|
|
- }
|
|
|
- const reason = await api.prompt(
|
|
|
- __('Message Retraction'),
|
|
|
- messages,
|
|
|
- __('Optional reason')
|
|
|
- );
|
|
|
- (reason !== false) && this.retractOtherMessage(message, reason);
|
|
|
- }
|
|
|
- } else {
|
|
|
- const err_msg = __(`Sorry, you're not allowed to retract this message`);
|
|
|
- api.alert('error', __('Error'), err_msg);
|
|
|
- }
|
|
|
- },
|
|
|
+ if (this.model.invitesAllowed()) {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Invite'),
|
|
|
+ 'i18n_title': __('Invite someone to join this groupchat'),
|
|
|
+ 'handler': ev => this.showInviteModal(ev),
|
|
|
+ 'a_class': 'open-invite-modal',
|
|
|
+ 'icon_class': 'fa-user-plus',
|
|
|
+ 'name': 'invite'
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- /**
|
|
|
- * Retract someone else's message in this groupchat.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#retractOtherMessage
|
|
|
- * @param { _converse.Message } message - The message which we're retracting.
|
|
|
- * @param { string } [reason] - The reason for retracting the message.
|
|
|
- */
|
|
|
- async retractOtherMessage (message, reason) {
|
|
|
- const result = await this.model.retractOtherMessage(message, reason);
|
|
|
- if (result === null) {
|
|
|
- const err_msg = __(`A timeout occurred while trying to retract the message`);
|
|
|
- api.alert('error', __('Error'), err_msg);
|
|
|
- log(err_msg, Strophe.LogLevel.WARN);
|
|
|
- } else if (u.isErrorStanza(result)) {
|
|
|
- const err_msg = __(`Sorry, you're not allowed to retract this message.`);
|
|
|
- api.alert('error', __('Error'), err_msg);
|
|
|
- log(err_msg, Strophe.LogLevel.WARN);
|
|
|
- log(result, Strophe.LogLevel.WARN);
|
|
|
- }
|
|
|
- },
|
|
|
+ const subject = this.model.get('subject');
|
|
|
+ if (subject && subject.text) {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': subject_hidden ? __('Show topic') : __('Hide topic'),
|
|
|
+ 'i18n_title': subject_hidden ?
|
|
|
+ __('Show the topic message in the heading') :
|
|
|
+ __('Hide the topic in the heading'),
|
|
|
+ 'handler': ev => this.toggleTopic(ev),
|
|
|
+ 'a_class': 'hide-topic',
|
|
|
+ 'icon_class': 'fa-minus-square',
|
|
|
+ 'name': 'toggle-topic'
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- showModeratorToolsModal (affiliation) {
|
|
|
- if (!this.verifyRoles(['moderator'])) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (isUndefined(this.model.modtools_modal)) {
|
|
|
- const model = new Model({'affiliation': affiliation});
|
|
|
- this.modtools_modal = new ModeratorToolsModal({model, _converse, 'chatroomview': this});
|
|
|
- } else {
|
|
|
- this.modtools_modal.set('affiliation', affiliation);
|
|
|
- }
|
|
|
- this.modtools_modal.show();
|
|
|
- },
|
|
|
|
|
|
- showRoomDetailsModal (ev) {
|
|
|
- ev.preventDefault();
|
|
|
- if (this.model.room_details_modal === undefined) {
|
|
|
- this.model.room_details_modal = new RoomDetailsModal({'model': this.model});
|
|
|
- }
|
|
|
- this.model.room_details_modal.show(ev);
|
|
|
- },
|
|
|
+ const conn_status = this.model.session.get('connection_status');
|
|
|
+ if (conn_status === converse.ROOMSTATUS.ENTERED) {
|
|
|
+ const allowed_commands = this.getAllowedCommands();
|
|
|
+ if (allowed_commands.includes('modtools')) {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Moderate'),
|
|
|
+ 'i18n_title': __('Moderate this groupchat'),
|
|
|
+ 'handler': () => this.showModeratorToolsModal(),
|
|
|
+ 'a_class': 'moderate-chatroom-button',
|
|
|
+ 'icon_class': 'fa-user-cog',
|
|
|
+ 'name': 'moderate'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (allowed_commands.includes('destroy')) {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Destroy'),
|
|
|
+ 'i18n_title': __('Remove this groupchat'),
|
|
|
+ 'handler': ev => this.destroy(ev),
|
|
|
+ 'a_class': 'destroy-chatroom-button',
|
|
|
+ 'icon_class': 'fa-trash',
|
|
|
+ 'name': 'destroy'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- showChatStateNotification (message) {
|
|
|
- if (message.get('sender') === 'me') {
|
|
|
- return;
|
|
|
- }
|
|
|
- return _converse.ChatBoxView.prototype.showChatStateNotification.apply(this, arguments);
|
|
|
- },
|
|
|
+ if (!api.settings.get("singleton")) {
|
|
|
+ buttons.push({
|
|
|
+ 'i18n_text': __('Leave'),
|
|
|
+ 'i18n_title': __('Leave and close this groupchat'),
|
|
|
+ 'handler': async ev => {
|
|
|
+ ev.stopPropagation();
|
|
|
+ const messages = [__('Are you sure you want to leave this groupchat?')];
|
|
|
+ const result = await api.confirm(__('Confirm'), messages);
|
|
|
+ result && this.close(ev);
|
|
|
+ },
|
|
|
+ 'a_class': 'close-chatbox-button',
|
|
|
+ 'standalone': api.settings.get("view_mode") === 'overlayed',
|
|
|
+ 'icon_class': 'fa-sign-out-alt',
|
|
|
+ 'name': 'signout'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return _converse.api.hook('getHeadingButtons', this, buttons);
|
|
|
+ },
|
|
|
|
|
|
- onOccupantAffiliationChanged (occupant) {
|
|
|
- if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
- this.renderHeading();
|
|
|
- }
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Returns the groupchat heading TemplateResult to be rendered.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#generateHeadingTemplate
|
|
|
+ */
|
|
|
+ async generateHeadingTemplate () {
|
|
|
+ const subject_hidden = await this.model.isSubjectHidden();
|
|
|
+ const heading_btns = await this.getHeadingButtons(subject_hidden);
|
|
|
+ const standalone_btns = heading_btns.filter(b => b.standalone);
|
|
|
+ const dropdown_btns = heading_btns.filter(b => !b.standalone);
|
|
|
+ return tpl_chatroom_head(
|
|
|
+ Object.assign(this.model.toJSON(), {
|
|
|
+ _converse,
|
|
|
+ subject_hidden,
|
|
|
+ 'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
|
|
|
+ 'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
|
|
|
+ 'title': this.model.getDisplayName(),
|
|
|
+ }));
|
|
|
+ },
|
|
|
|
|
|
- onOccupantRoleChanged (occupant) {
|
|
|
- if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
- this.renderBottomPanel();
|
|
|
- }
|
|
|
- },
|
|
|
+ toggleTopic () {
|
|
|
+ this.model.toggleSubjectHiddenState();
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Returns a list of objects which represent buttons for the groupchat header.
|
|
|
- * @emits _converse#getHeadingButtons
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#getHeadingButtons
|
|
|
- */
|
|
|
- getHeadingButtons (subject_hidden) {
|
|
|
- const buttons = [];
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Details'),
|
|
|
- 'i18n_title': __('Show more information about this groupchat'),
|
|
|
- 'handler': ev => this.showRoomDetailsModal(ev),
|
|
|
- 'a_class': 'show-room-details-modal',
|
|
|
- 'icon_class': 'fa-info-circle',
|
|
|
- 'name': 'details'
|
|
|
- });
|
|
|
+ showInviteModal (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ if (this.muc_invite_modal === undefined) {
|
|
|
+ this.muc_invite_modal = new MUCInviteModal({'model': new Model()});
|
|
|
+ // TODO: remove once we have API for sending direct invite
|
|
|
+ this.muc_invite_modal.chatroomview = this;
|
|
|
+ }
|
|
|
+ this.muc_invite_modal.show(ev);
|
|
|
+ },
|
|
|
|
|
|
- if (this.model.getOwnAffiliation() === 'owner') {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Configure'),
|
|
|
- 'i18n_title': __('Configure this groupchat'),
|
|
|
- 'handler': ev => this.getAndRenderConfigurationForm(ev),
|
|
|
- 'a_class': 'configure-chatroom-button',
|
|
|
- 'icon_class': 'fa-wrench',
|
|
|
- 'name': 'configure'
|
|
|
- });
|
|
|
- }
|
|
|
|
|
|
- if (this.model.invitesAllowed()) {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Invite'),
|
|
|
- 'i18n_title': __('Invite someone to join this groupchat'),
|
|
|
- 'handler': ev => this.showInviteModal(ev),
|
|
|
- 'a_class': 'open-invite-modal',
|
|
|
- 'icon_class': 'fa-user-plus',
|
|
|
- 'name': 'invite'
|
|
|
- });
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Callback method that gets called after the chat has become visible.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#afterShown
|
|
|
+ */
|
|
|
+ afterShown () {
|
|
|
+ // Override from converse-chatview, specifically to avoid
|
|
|
+ // the 'active' chat state from being sent out prematurely.
|
|
|
+ // This is instead done in `onConnectionStatusChanged` below.
|
|
|
+ if (u.isPersistableModel(this.model)) {
|
|
|
+ this.model.clearUnreadMsgCounter();
|
|
|
+ }
|
|
|
+ this.scrollDown();
|
|
|
+ },
|
|
|
|
|
|
- const subject = this.model.get('subject');
|
|
|
- if (subject && subject.text) {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': subject_hidden ? __('Show topic') : __('Hide topic'),
|
|
|
- 'i18n_title': subject_hidden ?
|
|
|
- __('Show the topic message in the heading') :
|
|
|
- __('Hide the topic in the heading'),
|
|
|
- 'handler': ev => this.toggleTopic(ev),
|
|
|
- 'a_class': 'hide-topic',
|
|
|
- 'icon_class': 'fa-minus-square',
|
|
|
- 'name': 'toggle-topic'
|
|
|
- });
|
|
|
- }
|
|
|
+ onConnectionStatusChanged () {
|
|
|
+ const conn_status = this.model.session.get('connection_status');
|
|
|
+ if (conn_status === converse.ROOMSTATUS.NICKNAME_REQUIRED) {
|
|
|
+ this.renderNicknameForm();
|
|
|
+ } else if (conn_status === converse.ROOMSTATUS.PASSWORD_REQUIRED) {
|
|
|
+ this.renderPasswordForm();
|
|
|
+ } else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
|
|
|
+ this.showSpinner();
|
|
|
+ } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
|
|
|
+ this.renderBottomPanel();
|
|
|
+ this.hideSpinner();
|
|
|
+ this.maybeFocus();
|
|
|
+ } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
|
|
|
+ this.showDisconnectMessage();
|
|
|
+ } else if (conn_status === converse.ROOMSTATUS.DESTROYED) {
|
|
|
+ this.showDestroyedMessage();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
+ getToolbarOptions () {
|
|
|
+ return Object.assign(
|
|
|
+ _converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments), {
|
|
|
+ 'is_groupchat': true,
|
|
|
+ 'label_hide_occupants': __('Hide the list of participants'),
|
|
|
+ 'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
|
|
|
- const conn_status = this.model.session.get('connection_status');
|
|
|
- if (conn_status === converse.ROOMSTATUS.ENTERED) {
|
|
|
- const allowed_commands = this.getAllowedCommands();
|
|
|
- if (allowed_commands.includes('modtools')) {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Moderate'),
|
|
|
- 'i18n_title': __('Moderate this groupchat'),
|
|
|
- 'handler': () => this.showModeratorToolsModal(),
|
|
|
- 'a_class': 'moderate-chatroom-button',
|
|
|
- 'icon_class': 'fa-user-cog',
|
|
|
- 'name': 'moderate'
|
|
|
- });
|
|
|
- }
|
|
|
- if (allowed_commands.includes('destroy')) {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Destroy'),
|
|
|
- 'i18n_title': __('Remove this groupchat'),
|
|
|
- 'handler': ev => this.destroy(ev),
|
|
|
- 'a_class': 'destroy-chatroom-button',
|
|
|
- 'icon_class': 'fa-trash',
|
|
|
- 'name': 'destroy'
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (!api.settings.get("singleton")) {
|
|
|
- buttons.push({
|
|
|
- 'i18n_text': __('Leave'),
|
|
|
- 'i18n_title': __('Leave and close this groupchat'),
|
|
|
- 'handler': async ev => {
|
|
|
- ev.stopPropagation();
|
|
|
- const messages = [__('Are you sure you want to leave this groupchat?')];
|
|
|
- const result = await api.confirm(__('Confirm'), messages);
|
|
|
- result && this.close(ev);
|
|
|
- },
|
|
|
- 'a_class': 'close-chatbox-button',
|
|
|
- 'standalone': api.settings.get("view_mode") === 'overlayed',
|
|
|
- 'icon_class': 'fa-sign-out-alt',
|
|
|
- 'name': 'signout'
|
|
|
- });
|
|
|
- }
|
|
|
- return _converse.api.hook('getHeadingButtons', this, buttons);
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Closes this chat box, which implies leaving the groupchat as well.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#close
|
|
|
+ */
|
|
|
+ async close () {
|
|
|
+ this.hide();
|
|
|
+ if (_converse.router.history.getFragment() === "converse/room?jid="+this.model.get('jid')) {
|
|
|
+ _converse.router.navigate('');
|
|
|
+ }
|
|
|
+ await this.model.leave();
|
|
|
+ return _converse.ChatBoxView.prototype.close.apply(this, arguments);
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Returns the groupchat heading TemplateResult to be rendered.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#generateHeadingTemplate
|
|
|
- */
|
|
|
- async generateHeadingTemplate () {
|
|
|
- const subject_hidden = await this.model.isSubjectHidden();
|
|
|
- const heading_btns = await this.getHeadingButtons(subject_hidden);
|
|
|
- const standalone_btns = heading_btns.filter(b => b.standalone);
|
|
|
- const dropdown_btns = heading_btns.filter(b => !b.standalone);
|
|
|
- return tpl_chatroom_head(
|
|
|
- Object.assign(this.model.toJSON(), {
|
|
|
- _converse,
|
|
|
- subject_hidden,
|
|
|
- 'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
|
|
|
- 'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
|
|
|
- 'title': this.model.getDisplayName(),
|
|
|
- }));
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Hide the right sidebar containing the chat occupants.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#hideOccupants
|
|
|
+ */
|
|
|
+ hideOccupants (ev) {
|
|
|
+ if (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ ev.stopPropagation();
|
|
|
+ }
|
|
|
+ this.model.save({'hidden_occupants': true});
|
|
|
+ this.scrollDown();
|
|
|
+ },
|
|
|
|
|
|
- toggleTopic () {
|
|
|
- this.model.toggleSubjectHiddenState();
|
|
|
- },
|
|
|
+ verifyRoles (roles, occupant, show_error=true) {
|
|
|
+ if (!Array.isArray(roles)) {
|
|
|
+ throw new TypeError('roles must be an Array');
|
|
|
+ }
|
|
|
+ if (!roles.length) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ occupant = occupant || this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
+ if (occupant) {
|
|
|
+ const role = occupant.get('role');
|
|
|
+ if (roles.includes(role)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (show_error) {
|
|
|
+ const message = __('Forbidden: you do not have the necessary role in order to do that.');
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ },
|
|
|
|
|
|
- showInviteModal (ev) {
|
|
|
- ev.preventDefault();
|
|
|
- if (this.muc_invite_modal === undefined) {
|
|
|
- this.muc_invite_modal = new MUCInviteModal({'model': new Model()});
|
|
|
- // TODO: remove once we have API for sending direct invite
|
|
|
- this.muc_invite_modal.chatroomview = this;
|
|
|
- }
|
|
|
- this.muc_invite_modal.show(ev);
|
|
|
- },
|
|
|
+ verifyAffiliations (affiliations, occupant, show_error=true) {
|
|
|
+ if (!Array.isArray(affiliations)) {
|
|
|
+ throw new TypeError('affiliations must be an Array');
|
|
|
+ }
|
|
|
+ if (!affiliations.length) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ occupant = occupant || this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
+ if (occupant) {
|
|
|
+ const a = occupant.get('affiliation');
|
|
|
+ if (affiliations.includes(a)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (show_error) {
|
|
|
+ const message = __('Forbidden: you do not have the necessary affiliation in order to do that.');
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ },
|
|
|
|
|
|
+ validateRoleOrAffiliationChangeArgs (command, args) {
|
|
|
+ if (!args) {
|
|
|
+ const message = __(
|
|
|
+ 'Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
|
|
|
+ command
|
|
|
+ );
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Callback method that gets called after the chat has become visible.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#afterShown
|
|
|
- */
|
|
|
- afterShown () {
|
|
|
- // Override from converse-chatview, specifically to avoid
|
|
|
- // the 'active' chat state from being sent out prematurely.
|
|
|
- // This is instead done in `onConnectionStatusChanged` below.
|
|
|
- if (u.isPersistableModel(this.model)) {
|
|
|
- this.model.clearUnreadMsgCounter();
|
|
|
- }
|
|
|
- this.scrollDown();
|
|
|
- },
|
|
|
+ getNickOrJIDFromCommandArgs (args) {
|
|
|
+ if (u.isValidJID(args.trim())) {
|
|
|
+ return args.trim();
|
|
|
+ }
|
|
|
+ if (!args.startsWith('@')) {
|
|
|
+ args = '@'+ args;
|
|
|
+ }
|
|
|
+ const [text, references] = this.model.parseTextForReferences(args); // eslint-disable-line no-unused-vars
|
|
|
+ if (!references.length) {
|
|
|
+ const message = __("Error: couldn't find a groupchat participant based on your arguments");
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (references.length > 1) {
|
|
|
+ const message = __("Error: found multiple groupchat participant based on your arguments");
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const nick_or_jid = references.pop().value;
|
|
|
+ const reason = args.split(nick_or_jid, 2)[1];
|
|
|
+ if (reason && !reason.startsWith(' ')) {
|
|
|
+ const message = __("Error: couldn't find a groupchat participant based on your arguments");
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return nick_or_jid;
|
|
|
+ },
|
|
|
|
|
|
- onConnectionStatusChanged () {
|
|
|
- const conn_status = this.model.session.get('connection_status');
|
|
|
- if (conn_status === converse.ROOMSTATUS.NICKNAME_REQUIRED) {
|
|
|
- this.renderNicknameForm();
|
|
|
- } else if (conn_status === converse.ROOMSTATUS.PASSWORD_REQUIRED) {
|
|
|
- this.renderPasswordForm();
|
|
|
- } else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
|
|
|
- this.showSpinner();
|
|
|
- } else if (conn_status === converse.ROOMSTATUS.ENTERED) {
|
|
|
- this.renderBottomPanel();
|
|
|
- this.hideSpinner();
|
|
|
- this.maybeFocus();
|
|
|
- } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
|
|
|
- this.showDisconnectMessage();
|
|
|
- } else if (conn_status === converse.ROOMSTATUS.DESTROYED) {
|
|
|
- this.showDestroyedMessage();
|
|
|
- }
|
|
|
- },
|
|
|
+ setAffiliation (command, args, required_affiliations) {
|
|
|
+ const affiliation = COMMAND_TO_AFFILIATION[command];
|
|
|
+ if (!affiliation) {
|
|
|
+ throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
|
|
|
+ }
|
|
|
+ if (!this.verifyAffiliations(required_affiliations)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
|
|
|
+ if (!nick_or_jid) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
|
|
|
- getToolbarOptions () {
|
|
|
- return Object.assign(
|
|
|
- _converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments), {
|
|
|
- 'is_groupchat': true,
|
|
|
- 'label_hide_occupants': __('Hide the list of participants'),
|
|
|
- 'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
|
|
|
- }
|
|
|
+ let jid;
|
|
|
+ const reason = args.split(nick_or_jid, 2)[1].trim();
|
|
|
+ const occupant = this.model.getOccupant(nick_or_jid);
|
|
|
+ if (occupant) {
|
|
|
+ jid = occupant.get('jid');
|
|
|
+ } else {
|
|
|
+ if (u.isValidJID(nick_or_jid)) {
|
|
|
+ jid = nick_or_jid;
|
|
|
+ } else {
|
|
|
+ const message = __(
|
|
|
+ "Couldn't find a participant with that nickname. "+
|
|
|
+ "They might have left the groupchat."
|
|
|
);
|
|
|
- },
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const attrs = { jid, reason };
|
|
|
+ if (occupant && api.settings.get('auto_register_muc_nickname')) {
|
|
|
+ attrs['nick'] = occupant.get('nick');
|
|
|
+ }
|
|
|
+ this.model.setAffiliation(affiliation, [attrs])
|
|
|
+ .then(() => this.model.occupants.fetchMembers())
|
|
|
+ .catch(err => this.onCommandError(err));
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Closes this chat box, which implies leaving the groupchat as well.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#close
|
|
|
- */
|
|
|
- async close () {
|
|
|
- this.hide();
|
|
|
- if (_converse.router.history.getFragment() === "converse/room?jid="+this.model.get('jid')) {
|
|
|
- _converse.router.navigate('');
|
|
|
- }
|
|
|
- await this.model.leave();
|
|
|
- return _converse.ChatBoxView.prototype.close.apply(this, arguments);
|
|
|
- },
|
|
|
+ getReason (args) {
|
|
|
+ return args.includes(',') ? args.slice(args.indexOf(',')+1).trim() : null;
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Hide the right sidebar containing the chat occupants.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#hideOccupants
|
|
|
- */
|
|
|
- hideOccupants (ev) {
|
|
|
- if (ev) {
|
|
|
- ev.preventDefault();
|
|
|
- ev.stopPropagation();
|
|
|
- }
|
|
|
- this.model.save({'hidden_occupants': true});
|
|
|
- this.scrollDown();
|
|
|
- },
|
|
|
+ setRole (command, args, required_affiliations=[], required_roles=[]) {
|
|
|
+ /* Check that a command to change a groupchat user's role or
|
|
|
+ * affiliation has anough arguments.
|
|
|
+ */
|
|
|
+ const role = COMMAND_TO_ROLE[command];
|
|
|
+ if (!role) {
|
|
|
+ throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
|
|
|
+ }
|
|
|
+ if (!this.verifyAffiliations(required_affiliations) || !this.verifyRoles(required_roles)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
|
|
|
+ if (!nick_or_jid) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const reason = args.split(nick_or_jid, 2)[1].trim();
|
|
|
+ // We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
|
|
|
+ const occupant = this.model.getOccupant(nick_or_jid);
|
|
|
+ this.model.setRole(occupant, role, reason, undefined, this.onCommandError.bind(this));
|
|
|
+ return true;
|
|
|
+ },
|
|
|
|
|
|
- verifyRoles (roles, occupant, show_error=true) {
|
|
|
- if (!Array.isArray(roles)) {
|
|
|
- throw new TypeError('roles must be an Array');
|
|
|
- }
|
|
|
- if (!roles.length) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- occupant = occupant || this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
- if (occupant) {
|
|
|
- const role = occupant.get('role');
|
|
|
- if (roles.includes(role)) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- if (show_error) {
|
|
|
- const message = __('Forbidden: you do not have the necessary role in order to do that.');
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- }
|
|
|
- return false;
|
|
|
- },
|
|
|
+ onCommandError (err) {
|
|
|
+ log.fatal(err);
|
|
|
+ const message =
|
|
|
+ __("Sorry, an error happened while running the command.") + " " +
|
|
|
+ __("Check your browser's developer console for details.");
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
+ },
|
|
|
|
|
|
- verifyAffiliations (affiliations, occupant, show_error=true) {
|
|
|
- if (!Array.isArray(affiliations)) {
|
|
|
- throw new TypeError('affiliations must be an Array');
|
|
|
- }
|
|
|
- if (!affiliations.length) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- occupant = occupant || this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
- if (occupant) {
|
|
|
- const a = occupant.get('affiliation');
|
|
|
- if (affiliations.includes(a)) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- if (show_error) {
|
|
|
- const message = __('Forbidden: you do not have the necessary affiliation in order to do that.');
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- }
|
|
|
- return false;
|
|
|
- },
|
|
|
+ getAllowedCommands () {
|
|
|
+ let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
|
|
|
+ if (this.model.config.get('changesubject') || ['owner', 'admin'].includes(this.model.getOwnAffiliation())) {
|
|
|
+ allowed_commands = [...allowed_commands, ...['subject', 'topic']];
|
|
|
+ }
|
|
|
+ const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
+ if (this.verifyAffiliations(['owner'], occupant, false)) {
|
|
|
+ allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
|
|
|
+ } else if (this.verifyAffiliations(['admin'], occupant, false)) {
|
|
|
+ allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
|
|
|
+ }
|
|
|
+ if (this.verifyRoles(['moderator'], occupant, false)) {
|
|
|
+ allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
|
|
|
+ } else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
|
|
|
+ allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
|
|
|
+ }
|
|
|
+ allowed_commands.sort();
|
|
|
|
|
|
- validateRoleOrAffiliationChangeArgs (command, args) {
|
|
|
- if (!args) {
|
|
|
- const message = __(
|
|
|
- 'Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
|
|
|
- command
|
|
|
- );
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- return false;
|
|
|
- }
|
|
|
- return true;
|
|
|
- },
|
|
|
+ if (Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
|
|
|
+ return allowed_commands.filter(c => !api.settings.get('muc_disable_slash_commands').includes(c));
|
|
|
+ } else {
|
|
|
+ return allowed_commands;
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- getNickOrJIDFromCommandArgs (args) {
|
|
|
- if (u.isValidJID(args.trim())) {
|
|
|
- return args.trim();
|
|
|
- }
|
|
|
- if (!args.startsWith('@')) {
|
|
|
- args = '@'+ args;
|
|
|
- }
|
|
|
- const [text, references] = this.model.parseTextForReferences(args); // eslint-disable-line no-unused-vars
|
|
|
- if (!references.length) {
|
|
|
- const message = __("Error: couldn't find a groupchat participant based on your arguments");
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- return;
|
|
|
- }
|
|
|
- if (references.length > 1) {
|
|
|
- const message = __("Error: found multiple groupchat participant based on your arguments");
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- return;
|
|
|
- }
|
|
|
- const nick_or_jid = references.pop().value;
|
|
|
- const reason = args.split(nick_or_jid, 2)[1];
|
|
|
- if (reason && !reason.startsWith(' ')) {
|
|
|
- const message = __("Error: couldn't find a groupchat participant based on your arguments");
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- return;
|
|
|
- }
|
|
|
- return nick_or_jid;
|
|
|
- },
|
|
|
+ async destroy () {
|
|
|
+ const messages = [__('Are you sure you want to destroy this groupchat?')];
|
|
|
+ let fields = [{
|
|
|
+ 'name': 'challenge',
|
|
|
+ 'label': __('Please enter the XMPP address of this groupchat to confirm'),
|
|
|
+ 'challenge': this.model.get('jid'),
|
|
|
+ 'placeholder': __('name@example.org'),
|
|
|
+ 'required': true
|
|
|
+ }, {
|
|
|
+ 'name': 'reason',
|
|
|
+ 'label': __('Optional reason for destroying this groupchat'),
|
|
|
+ 'placeholder': __('Reason')
|
|
|
+ }, {
|
|
|
+ 'name': 'newjid',
|
|
|
+ 'label': __('Optional XMPP address for a new groupchat that replaces this one'),
|
|
|
+ 'placeholder': __('replacement@example.org')
|
|
|
+ }];
|
|
|
+ try {
|
|
|
+ fields = await api.confirm(__('Confirm'), messages, fields);
|
|
|
+ const reason = fields.filter(f => f.name === 'reason').pop()?.value;
|
|
|
+ const newjid = fields.filter(f => f.name === 'newjid').pop()?.value;
|
|
|
+ return this.model.sendDestroyIQ(reason, newjid).then(() => this.close())
|
|
|
+ } catch (e) {
|
|
|
+ log.error(e);
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- setAffiliation (command, args, required_affiliations) {
|
|
|
- const affiliation = COMMAND_TO_AFFILIATION[command];
|
|
|
- if (!affiliation) {
|
|
|
- throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
|
|
|
- }
|
|
|
- if (!this.verifyAffiliations(required_affiliations)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
|
|
|
- if (!nick_or_jid) {
|
|
|
- return false;
|
|
|
+ parseMessageForCommands (text) {
|
|
|
+ if (api.settings.get('muc_disable_slash_commands') &&
|
|
|
+ !Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
|
|
|
+ return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
|
|
|
+ }
|
|
|
+ text = text.replace(/^\s*/, "");
|
|
|
+ const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
|
|
|
+ if (!command) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const args = text.slice(('/'+command).length+1).trim();
|
|
|
+ if (!this.getAllowedCommands().includes(command)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (command) {
|
|
|
+ case 'admin': {
|
|
|
+ this.setAffiliation(command, args, ['owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'ban': {
|
|
|
+ this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'modtools': {
|
|
|
+ this.showModeratorToolsModal(args);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'deop': {
|
|
|
+ // FIXME: /deop only applies to setting a moderators
|
|
|
+ // role to "participant" (which only admin/owner can
|
|
|
+ // do). Moderators can however set non-moderator's role
|
|
|
+ // to participant (e.g. visitor => participant).
|
|
|
+ // Currently we don't distinguish between these two
|
|
|
+ // cases.
|
|
|
+ this.setRole(command, args, ['admin', 'owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'destroy': {
|
|
|
+ if (!this.verifyAffiliations(['owner'])) {
|
|
|
+ break;
|
|
|
}
|
|
|
+ this.destroy().catch(e => this.onCommandError(e));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'help': {
|
|
|
+ this.model.set({'show_help_messages': true});
|
|
|
+ break;
|
|
|
+ } case 'kick': {
|
|
|
+ this.setRole(command, args, [], ['moderator']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'mute': {
|
|
|
+ this.setRole(command, args, [], ['moderator']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'member': {
|
|
|
+ this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'nick': {
|
|
|
+ if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
|
|
|
+ break;
|
|
|
+ } else if (args.length === 0) {
|
|
|
+ // e.g. Your nickname is "coolguy69"
|
|
|
+ const message = __('Your nickname is "%1$s"', this.model.get('nick'));
|
|
|
+ this.model.createMessage({message, 'type': 'error'});
|
|
|
|
|
|
- let jid;
|
|
|
- const reason = args.split(nick_or_jid, 2)[1].trim();
|
|
|
- const occupant = this.model.getOccupant(nick_or_jid);
|
|
|
- if (occupant) {
|
|
|
- jid = occupant.get('jid');
|
|
|
} else {
|
|
|
- if (u.isValidJID(nick_or_jid)) {
|
|
|
- jid = nick_or_jid;
|
|
|
- } else {
|
|
|
- const message = __(
|
|
|
- "Couldn't find a participant with that nickname. "+
|
|
|
- "They might have left the groupchat."
|
|
|
- );
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- const attrs = { jid, reason };
|
|
|
- if (occupant && api.settings.get('auto_register_muc_nickname')) {
|
|
|
- attrs['nick'] = occupant.get('nick');
|
|
|
+ const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
|
|
|
+ api.send($pres({
|
|
|
+ from: _converse.connection.jid,
|
|
|
+ to: `${jid}/${args}`,
|
|
|
+ id: u.getUniqueId()
|
|
|
+ }).tree());
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'owner':
|
|
|
+ this.setAffiliation(command, args, ['owner']);
|
|
|
+ break;
|
|
|
+ case 'op': {
|
|
|
+ this.setRole(command, args, ['admin', 'owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'register': {
|
|
|
+ if (args.length > 1) {
|
|
|
+ this.model.createMessage({
|
|
|
+ 'message': __('Error: invalid number of arguments'),
|
|
|
+ 'type': 'error'
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this.model.registerNickname().then(err_msg => {
|
|
|
+ err_msg && this.model.createMessage({'message': err_msg, 'type': 'error'});
|
|
|
+ });
|
|
|
}
|
|
|
- this.model.setAffiliation(affiliation, [attrs])
|
|
|
- .then(() => this.model.occupants.fetchMembers())
|
|
|
- .catch(err => this.onCommandError(err));
|
|
|
- },
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'revoke': {
|
|
|
+ this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'topic':
|
|
|
+ case 'subject':
|
|
|
+ this.model.setSubject(args);
|
|
|
+ break;
|
|
|
+ case 'voice': {
|
|
|
+ this.setRole(command, args, [], ['moderator']);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
|
|
|
- getReason (args) {
|
|
|
- return args.includes(',') ? args.slice(args.indexOf(',')+1).trim() : null;
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Renders a form given an IQ stanza containing the current
|
|
|
+ * groupchat configuration.
|
|
|
+ * Returns a promise which resolves once the user has
|
|
|
+ * either submitted the form, or canceled it.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#renderConfigurationForm
|
|
|
+ * @param { XMLElement } stanza: The IQ stanza containing the groupchat config.
|
|
|
+ */
|
|
|
+ renderConfigurationForm (stanza) {
|
|
|
+ this.hideChatRoomContents();
|
|
|
+ this.model.save('config_stanza', stanza.outerHTML);
|
|
|
+ if (!this.config_form) {
|
|
|
+ const { _converse } = this.__super__;
|
|
|
+ this.config_form = new _converse.MUCConfigForm({
|
|
|
+ 'model': this.model,
|
|
|
+ 'chatroomview': this
|
|
|
+ });
|
|
|
+ const container_el = this.el.querySelector('.chatroom-body');
|
|
|
+ container_el.insertAdjacentElement('beforeend', this.config_form.el);
|
|
|
+ }
|
|
|
+ u.showElement(this.config_form.el);
|
|
|
+ },
|
|
|
|
|
|
- setRole (command, args, required_affiliations=[], required_roles=[]) {
|
|
|
- /* Check that a command to change a groupchat user's role or
|
|
|
- * affiliation has anough arguments.
|
|
|
- */
|
|
|
- const role = COMMAND_TO_ROLE[command];
|
|
|
- if (!role) {
|
|
|
- throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
|
|
|
- }
|
|
|
- if (!this.verifyAffiliations(required_affiliations) || !this.verifyRoles(required_roles)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
|
|
|
- if (!nick_or_jid) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- const reason = args.split(nick_or_jid, 2)[1].trim();
|
|
|
- // We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
|
|
|
- const occupant = this.model.getOccupant(nick_or_jid);
|
|
|
- this.model.setRole(occupant, role, reason, undefined, this.onCommandError.bind(this));
|
|
|
- return true;
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Renders a form which allows the user to choose theirnickname.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#renderNicknameForm
|
|
|
+ */
|
|
|
+ renderNicknameForm () {
|
|
|
+ const heading = api.settings.get('muc_show_logs_before_join') ?
|
|
|
+ __('Choose a nickname to enter') :
|
|
|
+ __('Please choose your nickname');
|
|
|
+
|
|
|
+ const html = tpl_chatroom_nickname_form(Object.assign({
|
|
|
+ heading,
|
|
|
+ 'label_nickname': __('Nickname'),
|
|
|
+ 'label_join': __('Enter groupchat'),
|
|
|
+ }, this.model.toJSON()));
|
|
|
+
|
|
|
+ if (api.settings.get('muc_show_logs_before_join')) {
|
|
|
+ const container = this.el.querySelector('.muc-bottom-panel');
|
|
|
+ container.innerHTML = html;
|
|
|
+ u.addClass('muc-bottom-panel--nickname', container);
|
|
|
+ } else {
|
|
|
+ const form = this.el.querySelector('.muc-nickname-form');
|
|
|
+ if (form) {
|
|
|
+ sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
+ form.outerHTML = html;
|
|
|
+ } else {
|
|
|
+ this.hideChatRoomContents();
|
|
|
+ const container = this.el.querySelector('.chatroom-body');
|
|
|
+ container.insertAdjacentHTML('beforeend', html);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ u.safeSave(this.model.session, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
|
|
|
+ },
|
|
|
|
|
|
- onCommandError (err) {
|
|
|
- log.fatal(err);
|
|
|
- const message =
|
|
|
- __("Sorry, an error happened while running the command.") + " " +
|
|
|
- __("Check your browser's developer console for details.");
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Remove the configuration form without submitting and return to the chat view.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#closeForm
|
|
|
+ */
|
|
|
+ closeForm () {
|
|
|
+ sizzle('.chatroom-form-container', this.el).forEach(e => u.addClass('hidden', e));
|
|
|
+ this.renderAfterTransition();
|
|
|
+ },
|
|
|
|
|
|
- getAllowedCommands () {
|
|
|
- let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
|
|
|
- if (this.model.config.get('changesubject') || ['owner', 'admin'].includes(this.model.getOwnAffiliation())) {
|
|
|
- allowed_commands = [...allowed_commands, ...['subject', 'topic']];
|
|
|
- }
|
|
|
- const occupant = this.model.occupants.findWhere({'jid': _converse.bare_jid});
|
|
|
- if (this.verifyAffiliations(['owner'], occupant, false)) {
|
|
|
- allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
|
|
|
- } else if (this.verifyAffiliations(['admin'], occupant, false)) {
|
|
|
- allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
|
|
|
- }
|
|
|
- if (this.verifyRoles(['moderator'], occupant, false)) {
|
|
|
- allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
|
|
|
- } else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
|
|
|
- allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
|
|
|
- }
|
|
|
- allowed_commands.sort();
|
|
|
+ /**
|
|
|
+ * Start the process of configuring a groupchat, either by
|
|
|
+ * rendering a configuration form, or by auto-configuring
|
|
|
+ * based on the "roomconfig" data stored on the
|
|
|
+ * {@link _converse.ChatRoom}.
|
|
|
+ * Stores the new configuration on the {@link _converse.ChatRoom}
|
|
|
+ * once completed.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#getAndRenderConfigurationForm
|
|
|
+ * @param { Event } ev - DOM event that might be passed in if this
|
|
|
+ * method is called due to a user action. In this
|
|
|
+ * case, auto-configure won't happen, regardless of
|
|
|
+ * the settings.
|
|
|
+ */
|
|
|
+ getAndRenderConfigurationForm () {
|
|
|
+ if (!this.config_form || !u.isVisible(this.config_form.el)) {
|
|
|
+ this.showSpinner();
|
|
|
+ this.model.fetchRoomConfiguration()
|
|
|
+ .then(iq => this.renderConfigurationForm(iq))
|
|
|
+ .catch(e => log.error(e));
|
|
|
+ } else {
|
|
|
+ this.closeForm();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- if (Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
|
|
|
- return allowed_commands.filter(c => !api.settings.get('muc_disable_slash_commands').includes(c));
|
|
|
- } else {
|
|
|
- return allowed_commands;
|
|
|
- }
|
|
|
- },
|
|
|
+ hideChatRoomContents () {
|
|
|
+ const container_el = this.el.querySelector('.chatroom-body');
|
|
|
+ if (container_el !== null) {
|
|
|
+ [].forEach.call(container_el.children, child => child.classList.add('hidden'));
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- async destroy () {
|
|
|
- const messages = [__('Are you sure you want to destroy this groupchat?')];
|
|
|
- let fields = [{
|
|
|
- 'name': 'challenge',
|
|
|
- 'label': __('Please enter the XMPP address of this groupchat to confirm'),
|
|
|
- 'challenge': this.model.get('jid'),
|
|
|
- 'placeholder': __('name@example.org'),
|
|
|
- 'required': true
|
|
|
- }, {
|
|
|
- 'name': 'reason',
|
|
|
- 'label': __('Optional reason for destroying this groupchat'),
|
|
|
- 'placeholder': __('Reason')
|
|
|
- }, {
|
|
|
- 'name': 'newjid',
|
|
|
- 'label': __('Optional XMPP address for a new groupchat that replaces this one'),
|
|
|
- 'placeholder': __('replacement@example.org')
|
|
|
- }];
|
|
|
- try {
|
|
|
- fields = await api.confirm(__('Confirm'), messages, fields);
|
|
|
- const reason = fields.filter(f => f.name === 'reason').pop()?.value;
|
|
|
- const newjid = fields.filter(f => f.name === 'newjid').pop()?.value;
|
|
|
- return this.model.sendDestroyIQ(reason, newjid).then(() => this.close())
|
|
|
- } catch (e) {
|
|
|
- log.error(e);
|
|
|
- }
|
|
|
- },
|
|
|
+ renderPasswordForm () {
|
|
|
+ this.hideChatRoomContents();
|
|
|
+ const message = this.model.get('password_validation_message');
|
|
|
+ this.model.save('password_validation_message', undefined);
|
|
|
+
|
|
|
+ if (!this.password_form) {
|
|
|
+ this.password_form = new _converse.MUCPasswordForm({
|
|
|
+ 'model': new Model({
|
|
|
+ 'validation_message': message
|
|
|
+ }),
|
|
|
+ 'chatroomview': this,
|
|
|
+ });
|
|
|
+ const container_el = this.el.querySelector('.chatroom-body');
|
|
|
+ container_el.insertAdjacentElement('beforeend', this.password_form.el);
|
|
|
+ } else {
|
|
|
+ this.password_form.model.set('validation_message', message);
|
|
|
+ }
|
|
|
+ u.showElement(this.password_form.el);
|
|
|
+ this.model.session.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
|
|
|
+ },
|
|
|
|
|
|
- parseMessageForCommands (text) {
|
|
|
- if (api.settings.get('muc_disable_slash_commands') &&
|
|
|
- !Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
|
|
|
- return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
|
|
|
- }
|
|
|
- text = text.replace(/^\s*/, "");
|
|
|
- const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
|
|
|
- if (!command) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- const args = text.slice(('/'+command).length+1).trim();
|
|
|
- if (!this.getAllowedCommands().includes(command)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
+ showDestroyedMessage () {
|
|
|
+ u.hideElement(this.el.querySelector('.chat-area'));
|
|
|
+ u.hideElement(this.el.querySelector('.occupants'));
|
|
|
+ sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
|
|
|
- switch (command) {
|
|
|
- case 'admin': {
|
|
|
- this.setAffiliation(command, args, ['owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'ban': {
|
|
|
- this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'modtools': {
|
|
|
- this.showModeratorToolsModal(args);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'deop': {
|
|
|
- // FIXME: /deop only applies to setting a moderators
|
|
|
- // role to "participant" (which only admin/owner can
|
|
|
- // do). Moderators can however set non-moderator's role
|
|
|
- // to participant (e.g. visitor => participant).
|
|
|
- // Currently we don't distinguish between these two
|
|
|
- // cases.
|
|
|
- this.setRole(command, args, ['admin', 'owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'destroy': {
|
|
|
- if (!this.verifyAffiliations(['owner'])) {
|
|
|
- break;
|
|
|
- }
|
|
|
- this.destroy().catch(e => this.onCommandError(e));
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'help': {
|
|
|
- this.model.set({'show_help_messages': true});
|
|
|
- break;
|
|
|
- } case 'kick': {
|
|
|
- this.setRole(command, args, [], ['moderator']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'mute': {
|
|
|
- this.setRole(command, args, [], ['moderator']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'member': {
|
|
|
- this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'nick': {
|
|
|
- if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
|
|
|
- break;
|
|
|
- } else if (args.length === 0) {
|
|
|
- // e.g. Your nickname is "coolguy69"
|
|
|
- const message = __('Your nickname is "%1$s"', this.model.get('nick'));
|
|
|
- this.model.createMessage({message, 'type': 'error'});
|
|
|
+ const reason = this.model.get('destroyed_reason');
|
|
|
+ const moved_jid = this.model.get('moved_jid');
|
|
|
+ this.model.save({
|
|
|
+ 'destroyed_reason': undefined,
|
|
|
+ 'moved_jid': undefined
|
|
|
+ });
|
|
|
+ const container = this.el.querySelector('.disconnect-container');
|
|
|
+ container.innerHTML = tpl_chatroom_destroyed({
|
|
|
+ '__':__,
|
|
|
+ 'jid': moved_jid,
|
|
|
+ 'reason': reason ? `"${reason}"` : null
|
|
|
+ });
|
|
|
+ const switch_el = container.querySelector('a.switch-chat');
|
|
|
+ if (switch_el) {
|
|
|
+ switch_el.addEventListener('click', async ev => {
|
|
|
+ ev.preventDefault();
|
|
|
+ const room = await api.rooms.get(moved_jid, null, true);
|
|
|
+ room.maybeShow(true);
|
|
|
+ this.model.destroy();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ u.showElement(container);
|
|
|
+ },
|
|
|
|
|
|
- } else {
|
|
|
- const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
|
|
|
- api.send($pres({
|
|
|
- from: _converse.connection.jid,
|
|
|
- to: `${jid}/${args}`,
|
|
|
- id: u.getUniqueId()
|
|
|
- }).tree());
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'owner':
|
|
|
- this.setAffiliation(command, args, ['owner']);
|
|
|
- break;
|
|
|
- case 'op': {
|
|
|
- this.setRole(command, args, ['admin', 'owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'register': {
|
|
|
- if (args.length > 1) {
|
|
|
- this.model.createMessage({
|
|
|
- 'message': __('Error: invalid number of arguments'),
|
|
|
- 'type': 'error'
|
|
|
- });
|
|
|
- } else {
|
|
|
- this.model.registerNickname().then(err_msg => {
|
|
|
- err_msg && this.model.createMessage({'message': err_msg, 'type': 'error'});
|
|
|
- });
|
|
|
- }
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'revoke': {
|
|
|
- this.setAffiliation(command, args, ['admin', 'owner']);
|
|
|
- break;
|
|
|
- }
|
|
|
- case 'topic':
|
|
|
- case 'subject':
|
|
|
- this.model.setSubject(args);
|
|
|
- break;
|
|
|
- case 'voice': {
|
|
|
- this.setRole(command, args, [], ['moderator']);
|
|
|
- break;
|
|
|
- }
|
|
|
- default:
|
|
|
- return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
|
|
|
- }
|
|
|
- return true;
|
|
|
- },
|
|
|
+ showDisconnectMessage () {
|
|
|
+ const message = this.model.get('disconnection_message');
|
|
|
+ if (!message) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ u.hideElement(this.el.querySelector('.chat-area'));
|
|
|
+ u.hideElement(this.el.querySelector('.occupants'));
|
|
|
+ sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
+
|
|
|
+ const messages = [message];
|
|
|
+ const actor = this.model.get('disconnection_actor');
|
|
|
+ if (actor) {
|
|
|
+ messages.push(__('This action was done by %1$s.', actor));
|
|
|
+ }
|
|
|
+ const reason = this.model.get('disconnection_reason');
|
|
|
+ if (reason) {
|
|
|
+ messages.push(__('The reason given is: "%1$s".', reason));
|
|
|
+ }
|
|
|
+ this.model.save({
|
|
|
+ 'disconnection_message': undefined,
|
|
|
+ 'disconnection_reason': undefined,
|
|
|
+ 'disconnection_actor': undefined
|
|
|
+ });
|
|
|
+ const container = this.el.querySelector('.disconnect-container');
|
|
|
+ container.innerHTML = tpl_chatroom_disconnect({messages})
|
|
|
+ u.showElement(container);
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Renders a form given an IQ stanza containing the current
|
|
|
- * groupchat configuration.
|
|
|
- * Returns a promise which resolves once the user has
|
|
|
- * either submitted the form, or canceled it.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#renderConfigurationForm
|
|
|
- * @param { XMLElement } stanza: The IQ stanza containing the groupchat config.
|
|
|
- */
|
|
|
- renderConfigurationForm (stanza) {
|
|
|
- this.hideChatRoomContents();
|
|
|
- this.model.save('config_stanza', stanza.outerHTML);
|
|
|
- if (!this.config_form) {
|
|
|
- const { _converse } = this.__super__;
|
|
|
- this.config_form = new _converse.MUCConfigForm({
|
|
|
- 'model': this.model,
|
|
|
- 'chatroomview': this
|
|
|
- });
|
|
|
- const container_el = this.el.querySelector('.chatroom-body');
|
|
|
- container_el.insertAdjacentElement('beforeend', this.config_form.el);
|
|
|
- }
|
|
|
- u.showElement(this.config_form.el);
|
|
|
- },
|
|
|
+ onOccupantAdded (occupant) {
|
|
|
+ if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
+ this.renderHeading();
|
|
|
+ this.renderBottomPanel();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Renders a form which allows the user to choose theirnickname.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#renderNicknameForm
|
|
|
- */
|
|
|
- renderNicknameForm () {
|
|
|
- const heading = api.settings.get('muc_show_logs_before_join') ?
|
|
|
- __('Choose a nickname to enter') :
|
|
|
- __('Please choose your nickname');
|
|
|
-
|
|
|
- const html = tpl_chatroom_nickname_form(Object.assign({
|
|
|
- heading,
|
|
|
- 'label_nickname': __('Nickname'),
|
|
|
- 'label_join': __('Enter groupchat'),
|
|
|
- }, this.model.toJSON()));
|
|
|
-
|
|
|
- if (api.settings.get('muc_show_logs_before_join')) {
|
|
|
- const container = this.el.querySelector('.muc-bottom-panel');
|
|
|
- container.innerHTML = html;
|
|
|
- u.addClass('muc-bottom-panel--nickname', container);
|
|
|
- } else {
|
|
|
- const form = this.el.querySelector('.muc-nickname-form');
|
|
|
- if (form) {
|
|
|
- sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
- form.outerHTML = html;
|
|
|
- } else {
|
|
|
- this.hideChatRoomContents();
|
|
|
- const container = this.el.querySelector('.chatroom-body');
|
|
|
- container.insertAdjacentHTML('beforeend', html);
|
|
|
- }
|
|
|
- }
|
|
|
- u.safeSave(this.model.session, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Working backwards, get today's most recent join/leave notification
|
|
|
+ * from the same user (if any exists) after the most recent chat message.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#getPreviousJoinOrLeaveNotification
|
|
|
+ * @param {HTMLElement} el
|
|
|
+ * @param {string} nick
|
|
|
+ */
|
|
|
+ getPreviousJoinOrLeaveNotification (el, nick) {
|
|
|
+ const today = (new Date()).toISOString().split('T')[0];
|
|
|
+ while (el !== null) {
|
|
|
+ if (!el.classList.contains('chat-info')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Check whether el is still from today.
|
|
|
+ // We don't use `Dayjs.same` here, since it's about 4 times slower.
|
|
|
+ const date = el.getAttribute('data-isodate');
|
|
|
+ if (date && date.split('T')[0] !== today) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const data = el?.dataset || {};
|
|
|
+ if (data.join === nick ||
|
|
|
+ data.leave === nick ||
|
|
|
+ data.leavejoin === nick ||
|
|
|
+ data.joinleave === nick) {
|
|
|
+ return el;
|
|
|
+ }
|
|
|
+ el = el.previousElementSibling;
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Remove the configuration form without submitting and return to the chat view.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#closeForm
|
|
|
- */
|
|
|
- closeForm () {
|
|
|
- sizzle('.chatroom-form-container', this.el).forEach(e => u.addClass('hidden', e));
|
|
|
- this.renderAfterTransition();
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Rerender the groupchat after some kind of transition. For
|
|
|
+ * example after the spinner has been removed or after a
|
|
|
+ * form has been submitted and removed.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#renderAfterTransition
|
|
|
+ */
|
|
|
+ renderAfterTransition () {
|
|
|
+ const conn_status = this.model.session.get('connection_status')
|
|
|
+ if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
|
|
|
+ this.renderNicknameForm();
|
|
|
+ } else if (conn_status == converse.ROOMSTATUS.PASSWORD_REQUIRED) {
|
|
|
+ this.renderPasswordForm();
|
|
|
+ } else if (conn_status == converse.ROOMSTATUS.ENTERED) {
|
|
|
+ this.hideChatRoomContents();
|
|
|
+ u.showElement(this.el.querySelector('.chat-area'));
|
|
|
+ u.showElement(this.el.querySelector('.occupants'));
|
|
|
+ this.scrollDown();
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- /**
|
|
|
- * Start the process of configuring a groupchat, either by
|
|
|
- * rendering a configuration form, or by auto-configuring
|
|
|
- * based on the "roomconfig" data stored on the
|
|
|
- * {@link _converse.ChatRoom}.
|
|
|
- * Stores the new configuration on the {@link _converse.ChatRoom}
|
|
|
- * once completed.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#getAndRenderConfigurationForm
|
|
|
- * @param { Event } ev - DOM event that might be passed in if this
|
|
|
- * method is called due to a user action. In this
|
|
|
- * case, auto-configure won't happen, regardless of
|
|
|
- * the settings.
|
|
|
- */
|
|
|
- getAndRenderConfigurationForm () {
|
|
|
- if (!this.config_form || !u.isVisible(this.config_form.el)) {
|
|
|
- this.showSpinner();
|
|
|
- this.model.fetchRoomConfiguration()
|
|
|
- .then(iq => this.renderConfigurationForm(iq))
|
|
|
- .catch(e => log.error(e));
|
|
|
- } else {
|
|
|
- this.closeForm();
|
|
|
- }
|
|
|
- },
|
|
|
+ showSpinner () {
|
|
|
+ sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
+ this.hideChatRoomContents();
|
|
|
+ const container_el = this.el.querySelector('.chatroom-body');
|
|
|
+ container_el.insertAdjacentHTML('afterbegin', tpl_spinner());
|
|
|
+ },
|
|
|
|
|
|
- hideChatRoomContents () {
|
|
|
- const container_el = this.el.querySelector('.chatroom-body');
|
|
|
- if (container_el !== null) {
|
|
|
- [].forEach.call(container_el.children, child => child.classList.add('hidden'));
|
|
|
- }
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * Check if the spinner is being shown and if so, hide it.
|
|
|
+ * Also make sure then that the chat area and occupants
|
|
|
+ * list are both visible.
|
|
|
+ * @private
|
|
|
+ * @method _converse.ChatRoomView#hideSpinner
|
|
|
+ */
|
|
|
+ hideSpinner () {
|
|
|
+ const spinner = this.el.querySelector('.spinner');
|
|
|
+ if (spinner !== null) {
|
|
|
+ u.removeElement(spinner);
|
|
|
+ this.renderAfterTransition();
|
|
|
+ }
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
- renderPasswordForm () {
|
|
|
- this.hideChatRoomContents();
|
|
|
- const message = this.model.get('password_validation_message');
|
|
|
- this.model.save('password_validation_message', undefined);
|
|
|
-
|
|
|
- if (!this.password_form) {
|
|
|
- this.password_form = new _converse.MUCPasswordForm({
|
|
|
- 'model': new Model({
|
|
|
- 'validation_message': message
|
|
|
- }),
|
|
|
- 'chatroomview': this,
|
|
|
- });
|
|
|
- const container_el = this.el.querySelector('.chatroom-body');
|
|
|
- container_el.insertAdjacentElement('beforeend', this.password_form.el);
|
|
|
- } else {
|
|
|
- this.password_form.model.set('validation_message', message);
|
|
|
- }
|
|
|
- u.showElement(this.password_form.el);
|
|
|
- this.model.session.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
|
|
|
- },
|
|
|
|
|
|
- showDestroyedMessage () {
|
|
|
- u.hideElement(this.el.querySelector('.chat-area'));
|
|
|
- u.hideElement(this.el.querySelector('.occupants'));
|
|
|
- sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
+/**
|
|
|
+ * View which renders MUC section of the control box.
|
|
|
+ * @class
|
|
|
+ * @namespace _converse.RoomsPanel
|
|
|
+ * @memberOf _converse
|
|
|
+ */
|
|
|
+export const RoomsPanel = View.extend({
|
|
|
+ tagName: 'div',
|
|
|
+ className: 'controlbox-section',
|
|
|
+ id: 'chatrooms',
|
|
|
+ events: {
|
|
|
+ 'click a.controlbox-heading__btn.show-add-muc-modal': 'showAddRoomModal',
|
|
|
+ 'click a.controlbox-heading__btn.show-list-muc-modal': 'showMUCListModal'
|
|
|
+ },
|
|
|
|
|
|
- const reason = this.model.get('destroyed_reason');
|
|
|
- const moved_jid = this.model.get('moved_jid');
|
|
|
- this.model.save({
|
|
|
- 'destroyed_reason': undefined,
|
|
|
- 'moved_jid': undefined
|
|
|
- });
|
|
|
- const container = this.el.querySelector('.disconnect-container');
|
|
|
- container.innerHTML = tpl_chatroom_destroyed({
|
|
|
- '__':__,
|
|
|
- 'jid': moved_jid,
|
|
|
- 'reason': reason ? `"${reason}"` : null
|
|
|
- });
|
|
|
- const switch_el = container.querySelector('a.switch-chat');
|
|
|
- if (switch_el) {
|
|
|
- switch_el.addEventListener('click', async ev => {
|
|
|
- ev.preventDefault();
|
|
|
- const room = await api.rooms.get(moved_jid, null, true);
|
|
|
- room.maybeShow(true);
|
|
|
- this.model.destroy();
|
|
|
- });
|
|
|
- }
|
|
|
- u.showElement(container);
|
|
|
- },
|
|
|
+ render () {
|
|
|
+ this.el.innerHTML = tpl_room_panel({
|
|
|
+ 'heading_chatrooms': __('Groupchats'),
|
|
|
+ 'title_new_room': __('Add a new groupchat'),
|
|
|
+ 'title_list_rooms': __('Query for groupchats')
|
|
|
+ });
|
|
|
+ return this;
|
|
|
+ },
|
|
|
|
|
|
- showDisconnectMessage () {
|
|
|
- const message = this.model.get('disconnection_message');
|
|
|
- if (!message) {
|
|
|
- return;
|
|
|
- }
|
|
|
- u.hideElement(this.el.querySelector('.chat-area'));
|
|
|
- u.hideElement(this.el.querySelector('.occupants'));
|
|
|
- sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
+ showAddRoomModal (ev) {
|
|
|
+ if (this.add_room_modal === undefined) {
|
|
|
+ this.add_room_modal = new AddMUCModal({'model': this.model});
|
|
|
+ }
|
|
|
+ this.add_room_modal.show(ev);
|
|
|
+ },
|
|
|
|
|
|
- const messages = [message];
|
|
|
- const actor = this.model.get('disconnection_actor');
|
|
|
- if (actor) {
|
|
|
- messages.push(__('This action was done by %1$s.', actor));
|
|
|
- }
|
|
|
- const reason = this.model.get('disconnection_reason');
|
|
|
- if (reason) {
|
|
|
- messages.push(__('The reason given is: "%1$s".', reason));
|
|
|
- }
|
|
|
- this.model.save({
|
|
|
- 'disconnection_message': undefined,
|
|
|
- 'disconnection_reason': undefined,
|
|
|
- 'disconnection_actor': undefined
|
|
|
- });
|
|
|
- const container = this.el.querySelector('.disconnect-container');
|
|
|
- container.innerHTML = tpl_chatroom_disconnect({messages})
|
|
|
- u.showElement(container);
|
|
|
- },
|
|
|
+ showMUCListModal(ev) {
|
|
|
+ if (this.muc_list_modal === undefined) {
|
|
|
+ this.muc_list_modal = new MUCListModal({'model': this.model});
|
|
|
+ }
|
|
|
+ this.muc_list_modal.show(ev);
|
|
|
+ }
|
|
|
+});
|
|
|
|
|
|
- onOccupantAdded (occupant) {
|
|
|
- if (occupant.get('jid') === _converse.bare_jid) {
|
|
|
- this.renderHeading();
|
|
|
- this.renderBottomPanel();
|
|
|
- }
|
|
|
- },
|
|
|
|
|
|
- /**
|
|
|
- * Working backwards, get today's most recent join/leave notification
|
|
|
- * from the same user (if any exists) after the most recent chat message.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#getPreviousJoinOrLeaveNotification
|
|
|
- * @param {HTMLElement} el
|
|
|
- * @param {string} nick
|
|
|
- */
|
|
|
- getPreviousJoinOrLeaveNotification (el, nick) {
|
|
|
- const today = (new Date()).toISOString().split('T')[0];
|
|
|
- while (el !== null) {
|
|
|
- if (!el.classList.contains('chat-info')) {
|
|
|
- return;
|
|
|
- }
|
|
|
- // Check whether el is still from today.
|
|
|
- // We don't use `Dayjs.same` here, since it's about 4 times slower.
|
|
|
- const date = el.getAttribute('data-isodate');
|
|
|
- if (date && date.split('T')[0] !== today) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const data = el?.dataset || {};
|
|
|
- if (data.join === nick ||
|
|
|
- data.leave === nick ||
|
|
|
- data.leavejoin === nick ||
|
|
|
- data.joinleave === nick) {
|
|
|
- return el;
|
|
|
- }
|
|
|
- el = el.previousElementSibling;
|
|
|
- }
|
|
|
- },
|
|
|
+converse.plugins.add('converse-muc-views', {
|
|
|
+ /* Dependencies are other plugins which might be
|
|
|
+ * overridden or relied upon, and therefore need to be loaded before
|
|
|
+ * this plugin. They are "optional" because they might not be
|
|
|
+ * available, in which case any overrides applicable to them will be
|
|
|
+ * ignored.
|
|
|
+ *
|
|
|
+ * NB: These plugins need to have already been loaded via require.js.
|
|
|
+ *
|
|
|
+ * It's possible to make these dependencies "non-optional".
|
|
|
+ * If the setting "strict_plugin_dependencies" is set to true,
|
|
|
+ * an error will be raised if the plugin is not found.
|
|
|
+ */
|
|
|
+ dependencies: ["converse-autocomplete", "converse-modal", "converse-controlbox", "converse-chatview"],
|
|
|
|
|
|
- /**
|
|
|
- * Rerender the groupchat after some kind of transition. For
|
|
|
- * example after the spinner has been removed or after a
|
|
|
- * form has been submitted and removed.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#renderAfterTransition
|
|
|
- */
|
|
|
- renderAfterTransition () {
|
|
|
- const conn_status = this.model.session.get('connection_status')
|
|
|
- if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
|
|
|
- this.renderNicknameForm();
|
|
|
- } else if (conn_status == converse.ROOMSTATUS.PASSWORD_REQUIRED) {
|
|
|
- this.renderPasswordForm();
|
|
|
- } else if (conn_status == converse.ROOMSTATUS.ENTERED) {
|
|
|
- this.hideChatRoomContents();
|
|
|
- u.showElement(this.el.querySelector('.chat-area'));
|
|
|
- u.showElement(this.el.querySelector('.occupants'));
|
|
|
- this.scrollDown();
|
|
|
+ overrides: {
|
|
|
+ ControlBoxView: {
|
|
|
+ renderControlBoxPane () {
|
|
|
+ this.__super__.renderControlBoxPane.apply(this, arguments);
|
|
|
+ if (api.settings.get('allow_muc')) {
|
|
|
+ this.renderRoomsPanel();
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- showSpinner () {
|
|
|
- sizzle('.spinner', this.el).forEach(u.removeElement);
|
|
|
- this.hideChatRoomContents();
|
|
|
- const container_el = this.el.querySelector('.chatroom-body');
|
|
|
- container_el.insertAdjacentHTML('afterbegin', tpl_spinner());
|
|
|
- },
|
|
|
+ initialize () {
|
|
|
+ const { _converse } = this;
|
|
|
|
|
|
- /**
|
|
|
- * Check if the spinner is being shown and if so, hide it.
|
|
|
- * Also make sure then that the chat area and occupants
|
|
|
- * list are both visible.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatRoomView#hideSpinner
|
|
|
- */
|
|
|
- hideSpinner () {
|
|
|
- const spinner = this.el.querySelector('.spinner');
|
|
|
- if (spinner !== null) {
|
|
|
- u.removeElement(spinner);
|
|
|
- this.renderAfterTransition();
|
|
|
- }
|
|
|
- return this;
|
|
|
+ api.promises.add(['roomsPanelRendered']);
|
|
|
+
|
|
|
+ // Configuration values for this plugin
|
|
|
+ // ====================================
|
|
|
+ // Refer to docs/source/configuration.rst for explanations of these
|
|
|
+ // configuration settings.
|
|
|
+ api.settings.extend({
|
|
|
+ 'auto_list_rooms': false,
|
|
|
+ 'cache_muc_messages': true,
|
|
|
+ 'locked_muc_nickname': false,
|
|
|
+ 'modtools_disable_query': [],
|
|
|
+ 'modtools_disable_assign': false,
|
|
|
+ 'muc_disable_slash_commands': false,
|
|
|
+ 'muc_mention_autocomplete_filter': 'contains',
|
|
|
+ 'muc_mention_autocomplete_min_chars': 0,
|
|
|
+ 'muc_mention_autocomplete_show_avatar': true,
|
|
|
+ 'muc_roomid_policy': null,
|
|
|
+ 'muc_roomid_policy_hint': null,
|
|
|
+ 'roomconfig_whitelist': [],
|
|
|
+ 'show_retraction_warning': true,
|
|
|
+ 'visible_toolbar_buttons': {
|
|
|
+ 'toggle_occupants': true
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
- /**
|
|
|
- * View which renders MUC section of the control box.
|
|
|
- * @class
|
|
|
- * @namespace _converse.RoomsPanel
|
|
|
- * @memberOf _converse
|
|
|
- */
|
|
|
- _converse.RoomsPanel = View.extend({
|
|
|
- tagName: 'div',
|
|
|
- className: 'controlbox-section',
|
|
|
- id: 'chatrooms',
|
|
|
- events: {
|
|
|
- 'click a.controlbox-heading__btn.show-add-muc-modal': 'showAddRoomModal',
|
|
|
- 'click a.controlbox-heading__btn.show-list-muc-modal': 'showMUCListModal'
|
|
|
- },
|
|
|
+ _converse.ChatRoomView = ChatRoomView;
|
|
|
+ _converse.RoomsPanel = RoomsPanel;
|
|
|
|
|
|
- render () {
|
|
|
- this.el.innerHTML = tpl_room_panel({
|
|
|
- 'heading_chatrooms': __('Groupchats'),
|
|
|
- 'title_new_room': __('Add a new groupchat'),
|
|
|
- 'title_list_rooms': __('Query for groupchats')
|
|
|
- });
|
|
|
- return this;
|
|
|
- },
|
|
|
|
|
|
- showAddRoomModal (ev) {
|
|
|
- if (this.add_room_modal === undefined) {
|
|
|
- this.add_room_modal = new AddMUCModal({'model': this.model});
|
|
|
+ const viewWithRoomsPanel = {
|
|
|
+ renderRoomsPanel () {
|
|
|
+ if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
|
|
|
+ return this.roomspanel;
|
|
|
}
|
|
|
- this.add_room_modal.show(ev);
|
|
|
+ const id = `converse.roomspanel${_converse.bare_jid}`;
|
|
|
+
|
|
|
+ this.roomspanel = new _converse.RoomsPanel({
|
|
|
+ 'model': new (_converse.RoomsPanelModel.extend({
|
|
|
+ id,
|
|
|
+ 'browserStorage': _converse.createStore(id)
|
|
|
+ }))()
|
|
|
+ });
|
|
|
+ this.roomspanel.model.fetch();
|
|
|
+ this.el.querySelector('.controlbox-pane').insertAdjacentElement(
|
|
|
+ 'beforeEnd', this.roomspanel.render().el);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Triggered once the section of the { @link _converse.ControlBoxView }
|
|
|
+ * which shows gropuchats has been rendered.
|
|
|
+ * @event _converse#roomsPanelRendered
|
|
|
+ * @example _converse.api.listen.on('roomsPanelRendered', () => { ... });
|
|
|
+ */
|
|
|
+ api.trigger('roomsPanelRendered');
|
|
|
+ return this.roomspanel;
|
|
|
},
|
|
|
|
|
|
- showMUCListModal(ev) {
|
|
|
- if (this.muc_list_modal === undefined) {
|
|
|
- this.muc_list_modal = new MUCListModal({'model': this.model});
|
|
|
+ getRoomsPanel () {
|
|
|
+ if (this.roomspanel && u.isInDOM(this.roomspanel.el)) {
|
|
|
+ return this.roomspanel;
|
|
|
+ } else {
|
|
|
+ return this.renderRoomsPanel();
|
|
|
}
|
|
|
- this.muc_list_modal.show(ev);
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_converse.ControlBoxView) {
|
|
|
+ Object.assign(_converse.ControlBoxView.prototype, viewWithRoomsPanel);
|
|
|
+ }
|
|
|
|
|
|
|
|
|
_converse.MUCConfigForm = View.extend({
|