Add modal for changing your nickname...

once you're already in a MUC.
This commit is contained in:
JC Brand 2022-02-06 16:15:04 +01:00
parent ba52defdae
commit f4fdc36d31
11 changed files with 126 additions and 29 deletions

View File

@ -5,8 +5,8 @@
- Updated translations: lt - Updated translations: lt
- Increased stanza timeout from 10 to 20 seconds - Increased stanza timeout from 10 to 20 seconds
- Replace various font icons with SVG icons - Replace various font icons with SVG icons
- Fix OMEMO race condition related to automatic reconnection
- #1761: Add a new dark theme based on the [Dracula](https://draculatheme.com/) theme - #1761: Add a new dark theme based on the [Dracula](https://draculatheme.com/) theme
- #2733: Fix OMEMO race condition related to automatic reconnection
- #2751: Media not rendered when Converse runs in a browser extension - #2751: Media not rendered when Converse runs in a browser extension
- #2786: Fix webpack configuration not working on Windows OS - #2786: Fix webpack configuration not working on Windows OS
- #2788: `TypeError` when trying to use `@converse/headless` - #2788: `TypeError` when trying to use `@converse/headless`

View File

@ -124,6 +124,10 @@ const ChatRoomMixin = {
this.initialized.resolve(); this.initialized.resolve();
}, },
isEntered () {
return this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
},
/** /**
* Checks whether we're still joined and if so, restores the MUC state from cache. * Checks whether we're still joined and if so, restores the MUC state from cache.
* @private * @private
@ -131,7 +135,7 @@ const ChatRoomMixin = {
* @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`. * @returns { Boolean } Returns `true` if we're still joined, otherwise returns `false`.
*/ */
async restoreFromCache () { async restoreFromCache () {
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED && (await this.isJoined())) { if (this.isEntered() && (await this.isJoined())) {
// We've restored the room from cache and we're still joined. // We've restored the room from cache and we're still joined.
await new Promise(resolve => this.features.fetch({ 'success': resolve, 'error': resolve })); await new Promise(resolve => this.features.fetch({ 'success': resolve, 'error': resolve }));
await this.fetchOccupants().catch(e => log.error(e)); await this.fetchOccupants().catch(e => log.error(e));
@ -154,7 +158,7 @@ const ChatRoomMixin = {
* model (if available). * model (if available).
*/ */
async join (nick, password) { async join (nick, password) {
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED) { if (this.isEntered()) {
// We have restored a groupchat from session storage, // We have restored a groupchat from session storage,
// so we don't send out a presence stanza again. // so we don't send out a presence stanza again.
return this; return this;
@ -293,7 +297,7 @@ const ChatRoomMixin = {
onOccupantRemoved (occupant) { onOccupantRemoved (occupant) {
if ( if (
_converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED) && _converse.isInfoVisible(converse.MUC_TRAFFIC_STATES.EXITED) &&
this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED && this.isEntered() &&
occupant.get('show') === 'online' occupant.get('show') === 'online'
) { ) {
this.updateNotifications(occupant.get('nick'), converse.MUC_TRAFFIC_STATES.EXITED); this.updateNotifications(occupant.get('nick'), converse.MUC_TRAFFIC_STATES.EXITED);
@ -339,7 +343,7 @@ const ChatRoomMixin = {
}, },
async onConnectionStatusChanged () { async onConnectionStatusChanged () {
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED) { if (this.isEntered()) {
if (this.get('hidden') && api.settings.get('muc_subscribe_to_rai') && this.getOwnAffiliation() !== 'none') { if (this.get('hidden') && api.settings.get('muc_subscribe_to_rai') && this.getOwnAffiliation() !== 'none') {
try { try {
await this.leave(); await this.leave();
@ -491,8 +495,7 @@ const ChatRoomMixin = {
* @param { XMLElement } stanza * @param { XMLElement } stanza
*/ */
handleMessageFromMUCHost (stanza) { handleMessageFromMUCHost (stanza) {
const conn_status = this.session.get('connection_status'); if (this.isEntered()) {
if (conn_status === converse.ROOMSTATUS.ENTERED) {
// We're not interested in activity indicators when already joined to the room // We're not interested in activity indicators when already joined to the room
return; return;
} }
@ -513,7 +516,7 @@ const ChatRoomMixin = {
* @param { XMLElement } stanza * @param { XMLElement } stanza
*/ */
handleForwardedMentions (stanza) { handleForwardedMentions (stanza) {
if (this.session.get('connection_status') === converse.ROOMSTATUS.ENTERED) { if (this.isEntered()) {
// Avoid counting mentions twice // Avoid counting mentions twice
return; return;
} }
@ -1023,7 +1026,7 @@ const ChatRoomMixin = {
if ( if (
!api.settings.get('send_chat_state_notifications') || !api.settings.get('send_chat_state_notifications') ||
!this.get('chat_state') || !this.get('chat_state') ||
this.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED || !this.isEntered() ||
(this.features.get('moderated') && this.getOwnRole() === 'visitor') (this.features.get('moderated') && this.getOwnRole() === 'visitor')
) { ) {
return; return;
@ -1404,6 +1407,10 @@ const ChatRoomMixin = {
return this.occupants.getOwnOccupant(); return this.occupants.getOwnOccupant();
}, },
/**
* Send a presence stanza to update the user's nickname in this MUC.
* @param { String } nick
*/
async setNickname (nick) { async setNickname (nick) {
if ( if (
api.settings.get('auto_register_muc_nickname') && api.settings.get('auto_register_muc_nickname') &&

View File

@ -1,5 +1,6 @@
import { ElementView } from '@converse/skeletor/src/element.js'; import { ElementView } from '@converse/skeletor/src/element.js';
import MUCInviteModal from 'modals/muc-invite.js'; import MUCInviteModal from 'modals/muc-invite.js';
import NicknameModal from './modals/nickname.js';
import RoomDetailsModal from 'modals/muc-details.js'; import RoomDetailsModal from 'modals/muc-details.js';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import tpl_muc_head from './templates/muc-head.js'; import tpl_muc_head from './templates/muc-head.js';
@ -107,6 +108,15 @@ export default class MUCHeading extends ElementView {
}); });
} }
buttons.push({
'i18n_text': __('Nickname'),
'i18n_title': __("Change the nickname you're using in this groupchat"),
'handler': ev => api.modal.show(NicknameModal, { 'model': this.model }, ev),
'a_class': 'open-nickname-modal',
'icon_class': 'fa-smile',
'name': 'nickname'
});
if (this.model.invitesAllowed()) { if (this.model.invitesAllowed()) {
buttons.push({ buttons.push({
'i18n_text': __('Invite'), 'i18n_text': __('Invite'),

View File

@ -0,0 +1,15 @@
import tpl_nickname from "./templates/nickname.js";
import BaseModal from "plugins/modal/base.js";
export default BaseModal.extend({
id: 'change-nickname-modal',
initialize (attrs) {
this.model = attrs.model;
BaseModal.prototype.initialize.apply(this, arguments);
},
toHTML () {
return tpl_nickname(this);
},
});

View File

@ -0,0 +1,21 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (modal) => {
const jid = modal.model.get('jid');
const i18n_heading = __('Change your nickname');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="converse-modtools-modal-label">
${i18n_heading}</h5>
${modal_header_close_button}
</div>
<div class="modal-body d-flex flex-column">
<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>
</div>
</div>
</div>`;
}

View File

@ -19,7 +19,27 @@ class MUCNicknameForm extends CustomElement {
} }
render () { render () {
return tpl_muc_nickname_form(this.model); return tpl_muc_nickname_form(this);
}
submitNickname (ev) {
ev.preventDefault();
const nick = ev.target.nick.value.trim();
if (!nick) {
return;
}
if (this.model.isEntered()) {
this.model.setNickname(nick);
this.closeModal();
} else {
this.model.join(nick);
}
}
closeModal () {
const evt = document.createEvent('Event');
evt.initEvent('hide.bs.modal', true, true);
this.dispatchEvent(evt);
} }
} }

View File

@ -1,6 +1,6 @@
import '../message-form.js'; import '../message-form.js';
import '../nickname-form.js';
import 'shared/chat/toolbar.js'; import 'shared/chat/toolbar.js';
import tpl_muc_nickname_form from './muc-nickname-form.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core"; import { api, converse } from "@converse/headless/core";
import { html } from "lit"; import { html } from "lit";
@ -45,7 +45,9 @@ export default (o) => {
${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`; ${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) { } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
if (api.settings.get('muc_show_logs_before_join')) { if (api.settings.get('muc_show_logs_before_join')) {
return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model)}</span>`; return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">
<converse-muc-nickname-form jid="${o.model.get('jid')}"></converse-muc-nickname-form>
</span>`;
} }
} else { } else {
return ''; return '';

View File

@ -2,24 +2,18 @@ import { __ } from 'i18n';
import { api } from "@converse/headless/core"; import { api } from "@converse/headless/core";
import { html } from "lit"; import { html } from "lit";
function submitNickname (ev, model) { export default (el) => {
ev.preventDefault();
const nick = ev.target.nick.value.trim();
nick && model.join(nick);
}
export default (model) => {
const i18n_nickname = __('Nickname'); const i18n_nickname = __('Nickname');
const i18n_join = __('Enter groupchat'); const i18n_join = el.model?.isEntered() ? __('Change nickname') : __('Enter groupchat');
const i18n_heading = api.settings.get('muc_show_logs_before_join') ? const i18n_heading = api.settings.get('muc_show_logs_before_join') ?
__('Choose a nickname to enter') : __('Choose a nickname to enter') :
__('Please choose your nickname'); __('Please choose your nickname');
const validation_message = model.get('nickname_validation_message'); const validation_message = el.model?.get('nickname_validation_message');
return html` return html`
<div class="chatroom-form-container muc-nickname-form" <div class="chatroom-form-container muc-nickname-form"
@submit=${ev => submitNickname(ev, model)}> @submit=${ev => el.submitNickname(ev)}>
<form class="converse-form chatroom-form converse-centered-form"> <form class="converse-form chatroom-form converse-centered-form">
<fieldset class="form-group"> <fieldset class="form-group">
<label>${i18n_heading}</label> <label>${i18n_heading}</label>
@ -27,7 +21,7 @@ export default (model) => {
<input type="text" <input type="text"
required="required" required="required"
name="nick" name="nick"
value="${model.get('nick') || ''}" value="${el.model?.get('nick') || ''}"
class="form-control ${validation_message ? 'error': ''}" class="form-control ${validation_message ? 'error': ''}"
placeholder="${i18n_nickname}"/> placeholder="${i18n_nickname}"/>
</fieldset> </fieldset>

View File

@ -2,7 +2,36 @@
const { $pres, $iq, Strophe, sizzle, u } = converse.env; const { $pres, $iq, Strophe, sizzle, u } = converse.env;
fdescribe("A MUC", function () { describe("A MUC", function () {
it("allows you to change your nickname via a modal",
mock.initConverse([], {'view_mode': 'fullscreen'}, async function (_converse) {
const muc_jid = 'lounge@montague.lit';
const nick = 'romeo';
await mock.openAndEnterChatRoom(_converse, muc_jid, nick);
const view = _converse.chatboxviews.get(muc_jid);
const dropdown_item = view.querySelector(".open-nickname-modal");
dropdown_item.click();
const modal = _converse.api.modal.get('change-nickname-modal');
await u.waitUntil(() => u.isVisible(modal.el));
const input = modal.el.querySelector('input[name="nick"]');
expect(input.value).toBe(nick);
const newnick = 'loverboy';
input.value = newnick;
modal.el.querySelector('input[type="submit"]')?.click();
await u.waitUntil(() => !u.isVisible(modal.el));
const { sent_stanzas } = _converse.connection;
const sent_stanza = sent_stanzas.pop()
expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe(
`<presence from="${_converse.jid}" id="${sent_stanza.getAttribute('id')}" to="${muc_jid}/${newnick}" xmlns="jabber:client"/>`);
}));
it("informs users if their nicknames have been changed.", it("informs users if their nicknames have been changed.",
mock.initConverse([], {}, async function (_converse) { mock.initConverse([], {}, async function (_converse) {
@ -44,9 +73,8 @@ fdescribe("A MUC", function () {
*/ */
const __ = _converse.__; const __ = _converse.__;
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'oldnick'); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'oldnick');
const view = _converse.chatboxviews.get('lounge@montague.lit');
expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => view.querySelectorAll('li .occupant-nick').length, 500); await u.waitUntil(() => view.querySelectorAll('li .occupant-nick').length, 500);
let occupants = view.querySelector('.occupant-list'); let occupants = view.querySelector('.occupant-list');
expect(occupants.childElementCount).toBe(1); expect(occupants.childElementCount).toBe(1);

View File

@ -38,8 +38,8 @@
muc_domain: 'conference.chat.example.org', muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true, muc_respect_autojoin: true,
view_mode: 'fullscreen', view_mode: 'fullscreen',
// websocket_url: 'ws://chat.example.org:5380/xmpp-websocket', websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
websocket_url: 'wss://conversejs.org/xmpp-websocket', // websocket_url: 'wss://conversejs.org/xmpp-websocket',
// bosh_service_url: 'http://chat.example.org:5280/http-bind', // bosh_service_url: 'http://chat.example.org:5280/http-bind',
allow_user_defined_connection_url: true, allow_user_defined_connection_url: true,
muc_show_logs_before_join: true, muc_show_logs_before_join: true,

View File

@ -9,7 +9,7 @@ module.exports = merge(common, {
devtool: "inline-source-map", devtool: "inline-source-map",
devServer: { devServer: {
static: [ path.resolve(__dirname, '../') ], static: [ path.resolve(__dirname, '../') ],
port: 3004 port: 3003
}, },
plugins: [ plugins: [
new HTMLWebpackPlugin({ new HTMLWebpackPlugin({