From 5586d4955676431c4cbdadc97adc443fe3ff3980 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Thu, 2 Jan 2025 21:51:24 +0200 Subject: [PATCH] Upon nickname change, set new nick on the MUC bookmark - Adds a new bookmarks API --- src/headless/plugins/bookmarks/api.js | 40 ++++ src/headless/plugins/bookmarks/plugin.js | 93 +++++---- .../plugins/bookmarks/tests/bookmarks.js | 130 ++++++++++-- .../plugins/bookmarks/tests/deprecated.js | 10 +- src/headless/plugins/bookmarks/types.ts | 2 +- src/headless/plugins/muc/muc.js | 4 +- src/headless/plugins/muc/parsers.js | 185 ++++++++++-------- src/headless/plugins/muc/tests/messages.js | 45 +++-- src/headless/plugins/muc/tests/occupants.js | 43 ++-- .../plugins/muc/tests/registration.js | 3 +- src/headless/plugins/muc/types.ts | 1 + src/headless/types/plugins/bookmarks/api.d.ts | 23 +++ .../types/plugins/bookmarks/types.d.ts | 2 +- src/headless/types/plugins/muc/muc.d.ts | 2 +- src/headless/types/plugins/muc/parsers.d.ts | 4 +- src/headless/types/plugins/muc/types.d.ts | 1 + src/plugins/muc-views/tests/autocomplete.js | 128 ++++++------ src/plugins/muc-views/tests/commands.js | 5 +- src/plugins/muc-views/tests/mentions.js | 59 +++--- src/plugins/muc-views/tests/muc-avatar.js | 3 +- src/plugins/muc-views/tests/muc-mentions.js | 12 +- src/plugins/muc-views/tests/muc-messages.js | 12 +- src/plugins/muc-views/tests/nickname.js | 17 +- src/plugins/muc-views/tests/probes.js | 4 +- src/plugins/muc-views/tests/rai.js | 10 +- 25 files changed, 537 insertions(+), 301 deletions(-) create mode 100644 src/headless/plugins/bookmarks/api.js create mode 100644 src/headless/types/plugins/bookmarks/api.d.ts diff --git a/src/headless/plugins/bookmarks/api.js b/src/headless/plugins/bookmarks/api.js new file mode 100644 index 0000000000..2a2b773167 --- /dev/null +++ b/src/headless/plugins/bookmarks/api.js @@ -0,0 +1,40 @@ +import _converse from '../../shared/_converse.js'; +import promise_api from '../../shared/api/promise.js'; + +const { waitUntil } = promise_api; + +/** + * Groups methods relevant to XEP-0402 MUC bookmarks. + * + * @namespace api.bookmarks + * @memberOf api + */ +const bookmarks = { + /** + * Calling this function will result in an IQ stanza being sent out to set + * the bookmark on the server. + * + * @method api.bookmarks.set + * @param {import('./types').BookmarkAttrs} attrs - The room attributes + * @param {boolean} create=true - Whether the bookmark should be created if it doesn't exist + * @returns {Promise} + */ + async set(attrs, create = true) { + const bookmarks = await waitUntil('bookmarksInitialized'); + return bookmarks.setBookmark(attrs, create); + }, + + /** + * @method api.bookmarks.get + * @param {string} jid - The JID of the bookmark to return. + * @returns {Promise} + */ + async get(jid) { + const bookmarks = await waitUntil('bookmarksInitialized'); + return bookmarks.get(jid); + }, +}; + +const bookmarks_api = { bookmarks }; + +export default bookmarks_api; diff --git a/src/headless/plugins/bookmarks/plugin.js b/src/headless/plugins/bookmarks/plugin.js index 651920acd9..0e567315aa 100644 --- a/src/headless/plugins/bookmarks/plugin.js +++ b/src/headless/plugins/bookmarks/plugin.js @@ -1,24 +1,24 @@ /** - * @copyright 2022, the Converse.js contributors + * @copyright 2025, the Converse.js contributors * @license Mozilla Public License (MPLv2) */ -import "../../plugins/muc/index.js"; import Bookmark from './model.js'; import Bookmarks from './collection.js'; import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import converse from '../../shared/api/public.js'; import { initBookmarks, getNicknameFromBookmark, handleBookmarksPush } from './utils.js'; +import '../../plugins/muc/index.js'; +import log from '../../log'; +import bookmarks_api from './api.js'; const { Strophe } = converse.env; Strophe.addNamespace('BOOKMARKS', 'storage:bookmarks'); Strophe.addNamespace('BOOKMARKS2', 'urn:xmpp:bookmarks:1'); - converse.plugins.add('converse-bookmarks', { - - dependencies: ["converse-chatboxes", "converse-muc"], + dependencies: ['converse-chatboxes', 'converse-muc'], overrides: { // Overrides mentioned here will be picked up by converse.js's @@ -27,7 +27,7 @@ converse.plugins.add('converse-bookmarks', { // New functions which don't exist yet can also be added. ChatRoom: { - getDisplayName () { + getDisplayName() { const { _converse, getDisplayName } = this.__super__; const { bookmarks } = _converse.state; const bookmark = this.get('bookmarked') ? bookmarks?.get(this.get('jid')) : null; @@ -37,14 +37,14 @@ converse.plugins.add('converse-bookmarks', { /** * @param {string} nick */ - getAndPersistNickname (nick) { + getAndPersistNickname(nick) { nick = nick || getNicknameFromBookmark(this.get('jid')); return this.__super__.getAndPersistNickname.call(this, nick); - } - } + }, + }, }, - initialize () { + initialize() { // Configuration values for this plugin // ==================================== // Refer to docs/source/configuration.rst for explanations of these @@ -52,25 +52,51 @@ converse.plugins.add('converse-bookmarks', { api.settings.extend({ allow_bookmarks: true, allow_public_bookmarks: false, - muc_respect_autojoin: true + muc_respect_autojoin: true, }); api.promises.add('bookmarksInitialized'); - const exports = { Bookmark, Bookmarks }; + Object.assign(api, bookmarks_api); + + const exports = { Bookmark, Bookmarks }; Object.assign(_converse, exports); // TODO: DEPRECATED Object.assign(_converse.exports, exports); + api.listen.on( + 'parseMUCPresence', + /** + * @param {Element} _stanza + * @param {import('../muc/types').MUCPresenceAttributes} attrs + */ + (_stanza, attrs) => { + if (attrs.is_self && attrs.codes.includes('303')) { + api.bookmarks.get(attrs.muc_jid).then( + /** @param {Bookmark} bookmark */ (bookmark) => { + if (!bookmark) log.warn('parseMUCPresence: no bookmark returned'); + + const { nick, muc_jid: jid } = attrs; + api.bookmarks.set({ + jid, + nick, + autojoin: bookmark?.get('autojoin') ?? true, + password: bookmark?.get('password') ?? '', + name: bookmark?.get('name') ?? '', + extensions: bookmark?.get('extensions') ?? [], + }); + } + ); + } + return attrs; + } + ); + api.listen.on( 'enteredNewRoom', /** @param {import('../muc/muc').default} muc */ - ({ attributes }) => { - const { bookmarks } = _converse.state; - if (!bookmarks) return; - - const { jid, nick, password, name } = /** @type {import("../muc/types").MUCAttributes} */(attributes); - - bookmarks.setBookmark({ + async ({ attributes }) => { + const { jid, nick, password, name } = /** @type {import("../muc/types").MUCAttributes} */ (attributes); + await api.bookmarks.set({ jid, autojoin: true, nick, @@ -83,35 +109,34 @@ converse.plugins.add('converse-bookmarks', { api.listen.on( 'leaveRoom', /** @param {import('../muc/muc').default} muc */ - ({ attributes }) => { - const { bookmarks } = _converse.state; - if (!bookmarks) return; - - const { jid } = /** @type {import("../muc/types").MUCAttributes} */(attributes); - - bookmarks.setBookmark({ - jid, - autojoin: false, - }, false); + async ({ attributes }) => { + const { jid } = /** @type {import("../muc/types").MUCAttributes} */ (attributes); + await api.bookmarks.set( + { + jid, + autojoin: false, + }, + false + ); } ); api.listen.on('addClientFeatures', () => { if (api.settings.get('allow_bookmarks')) { - api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify') + api.disco.own.features.add(Strophe.NS.BOOKMARKS + '+notify'); } - }) + }); api.listen.on('clearSession', () => { const { state } = _converse; if (state.bookmarks) { - state.bookmarks.clearStore({'silent': true}); + state.bookmarks.clearStore({ 'silent': true }); window.sessionStorage.removeItem(state.bookmarks.fetched_flag); delete state.bookmarks; } }); - api.listen.on('connected', async () => { + api.listen.on('connected', async () => { // Add a handler for bookmarks pushed from other connected clients const bare_jid = _converse.session.get('bare_jid'); const connection = api.connection.get(); @@ -120,5 +145,5 @@ converse.plugins.add('converse-bookmarks', { await Promise.all([api.waitUntil('chatBoxesFetched')]); initBookmarks(); }); - } + }, }); diff --git a/src/headless/plugins/bookmarks/tests/bookmarks.js b/src/headless/plugins/bookmarks/tests/bookmarks.js index 66d988b87f..e16e1186b7 100644 --- a/src/headless/plugins/bookmarks/tests/bookmarks.js +++ b/src/headless/plugins/bookmarks/tests/bookmarks.js @@ -1,21 +1,14 @@ /* global mock, converse */ const { Strophe, sizzle, stx, u } = converse.env; -describe("A chat room", function () { + +describe("A bookmark", function () { beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); - it("is automatically bookmarked when opened", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => { - const { bare_jid } = _converse; + it("is automatically created when a MUC is entered", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => { await mock.waitForRoster(_converse, 'current', 0); - await mock.waitUntilDiscoConfirmed( - _converse, bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - [ - 'http://jabber.org/protocol/pubsub#publish-options', - 'urn:xmpp:bookmarks:1#compat' - ] - ); + await mock.waitUntilBookmarksReturned(_converse); const nick = 'JC'; const muc_jid = 'theplay@conference.shakespeare.lit'; @@ -64,9 +57,6 @@ describe("A chat room", function () { ` ); - /* Server acknowledges successful storage - * - */ const stanza = stx` jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + const nick = 'JC'; + const muc_jid = 'theplay@conference.shakespeare.lit'; + const settings = { name: "Play's the thing", password: 'secret' }; + const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, [], [], true, settings); + + const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; + let sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop()); + + const stanza = stx``; + _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); + + const newnick = 'BAP'; + muc.setNickname(newnick); + + const sent_IQs = _converse.api.connection.get().IQ_stanzas; + while (sent_IQs.length) { sent_IQs.pop(); } + + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx` + + + + + + ` + )); + + await u.waitUntil(() => muc.get('nick') === newnick); + + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx` + + + + + ` + )); + + sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle('iq publish[node="urn:xmpp:bookmarks:1"]', s).length).pop()); + + expect(sent_stanza).toEqualStanza( + stx` + + + + + ${newnick} + ${settings.password} + + + + + + + http://jabber.org/protocol/pubsub#publish-options + + + true + + + max + + + never + + + whitelist + + + + + ` + ); + })); describe("when autojoin is set", function () { @@ -203,8 +292,9 @@ describe("A bookmark", function () { const bare_jid = _converse.session.get('bare_jid'); const muc1_jid = 'theplay@conference.shakespeare.lit'; const { bookmarks } = _converse.state; + const { api } = _converse; - bookmarks.setBookmark({ + await api.bookmarks.set({ jid: muc1_jid, autojoin: true, name: 'Hamlet', @@ -247,7 +337,7 @@ describe("A bookmark", function () { const muc2_jid = 'balcony@conference.shakespeare.lit'; - bookmarks.setBookmark({ + await api.bookmarks.set({ jid: muc2_jid, autojoin: true, name: 'Balcony', @@ -293,7 +383,7 @@ describe("A bookmark", function () { `); const muc3_jid = 'garden@conference.shakespeare.lit'; - bookmarks.setBookmark({ + await api.bookmarks.set({ jid: muc3_jid, autojoin: false, name: 'Garden', diff --git a/src/headless/plugins/bookmarks/tests/deprecated.js b/src/headless/plugins/bookmarks/tests/deprecated.js index 6a14d748c8..0ecfc565a5 100644 --- a/src/headless/plugins/bookmarks/tests/deprecated.js +++ b/src/headless/plugins/bookmarks/tests/deprecated.js @@ -5,12 +5,12 @@ describe("A chat room", function () { beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); it("is automatically bookmarked when opened", mock.initConverse(['chatBoxesFetched'], {}, async (_converse) => { - const { bare_jid } = _converse; await mock.waitForRoster(_converse, 'current', 0); - await mock.waitUntilDiscoConfirmed( - _converse, bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - [ 'http://jabber.org/protocol/pubsub#publish-options' ] + await mock.waitUntilBookmarksReturned( + _converse, + [], + ['http://jabber.org/protocol/pubsub#publish-options'], + 'storage:bookmarks' ); const nick = 'JC'; diff --git a/src/headless/plugins/bookmarks/types.ts b/src/headless/plugins/bookmarks/types.ts index 30c3e2abdb..6b8f5cd0f9 100644 --- a/src/headless/plugins/bookmarks/types.ts +++ b/src/headless/plugins/bookmarks/types.ts @@ -4,5 +4,5 @@ export type BookmarkAttrs = { autojoin?: boolean; nick?: string; password?: string; - extensions?: string[]; + extensions: string[]; } diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index acd8044735..f22ac17243 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -2686,12 +2686,12 @@ class MUC extends ModelWithMessages(ColorAwareModel(ChatBoxBase)) { * Handles incoming presence stanzas coming from the MUC * @param {Element} stanza */ - onPresence (stanza) { + async onPresence (stanza) { if (stanza.getAttribute('type') === 'error') { return this.onErrorPresence(stanza); } - const attrs = parseMUCPresence(stanza, this); + const attrs = await parseMUCPresence(stanza, this); attrs.codes.forEach(async (code) => { this.createInfoMessageFromPresence(code, attrs); diff --git a/src/headless/plugins/muc/parsers.js b/src/headless/plugins/muc/parsers.js index 9431076776..bfeb0d428d 100644 --- a/src/headless/plugins/muc/parsers.js +++ b/src/headless/plugins/muc/parsers.js @@ -37,25 +37,26 @@ const { NS } = Strophe; * @param {Element} stanza - The message stanza * @returns {Array} Returns an array of objects representing elements. */ -export function getMEPActivities (stanza) { +export function getMEPActivities(stanza) { const items_el = sizzle(`items[node="${Strophe.NS.CONFINFO}"]`, stanza).pop(); if (!items_el) { return null; } const from = stanza.getAttribute('from'); const msgid = stanza.getAttribute('id'); - const selector = `item `+ - `conference-info[xmlns="${Strophe.NS.CONFINFO}"] `+ - `activity[xmlns="${Strophe.NS.ACTIVITY}"]`; - return sizzle(selector, items_el).map(/** @param {Element} el */(el) => { - const message = el.querySelector('text')?.textContent; - if (message) { - const references = getReferences(stanza); - const reason = el.querySelector('reason')?.textContent; - return { from, msgid, message, reason, references, 'type': 'mep' }; + const selector = + `item ` + `conference-info[xmlns="${Strophe.NS.CONFINFO}"] ` + `activity[xmlns="${Strophe.NS.ACTIVITY}"]`; + return sizzle(selector, items_el).map( + /** @param {Element} el */ (el) => { + const message = el.querySelector('text')?.textContent; + if (message) { + const references = getReferences(stanza); + const reason = el.querySelector('reason')?.textContent; + return { from, msgid, message, reason, references, 'type': 'mep' }; + } + return {}; } - return {}; - }); + ); } /** @@ -70,7 +71,7 @@ export function getMEPActivities (stanza) { * @param {Element} stanza - The message stanza * @returns {Object} */ -function getJIDFromMUCUserData (stanza) { +function getJIDFromMUCUserData(stanza) { const item = sizzle(`message > x[xmlns="${Strophe.NS.MUC_USER}"] item`, stanza).pop(); return item?.getAttribute('jid'); } @@ -80,7 +81,7 @@ function getJIDFromMUCUserData (stanza) { * message stanza, if it was contained, otherwise it's the message stanza itself. * @returns {Object} */ -function getModerationAttributes (stanza) { +function getModerationAttributes(stanza) { const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop(); if (fastening) { const applies_to_id = fastening.getAttribute('id'); @@ -93,7 +94,7 @@ function getModerationAttributes (stanza) { 'moderated': 'retracted', 'moderated_by': moderated.getAttribute('by'), 'moderated_id': applies_to_id, - 'moderation_reason': moderated.querySelector('reason')?.textContent + 'moderation_reason': moderated.querySelector('reason')?.textContent, }; } } @@ -107,7 +108,7 @@ function getModerationAttributes (stanza) { 'is_tombstone': true, 'moderated_by': tombstone.getAttribute('by'), 'retracted': tombstone.getAttribute('stamp'), - 'moderation_reason': tombstone.querySelector('reason')?.textContent + 'moderation_reason': tombstone.querySelector('reason')?.textContent, }; } } @@ -128,7 +129,8 @@ function getStatusCodes(stanza, type) { .map(/** @param {Element} s */ (s) => s.getAttribute('code')) .filter( /** @param {MUCStatusCode} c */ - (c) => STATUS_CODE_STANZAS[c]?.includes(type)); + (c) => STATUS_CODE_STANZAS[c]?.includes(type) + ); if (type === 'presence' && codes.includes('333') && codes.includes('307')) { // See: https://github.com/xsf/xeps/pull/969/files#diff-ac5113766e59219806793c1f7d967f1bR4966 @@ -137,7 +139,7 @@ function getStatusCodes(stanza, type) { return { codes, - is_self: codes.includes('110') + is_self: codes.includes('110'), }; } @@ -145,7 +147,7 @@ function getStatusCodes(stanza, type) { * @param {Element} stanza * @param {MUC} chatbox */ -function getOccupantID (stanza, chatbox) { +function getOccupantID(stanza, chatbox) { if (chatbox.features.get(Strophe.NS.OCCUPANTID)) { return sizzle(`occupant-id[xmlns="${Strophe.NS.OCCUPANTID}"]`, stanza).pop()?.getAttribute('id'); } @@ -158,7 +160,7 @@ function getOccupantID (stanza, chatbox) { * @param {MUC} chatbox * @returns {'me'|'them'} */ -function getSender (attrs, chatbox) { +function getSender(attrs, chatbox) { let is_me; const own_occupant_id = chatbox.get('occupant_id'); @@ -168,7 +170,7 @@ function getSender (attrs, chatbox) { const bare_jid = _converse.session.get('bare_jid'); is_me = Strophe.getBareJidFromJid(attrs.from_real_jid) === bare_jid; } else { - is_me = attrs.nick === chatbox.get('nick') + is_me = attrs.nick === chatbox.get('nick'); } return is_me ? 'me' : 'them'; } @@ -179,7 +181,7 @@ function getSender (attrs, chatbox) { * @param {MUC} chatbox * @returns {Promise} */ -export async function parseMUCMessage (original_stanza, chatbox) { +export async function parseMUCMessage(original_stanza, chatbox) { throwErrorIfInvalidForward(original_stanza); const forwarded_stanza = sizzle( @@ -191,7 +193,7 @@ export async function parseMUCMessage (original_stanza, chatbox) { if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) { return new StanzaParseError( stanza, - `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`, + `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}` ); } @@ -202,7 +204,7 @@ export async function parseMUCMessage (original_stanza, chatbox) { if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, forwarded_stanza).length) { return new StanzaParseError( original_stanza, - `Invalid Stanza: Forged MAM groupchat message from ${original_stanza.getAttribute('from')}`, + `Invalid Stanza: Forged MAM groupchat message from ${original_stanza.getAttribute('from')}` ); } delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, forwarded_stanza.parentElement).pop(); @@ -212,71 +214,78 @@ export async function parseMUCMessage (original_stanza, chatbox) { body = original_stanza.querySelector(':scope > body')?.textContent?.trim(); } - const from = stanza.getAttribute('from'); const marker = getChatMarker(stanza); - let attrs = /** @type {MUCMessageAttributes} */(Object.assign( - { - from, - body, - 'activities': getMEPActivities(stanza), - 'chat_state': getChatState(stanza), - 'from_muc': Strophe.getBareJidFromJid(from), - 'is_archived': isArchived(original_stanza), - 'is_carbon': isCarbon(original_stanza), - 'is_delayed': !!delay, - 'is_forwarded': !!sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length, - 'is_headline': isHeadline(stanza), - 'is_markable': !!sizzle(`message > markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length, - 'is_marker': !!marker, - 'is_unstyled': !!sizzle(`message > unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length, - 'marker_id': marker && marker.getAttribute('id'), - 'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'), - 'nick': Strophe.unescapeNode(Strophe.getResourceFromJid(from)), - 'occupant_id': getOccupantID(stanza, chatbox), - 'receipt_id': getReceiptId(stanza), - 'received': new Date().toISOString(), - 'references': getReferences(stanza), - 'subject': stanza.querySelector(':scope > subject')?.textContent, - 'thread': stanza.querySelector(':scope > thread')?.textContent, - 'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString(), - 'to': stanza.getAttribute('to'), - 'type': stanza.getAttribute('type') - }, - getErrorAttributes(stanza), - getOutOfBandAttributes(stanza), - getSpoilerAttributes(stanza), - getCorrectionAttributes(stanza, original_stanza), - getStanzaIDs(stanza, original_stanza), - getOpenGraphMetadata(stanza), - getRetractionAttributes(stanza, original_stanza), - getModerationAttributes(stanza), - getEncryptionAttributes(stanza), - getStatusCodes(stanza, 'message'), - )); + let attrs = /** @type {MUCMessageAttributes} */ ( + Object.assign( + { + from, + body, + 'activities': getMEPActivities(stanza), + 'chat_state': getChatState(stanza), + 'from_muc': Strophe.getBareJidFromJid(from), + 'is_archived': isArchived(original_stanza), + 'is_carbon': isCarbon(original_stanza), + 'is_delayed': !!delay, + 'is_forwarded': !!sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length, + 'is_headline': isHeadline(stanza), + 'is_markable': !!sizzle(`message > markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length, + 'is_marker': !!marker, + 'is_unstyled': !!sizzle(`message > unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length, + 'marker_id': marker && marker.getAttribute('id'), + 'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'), + 'nick': Strophe.unescapeNode(Strophe.getResourceFromJid(from)), + 'occupant_id': getOccupantID(stanza, chatbox), + 'receipt_id': getReceiptId(stanza), + 'received': new Date().toISOString(), + 'references': getReferences(stanza), + 'subject': stanza.querySelector(':scope > subject')?.textContent, + 'thread': stanza.querySelector(':scope > thread')?.textContent, + 'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString(), + 'to': stanza.getAttribute('to'), + 'type': stanza.getAttribute('type'), + }, + getErrorAttributes(stanza), + getOutOfBandAttributes(stanza), + getSpoilerAttributes(stanza), + getCorrectionAttributes(stanza, original_stanza), + getStanzaIDs(stanza, original_stanza), + getOpenGraphMetadata(stanza), + getRetractionAttributes(stanza, original_stanza), + getModerationAttributes(stanza), + getEncryptionAttributes(stanza), + getStatusCodes(stanza, 'message') + ) + ); - attrs.from_real_jid = attrs.is_archived && getJIDFromMUCUserData(stanza) || - chatbox.occupants.findOccupant(attrs)?.get('jid'); + attrs.from_real_jid = + (attrs.is_archived && getJIDFromMUCUserData(stanza)) || chatbox.occupants.findOccupant(attrs)?.get('jid'); - attrs = Object.assign({ - 'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs), - 'message': attrs.body || attrs.error, // TODO: Should only be used for error and info messages - 'sender': getSender(attrs, chatbox), - }, attrs); + attrs = Object.assign( + { + 'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs), + 'message': attrs.body || attrs.error, // TODO: Should only be used for error and info messages + 'sender': getSender(attrs, chatbox), + }, + attrs + ); if (attrs.is_archived && original_stanza.getAttribute('from') !== attrs.from_muc) { return new StanzaParseError( original_stanza, - `Invalid Stanza: Forged MAM message from ${original_stanza.getAttribute('from')}`, + `Invalid Stanza: Forged MAM message from ${original_stanza.getAttribute('from')}` ); } else if (attrs.is_archived && original_stanza.getAttribute('from') !== chatbox.get('jid')) { return new StanzaParseError( original_stanza, - `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}`, + `Invalid Stanza: Forged MAM groupchat message from ${stanza.getAttribute('from')}` ); } else if (attrs.is_carbon) { - return new StanzaParseError(original_stanza, 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied'); + return new StanzaParseError( + original_stanza, + 'Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied' + ); } // We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates. @@ -301,7 +310,7 @@ export async function parseMUCMessage (original_stanza, chatbox) { * @param {Element} iq * @returns {import('./types').MemberListItem[]} */ -export function parseMemberListIQ (iq) { +export function parseMemberListIQ(iq) { return sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq).map( /** @param {Element} item */ (item) => { const data = { @@ -363,29 +372,37 @@ function parsePresenceUserItem(stanza, nick) { * Parses a passed in MUC presence stanza and returns an object of attributes. * @param {Element} stanza - The presence stanza * @param {MUC} chatbox - * @returns {import('./types').MUCPresenceAttributes} + * @returns {Promise} */ -export function parseMUCPresence (stanza, chatbox) { +export async function parseMUCPresence(stanza, chatbox) { /** * @typedef {import('./types').MUCPresenceAttributes} MUCPresenceAttributes */ const from = stanza.getAttribute('from'); const type = stanza.getAttribute('type'); const nick = Strophe.getResourceFromJid(from); - const attrs = /** @type {MUCPresenceAttributes} */({ + const attrs = /** @type {MUCPresenceAttributes} */ ({ from, nick, - occupant_id: getOccupantID(stanza, chatbox), type, + muc_jid: Strophe.getBareJidFromJid(from), + occupant_id: getOccupantID(stanza, chatbox), status: stanza.querySelector(':scope > status')?.textContent ?? undefined, show: stanza.querySelector(':scope > show')?.textContent ?? (type !== 'unavailable' ? 'online' : 'offline'), image_hash: sizzle(`presence > x[xmlns="${Strophe.NS.VCARDUPDATE}"] photo`, stanza).pop()?.textContent, - hats: sizzle(`presence > hats[xmlns="${Strophe.NS.MUC_HATS}"] hat`, stanza).map(/** @param {Element} h */(h) => ({ - title: h.getAttribute('title'), - uri: h.getAttribute('uri') - })), + hats: sizzle(`presence > hats[xmlns="${Strophe.NS.MUC_HATS}"] hat`, stanza).map( + /** @param {Element} h */ (h) => ({ + title: h.getAttribute('title'), + uri: h.getAttribute('uri'), + }) + ), ...getStatusCodes(stanza, 'presence'), ...parsePresenceUserItem(stanza, nick), }); - return attrs; + + /** + * *Hook* which allows plugins to add additional parsing + * @event _converse#parseMUCPresence + */ + return /** @type {import('./types').MUCPresenceAttributes}*/ (await api.hook('parseMUCPresence', stanza, attrs)); } diff --git a/src/headless/plugins/muc/tests/messages.js b/src/headless/plugins/muc/tests/messages.js index c23cf05922..35727d14dd 100644 --- a/src/headless/plugins/muc/tests/messages.js +++ b/src/headless/plugins/muc/tests/messages.js @@ -1,38 +1,41 @@ /*global mock, converse */ -const { Strophe, u, $msg } = converse.env; +const { Strophe, u, $msg, stx } = converse.env; describe("A MUC message", function () { it("saves the user's real JID as looked up via the XEP-0421 occupant id", mock.initConverse([], {}, async function (_converse) { + await mock.waitUntilBookmarksReturned(_converse); const muc_jid = 'lounge@montague.lit'; const nick = 'romeo'; const features = [...mock.default_muc_features, Strophe.NS.OCCUPANTID]; const model = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, features); const occupant_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const presence = u.toStanza(` - - - - - - `); + const presence = stx` + + + + + + `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - expect(model.getOccupantByNickname('thirdwitch').get('occupant_id')).toBe('dd72603deec90a38ba552f7c68cbcc61bca202cd'); - const stanza = u.toStanza(` - - Harpier cries: 'tis time, 'tis time. - - `); + const occupant = await u.waitUntil(() => model.getOccupantByNickname('thirdwitch')); + expect(occupant.get('occupant_id')).toBe('dd72603deec90a38ba552f7c68cbcc61bca202cd'); + + const stanza = stx` + + Harpier cries: 'tis time, 'tis time. + + `; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => model.messages.length); diff --git a/src/headless/plugins/muc/tests/occupants.js b/src/headless/plugins/muc/tests/occupants.js index 7b897e8af2..afde960b59 100644 --- a/src/headless/plugins/muc/tests/occupants.js +++ b/src/headless/plugins/muc/tests/occupants.js @@ -1,11 +1,13 @@ /*global mock, converse */ - -const { Strophe, u } = converse.env; +const { Strophe, u, stx } = converse.env; describe("A MUC occupant", function () { it("does not stores the XEP-0421 occupant id if the feature isn't advertised", mock.initConverse([], {}, async function (_converse) { + + await mock.waitUntilBookmarksReturned(_converse); + const muc_jid = 'lounge@montague.lit'; const nick = 'romeo'; const model = await mock.openAndEnterChatRoom(_converse, muc_jid, nick); @@ -13,21 +15,26 @@ describe("A MUC occupant", function () { // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres const id = u.getUniqueId(); const name = mock.chatroom_names[0]; - const presence = u.toStanza(` + const presence = stx` + to="${_converse.bare_jid}" + xmlns="jabber:client"> - `); + `; + _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - expect(model.getOccupantByNickname(name).get('occupant_id')).toBe(undefined); + const occupant = await u.waitUntil(() => model.getOccupantByNickname(name)); + expect(occupant.get('occupant_id')).toBe(undefined); })); it("stores the XEP-0421 occupant id received from a presence stanza", mock.initConverse([], {}, async function (_converse) { + await mock.waitUntilBookmarksReturned(_converse); + const muc_jid = 'lounge@montague.lit'; const nick = 'romeo'; const features = [...mock.default_muc_features, Strophe.NS.OCCUPANTID]; @@ -41,16 +48,18 @@ describe("A MUC occupant", function () { // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres const id = u.getUniqueId(); const name = mock.chatroom_names[i]; - const presence = u.toStanza(` + const presence = stx` + to="${_converse.bare_jid}" + xmlns="jabber:client"> - `); + `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - expect(model.getOccupantByNickname(name).get('occupant_id')).toBe(id); + const occupant = await u.waitUntil(() => model.getOccupantByNickname(name)); + expect(occupant.get('occupant_id')).toBe(id); } expect(model.occupants.length).toBe(mock.chatroom_names.length + 1); })); @@ -69,15 +78,16 @@ describe("A MUC occupant", function () { const occupant_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const stanza = u.toStanza(` + const stanza = stx` + type='groupchat' + xmlns="jabber:client"> Harpier cries: 'tis time, 'tis time. - `); + `; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => model.messages.length); @@ -93,16 +103,17 @@ describe("A MUC occupant", function () { expect(message.getDisplayName()).toBe('3rdwitch'); - const presence = u.toStanza(` + const presence = stx` + to="${_converse.bare_jid}" + xmlns="jabber:client"> - `); + `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); occupant = await u.waitUntil(() => model.getOccupantByNickname('thirdwitch')); diff --git a/src/headless/plugins/muc/tests/registration.js b/src/headless/plugins/muc/tests/registration.js index 72bbd3de71..3b8dc3cc5e 100644 --- a/src/headless/plugins/muc/tests/registration.js +++ b/src/headless/plugins/muc/tests/registration.js @@ -11,8 +11,9 @@ describe("Groupchats", function () { mock.initConverse(['chatBoxesFetched'], {'auto_register_muc_nickname': true}, async function (_converse) { + const nick = 'romeo'; const muc_jid = 'coven@chat.shakespeare.lit'; - await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + await mock.openAndEnterChatRoom(_converse, muc_jid, nick); const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; let stanza = await u.waitUntil(() => IQ_stanzas.find( diff --git a/src/headless/plugins/muc/types.ts b/src/headless/plugins/muc/types.ts index 5d51c90bd1..a7b7b9c91c 100644 --- a/src/headless/plugins/muc/types.ts +++ b/src/headless/plugins/muc/types.ts @@ -106,6 +106,7 @@ export type MUCPresenceAttributes = MUCPresenceItemAttributes & { hats: Array; // An array of XEP-0317 hats image_hash?: string; is_self: boolean; + muc_jid: string; // The JID of the MUC in which the presence was received nick: string; // The nickname of the sender occupant_id: string; // The XEP-0421 occupant ID show: string; diff --git a/src/headless/types/plugins/bookmarks/api.d.ts b/src/headless/types/plugins/bookmarks/api.d.ts new file mode 100644 index 0000000000..6fe8b3b009 --- /dev/null +++ b/src/headless/types/plugins/bookmarks/api.d.ts @@ -0,0 +1,23 @@ +export default bookmarks_api; +declare namespace bookmarks_api { + export { bookmarks }; +} +declare namespace bookmarks { + /** + * Calling this function will result in an IQ stanza being sent out to set + * the bookmark on the server. + * + * @method api.bookmarks.set + * @param {import('./types').BookmarkAttrs} attrs - The room attributes + * @param {boolean} create=true - Whether the bookmark should be created if it doesn't exist + * @returns {Promise} + */ + function set(attrs: import("./types").BookmarkAttrs, create?: boolean): Promise; + /** + * @method api.bookmarks.get + * @param {string} jid - The JID of the bookmark to return. + * @returns {Promise} + */ + function get(jid: string): Promise; +} +//# sourceMappingURL=api.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/bookmarks/types.d.ts b/src/headless/types/plugins/bookmarks/types.d.ts index a715ca6ec1..05d9e7d5c2 100644 --- a/src/headless/types/plugins/bookmarks/types.d.ts +++ b/src/headless/types/plugins/bookmarks/types.d.ts @@ -4,6 +4,6 @@ export type BookmarkAttrs = { autojoin?: boolean; nick?: string; password?: string; - extensions?: string[]; + extensions: string[]; }; //# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/src/headless/types/plugins/muc/muc.d.ts b/src/headless/types/plugins/muc/muc.d.ts index 2267938b51..a9452b358c 100644 --- a/src/headless/types/plugins/muc/muc.d.ts +++ b/src/headless/types/plugins/muc/muc.d.ts @@ -820,7 +820,7 @@ declare class MUC extends MUC_base { * Handles incoming presence stanzas coming from the MUC * @param {Element} stanza */ - onPresence(stanza: Element): void; + onPresence(stanza: Element): Promise; /** * Handles a received presence relating to the current user. * diff --git a/src/headless/types/plugins/muc/parsers.d.ts b/src/headless/types/plugins/muc/parsers.d.ts index 9ec30a9a44..d5164e9097 100644 --- a/src/headless/types/plugins/muc/parsers.d.ts +++ b/src/headless/types/plugins/muc/parsers.d.ts @@ -23,9 +23,9 @@ export function parseMemberListIQ(iq: Element): import("./types").MemberListItem * Parses a passed in MUC presence stanza and returns an object of attributes. * @param {Element} stanza - The presence stanza * @param {MUC} chatbox - * @returns {import('./types').MUCPresenceAttributes} + * @returns {Promise} */ -export function parseMUCPresence(stanza: Element, chatbox: MUC): import("./types").MUCPresenceAttributes; +export function parseMUCPresence(stanza: Element, chatbox: MUC): Promise; export type MUC = import("../muc/muc.js").default; export type MUCMessageAttributes = import("./types").MUCMessageAttributes; import { StanzaParseError } from '../../shared/errors.js'; diff --git a/src/headless/types/plugins/muc/types.d.ts b/src/headless/types/plugins/muc/types.d.ts index 94b673ff4c..55a0fdb28a 100644 --- a/src/headless/types/plugins/muc/types.d.ts +++ b/src/headless/types/plugins/muc/types.d.ts @@ -74,6 +74,7 @@ export type MUCPresenceAttributes = MUCPresenceItemAttributes & { hats: Array; image_hash?: string; is_self: boolean; + muc_jid: string; nick: string; occupant_id: string; show: string; diff --git a/src/plugins/muc-views/tests/autocomplete.js b/src/plugins/muc-views/tests/autocomplete.js index 257aeef867..3691d8b11d 100644 --- a/src/plugins/muc-views/tests/autocomplete.js +++ b/src/plugins/muc-views/tests/autocomplete.js @@ -200,69 +200,73 @@ describe("The nickname autocomplete feature", function () { })); it("should order by query index position and length", mock.initConverse( - ['chatBoxesFetched'], {}, async function (_converse) { - await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - - // Nicknames from presences - ['bernard', 'naber', 'helberlo', 'john', 'jones'].forEach((nick) => { - _converse.api.connection.get()._dataRecv(mock.createRequest( - stx` - - - - `)); - }); - - const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); - const at_event = { - 'target': textarea, - 'preventDefault': function preventDefault() { }, - 'stopPropagation': function stopPropagation() { }, - 'keyCode': 50, - 'key': '@' - }; - - const message_form = view.querySelector('converse-muc-message-form'); - // Test that results are sorted by query index - message_form.onKeyDown(at_event); - textarea.value = '@ber'; - message_form.onKeyUp(at_event); - await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3); - - const first_child = view.querySelector('.suggestion-box__results li:first-child converse-avatar'); - expect(first_child.textContent).toBe('B'); - expect(first_child.nextElementSibling.textContent).toBe('ber'); - expect(first_child.nextElementSibling.nextSibling.textContent).toBe('nard'); - - const second_child = view.querySelector('.suggestion-box__results li:nth-child(2) converse-avatar'); - expect(second_child.textContent).toBe('N'); - expect(second_child.nextSibling.textContent).toBe('na'); - expect(second_child.nextElementSibling.textContent).toBe('ber'); - - const third_child = view.querySelector('.suggestion-box__results li:nth-child(3) converse-avatar'); - expect(third_child.textContent).toBe('H'); - expect(third_child.nextSibling.textContent).toBe('hel'); - expect(third_child.nextSibling.nextSibling.textContent).toBe('ber'); - expect(third_child.nextSibling.nextSibling.nextSibling.textContent).toBe('lo'); - - // Test that when the query index is equal, results should be sorted by length - textarea.value = '@jo'; - message_form.onKeyUp(at_event); - await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2); - - // First char is the avatar initial - expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('Jjohn'); - expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('Jjones'); + ['chatBoxesFetched'], {}, async function (_converse) { + + await mock.waitUntilBookmarksReturned(_converse); + const model = await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + + // Nicknames from presences + ['bernard', 'naber', 'helberlo', 'john', 'jones'].forEach((nick) => { + _converse.api.connection.get()._dataRecv(mock.createRequest( + stx` + + + + `)); + }); + await u.waitUntil(() => model.getOccupantByNickname('jones')); + + const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); + const at_event = { + 'target': textarea, + 'preventDefault': function preventDefault() { }, + 'stopPropagation': function stopPropagation() { }, + 'keyCode': 50, + 'key': '@' + }; + + const message_form = view.querySelector('converse-muc-message-form'); + // Test that results are sorted by query index + message_form.onKeyDown(at_event); + textarea.value = '@ber'; + message_form.onKeyUp(at_event); + await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3); + + const first_child = view.querySelector('.suggestion-box__results li:first-child converse-avatar'); + expect(first_child.textContent).toBe('B'); + expect(first_child.nextElementSibling.textContent).toBe('ber'); + expect(first_child.nextElementSibling.nextSibling.textContent).toBe('nard'); + + const second_child = view.querySelector('.suggestion-box__results li:nth-child(2) converse-avatar'); + expect(second_child.textContent).toBe('N'); + expect(second_child.nextSibling.textContent).toBe('na'); + expect(second_child.nextElementSibling.textContent).toBe('ber'); + + const third_child = view.querySelector('.suggestion-box__results li:nth-child(3) converse-avatar'); + expect(third_child.textContent).toBe('H'); + expect(third_child.nextSibling.textContent).toBe('hel'); + expect(third_child.nextSibling.nextSibling.textContent).toBe('ber'); + expect(third_child.nextSibling.nextSibling.nextSibling.textContent).toBe('lo'); + + // Test that when the query index is equal, results should be sorted by length + textarea.value = '@jo'; + message_form.onKeyUp(at_event); + await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2); + + // First char is the avatar initial + expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('Jjohn'); + expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('Jjones'); })); it("autocompletes when the user presses tab", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { - await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + await mock.waitUntilBookmarksReturned(_converse); + const model = await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); const view = _converse.chatboxviews.get('lounge@montague.lit'); expect(view.model.occupants.length).toBe(1); let presence = stx` `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - expect(view.model.occupants.length).toBe(2); + + await u.waitUntil(() => view.model.occupants.length === 2); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); textarea.value = "hello som"; @@ -318,6 +323,7 @@ describe("The nickname autocomplete feature", function () { `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => model.getOccupantByNickname('some2')); textarea.value = "hello s s"; message_form.onKeyDown(tab_event); @@ -356,6 +362,7 @@ describe("The nickname autocomplete feature", function () { `)); + await u.waitUntil(() => model.getOccupantByNickname('z3r0')); textarea.value = "hello z"; message_form.onKeyDown(tab_event); @@ -370,6 +377,7 @@ describe("The nickname autocomplete feature", function () { it("autocompletes when the user presses backspace", mock.initConverse([], {}, async function (_converse) { + await mock.waitUntilBookmarksReturned(_converse); await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); const view = _converse.chatboxviews.get('lounge@montague.lit'); expect(view.model.occupants.length).toBe(1); @@ -382,7 +390,7 @@ describe("The nickname autocomplete feature", function () { `)); - expect(view.model.occupants.length).toBe(2); + await u.waitUntil(() => view.model.occupants.length === 2); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); textarea.value = "hello @some1 "; diff --git a/src/plugins/muc-views/tests/commands.js b/src/plugins/muc-views/tests/commands.js index 6aa3a5c90e..d53ec08318 100644 --- a/src/plugins/muc-views/tests/commands.js +++ b/src/plugins/muc-views/tests/commands.js @@ -162,8 +162,7 @@ describe("Groupchats", function () { ` )); - - expect(muc.occupants.length).toBe(2); + await u.waitUntil(() => muc.occupants.length === 2); const view = _converse.chatboxviews.get(muc_jid); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); @@ -194,7 +193,7 @@ describe("Groupchats", function () { keyCode: 13 }); - await u.waitUntil(() => sent_stanza.querySelector('item[affiliation="member"]')); + await u.waitUntil(() => sent_stanza?.querySelector('item[affiliation="member"]')); expect(sent_stanza).toEqualStanza( stx` diff --git a/src/plugins/muc-views/tests/mentions.js b/src/plugins/muc-views/tests/mentions.js index 52e76acb76..f6d26a9054 100644 --- a/src/plugins/muc-views/tests/mentions.js +++ b/src/plugins/muc-views/tests/mentions.js @@ -1,10 +1,10 @@ /*global mock, converse */ - const { Strophe, sizzle, stx, u } = converse.env; - describe("An incoming groupchat message", function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + it("is specially marked when you are mentioned in it", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { @@ -86,7 +86,7 @@ describe("An incoming groupchat message", function () { const muc_jid = 'lounge@montague.lit'; const nick = 'romeo'; - await mock.openAndEnterChatRoom(_converse, muc_jid, nick); + const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick); const view = _converse.chatboxviews.get(muc_jid); _converse.api.connection.get()._dataRecv(mock.createRequest( stx` ` )); + await u.waitUntil(() => muc.occupants.length === 1); + const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); textarea.value = 'hello @ThUnD3r|Gr33n' const enter_event = { @@ -113,15 +115,16 @@ describe("An incoming groupchat message", function () { await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); const sent_stanzas = _converse.api.connection.get().sent_stanzas; const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop()); - expect(Strophe.serialize(msg)) - .toBe(``+ - `hello ThUnD3r|Gr33n`+ - ``+ - ``+ - ``+ - ``); + expect(msg).toEqualStanza( + stx` + hello ThUnD3r|Gr33n + + + + `); const message = await u.waitUntil(() => view.querySelector('.chat-msg__text')); expect(message.innerHTML.replace(//g, '')).toBe('hello ThUnD3r|Gr33n'); @@ -170,6 +173,8 @@ describe("An incoming groupchat message", function () { describe("A sent groupchat message", function () { + beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); + describe("in which someone is mentioned", function () { it("gets parsed for mentions which get turned into references", @@ -473,7 +478,7 @@ describe("A sent groupchat message", function () { const nick = 'romeo'; const muc_jid = 'lounge@montague.lit'; - await mock.openAndEnterChatRoom(_converse, muc_jid, nick); + const muc = await mock.openAndEnterChatRoom(_converse, muc_jid, nick); const view = _converse.chatboxviews.get(muc_jid); ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { @@ -487,7 +492,7 @@ describe("A sent groupchat message", function () { `)); }); - await u.waitUntil(() => view.model.occupants.length === 5); + await u.waitUntil(() => muc.occupants.length === 5); spyOn(_converse.api.connection.get(), 'send'); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); @@ -502,18 +507,20 @@ describe("A sent groupchat message", function () { message_form.onKeyDown(enter_event); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); - const msg = _converse.api.connection.get().send.calls.all()[1].args[0]; - expect(Strophe.serialize(msg)) - .toBe(``+ - `hello z3r0 gibson mr.robot, how are you?`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``); + const msg = _converse.api.connection.get().send.calls.all()[0].args[0]; + expect(msg).toEqualStanza( + stx` + hello z3r0 gibson mr.robot, how are you? + + + + + + `); })); }); diff --git a/src/plugins/muc-views/tests/muc-avatar.js b/src/plugins/muc-views/tests/muc-avatar.js index 2d340b49f0..215639fb78 100644 --- a/src/plugins/muc-views/tests/muc-avatar.js +++ b/src/plugins/muc-views/tests/muc-avatar.js @@ -39,7 +39,7 @@ describe('Groupchats', () => { // have to mock stanza traffic. }, async function (_converse) { - const { Strophe, u } = converse.env; + const { u } = converse.env; const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; const muc_jid = 'coven@chat.shakespeare.lit'; await mock.waitForRoster(_converse, 'current', 0); @@ -144,6 +144,7 @@ describe('Groupchats', () => { `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.model.occupants.length === 2); els = modal.querySelectorAll('p.room-info'); expect(els[3].textContent).toBe('Online users: 2'); diff --git a/src/plugins/muc-views/tests/muc-mentions.js b/src/plugins/muc-views/tests/muc-mentions.js index ace88b8e1f..1d6609ae35 100644 --- a/src/plugins/muc-views/tests/muc-mentions.js +++ b/src/plugins/muc-views/tests/muc-mentions.js @@ -8,9 +8,9 @@ describe("MUC Mention Notfications", function () { it("may be received from a MUC in which the user is not currently present", mock.initConverse([], { - 'allow_bookmarks': false, // Hack to get the rooms list to render - 'muc_subscribe_to_rai': true, - 'view_mode': 'fullscreen'}, + allow_bookmarks: false, // Hack to get the rooms list to render + muc_subscribe_to_rai: true, + view_mode: 'overlayed'}, async function (_converse) { const { api } = _converse; @@ -19,16 +19,18 @@ describe("MUC Mention Notfications", function () { const muc_jid = 'lounge@montague.lit'; const nick = 'romeo'; - const muc_creation_promise = await api.rooms.open(muc_jid, {nick, 'hidden': true}, false); + const muc_creation_promise = await api.rooms.open(muc_jid, { nick }, false); await mock.getRoomFeatures(_converse, muc_jid, []); await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); await muc_creation_promise; const model = _converse.chatboxes.get(muc_jid); await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - expect(model.get('hidden')).toBe(true); + + model.save('hidden', true); await u.waitUntil(() => model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED); + await mock.openControlBox(_converse); const room_el = await u.waitUntil(() => document.querySelector("converse-rooms-list .available-chatroom")); expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy(); diff --git a/src/plugins/muc-views/tests/muc-messages.js b/src/plugins/muc-views/tests/muc-messages.js index 31e550d096..6b9781810e 100644 --- a/src/plugins/muc-views/tests/muc-messages.js +++ b/src/plugins/muc-views/tests/muc-messages.js @@ -154,7 +154,7 @@ describe("A Groupchat Message", function () { mock.initConverse([], {}, async function (_converse) { const muc_jid = 'lounge@montague.lit'; - await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const model = await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); const view = _converse.chatboxviews.get(muc_jid); let msg = stx` `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => model.occupants.length === 2); + await u.waitUntil(() => view.model.messages.last().occupant); - expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); - expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); - expect(view.model.messages.last().occupant.get('jid')).toBe('some1@montague.lit'); + const last_msg = view.model.messages.last(); + expect(last_msg.get('message')).toBe('Message from someone not in the MUC right now'); + expect(last_msg.occupant.get('nick')).toBe('some1'); + + await u.waitUntil(() => last_msg.occupant.get('jid') === 'some1@montague.lit'); presence = stx` `); + expect(sent_stanza).toEqualStanza( + stx``); + + // clear sent stanzas + const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; + while (IQ_stanzas.length) IQ_stanzas.pop(); // Two presence stanzas are received from the MUC service _converse.api.connection.get()._dataRecv(mock.createRequest( @@ -61,11 +68,7 @@ describe("A MUC", function () { ` )); - expect(model.get('nick')).toBe(newnick); - - // clear sent stanzas - const IQ_stanzas = _converse.api.connection.get().IQ_stanzas; - while (IQ_stanzas.length) IQ_stanzas.pop(); + await u.waitUntil(() => model.get('nick') === newnick); // Check that the new nickname gets registered with the MUC _converse.api.connection.get()._dataRecv(mock.createRequest( diff --git a/src/plugins/muc-views/tests/probes.js b/src/plugins/muc-views/tests/probes.js index 9739d619d8..eb23da8172 100644 --- a/src/plugins/muc-views/tests/probes.js +++ b/src/plugins/muc-views/tests/probes.js @@ -39,7 +39,7 @@ describe("Groupchats", function () { `; _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); - expect(occupant.get('affiliation')).toBe('member'); + await u.waitUntil(() => occupant.get('affiliation') === 'member'); expect(occupant.get('role')).toBe('participant'); // Check that unavailable but affiliated occupants don't get destroyed @@ -70,7 +70,7 @@ describe("Groupchats", function () { _converse.api.connection.get()._dataRecv(mock.createRequest(presence)); expect(view.model.occupants.length).toBe(3); - expect(occupant.get('affiliation')).toBe('member'); + await u.waitUntil(() => occupant.get('affiliation') === 'member'); expect(occupant.get('role')).toBe('participant'); })); }); diff --git a/src/plugins/muc-views/tests/rai.js b/src/plugins/muc-views/tests/rai.js index a31e86c219..5ecfd2ac45 100644 --- a/src/plugins/muc-views/tests/rai.js +++ b/src/plugins/muc-views/tests/rai.js @@ -124,9 +124,9 @@ describe("XEP-0437 Room Activity Indicators", function () { it("will be activated for a MUC that starts out hidden", mock.initConverse( [], { - 'allow_bookmarks': false, // Hack to get the rooms list to render - 'muc_subscribe_to_rai': true, - 'view_mode': 'fullscreen'}, + allow_bookmarks: false, // Hack to get the rooms list to render + muc_subscribe_to_rai: true, + view_mode: 'fullscreen'}, async function (_converse) { const { api } = _converse; @@ -136,15 +136,15 @@ describe("XEP-0437 Room Activity Indicators", function () { const nick = 'romeo'; const sent_stanzas = _converse.api.connection.get().sent_stanzas; - const muc_creation_promise = await api.rooms.open(muc_jid, {nick, 'hidden': true}, false); + const muc_creation_promise = await api.rooms.open(muc_jid, { nick }, false); await mock.getRoomFeatures(_converse, muc_jid, []); await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); await muc_creation_promise; const model = _converse.chatboxes.get(muc_jid); await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - expect(model.get('hidden')).toBe(true); + model.set('hidden', true); const getSentPresences = () => sent_stanzas.filter(s => s.nodeName === 'presence'); await u.waitUntil(() => getSentPresences().length === 3, 500);