diff --git a/CHANGES.md b/CHANGES.md index 3c8da1fa6..c1a4f94af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,8 +21,9 @@ - Add a checkbox to indicate whether a trusted device is being used or not. If the device is not trusted, sessionStorage is used and all user data is deleted from the browser cache upon logout. If the device is trusted, localStorage is used and user data is cached indefinitely. -- Initial support for XEP-0357 Push Notifications, specifically registering an "App Server". +- Initial support for [XEP-0357 Push Notifications](https://xmpp.org/extensions/xep-0357.html), specifically registering an "App Server". - Add support for logging in via OAuth (see the [oauth_providers](https://conversejs.org/docs/html/configurations.html#oauth-providers) setting) +- Add support for [XEP-0372 References](https://xmpp.org/extensions/xep-0372.html), specifically section "3.2 Mentions". ### Bugfixes diff --git a/css/converse.css b/css/converse.css index 608d8ed0b..c5f39b632 100644 --- a/css/converse.css +++ b/css/converse.css @@ -8540,9 +8540,6 @@ body.reset { #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chat-info.badge, #conversejs .chatroom .box-flyout .chatroom-body .chat-info.badge { color: white; } - #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .mentioned, - #conversejs .chatroom .box-flyout .chatroom-body .mentioned { - font-weight: bold; } #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .disconnect-container, #conversejs .chatroom .box-flyout .chatroom-body .disconnect-container { margin: 1em; @@ -8799,6 +8796,10 @@ body.reset { border: 1.2em solid #E7A151; border-top: 0.8em solid #E7A151; } +#conversejs .message .mention { + font-weight: bold; } +#conversejs .message .mention--self { + font-weight: normal; } #conversejs .message.date-separator { height: 2em; margin: 0; diff --git a/sass/_chatrooms.scss b/sass/_chatrooms.scss index 2f2a1477c..6dc78638b 100644 --- a/sass/_chatrooms.scss +++ b/sass/_chatrooms.scss @@ -116,9 +116,6 @@ color: $chat-head-text-color; } } - .mentioned { - font-weight: bold; - } .disconnect-container { margin: 1em; width: 100%; diff --git a/sass/_messages.scss b/sass/_messages.scss index 8bdfb6d48..4edc4f85b 100644 --- a/sass/_messages.scss +++ b/sass/_messages.scss @@ -1,6 +1,12 @@ #conversejs { .message { + .mention { + font-weight: bold; + } + .mention--self { + font-weight: normal; + } &.date-separator { height: 2em; margin: 0; diff --git a/spec/messages.js b/spec/messages.js index a2609403f..13bea05d0 100644 --- a/spec/messages.js +++ b/spec/messages.js @@ -2089,6 +2089,52 @@ }).catch(_.partial(console.error, _)); })); + describe("when received", function () { + + it("highlights all users mentioned via XEP-0372 references", + mock.initConverseWithPromises( + null, ['rosterGroupsFetched'], {}, + function (done, _converse) { + + test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom') + .then(() => { + const view = _converse.chatboxviews.get('lounge@localhost'); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { + _converse.connection._dataRecv(test_utils.createRequest( + $pres({ + 'to': 'tom@localhost/resource', + 'from': `lounge@localhost/${nick}` + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `${nick}@localhost/resource`, + 'role': 'participant' + })) + ); + }); + + const msg = $msg({ + from: 'lounge@localhost/gibson', + id: (new Date()).getTime(), + to: 'dummy@localhost', + type: 'groupchat' + }).c('body').t('hello z3r0 tom mr.robot, how are you?').up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@localhost'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:dummy@localhost'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@localhost'}).nodeTree; + view.model.onMessage(msg); + + expect(view.el.querySelectorAll('.chat-msg__text').length).toBe(1); + expect(view.el.querySelector('.chat-msg__text').outerHTML).toBe( + '
hello z3r0 '+ + 'tom '+ + 'mr.robot, how are you?
'); + done(); + }).catch(_.partial(console.error, _)); + })); + }); + describe("in which someone is mentioned", function () { it("gets parsed for mentions which get turned into references", @@ -2113,33 +2159,33 @@ }))); }); - // Run a few unit tests for the parseForReferences method - let [text, references] = view.model.parseForReferences('hello z3r0') + // Run a few unit tests for the parseTextForReferences method + let [text, references] = view.model.parseTextForReferences('hello z3r0') expect(references.length).toBe(0); expect(text).toBe('hello z3r0'); - [text, references] = view.model.parseForReferences('hello @z3r0') + [text, references] = view.model.parseTextForReferences('hello @z3r0') expect(references.length).toBe(1); expect(text).toBe('hello z3r0'); expect(JSON.stringify(references)) .toBe('[{"begin":6,"end":10,"type":"mention","uri":"xmpp:z3r0@localhost"}]'); - [text, references] = view.model.parseForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?') + [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?') expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?'); expect(JSON.stringify(references)) .toBe('[{"begin":13,"end":17,"type":"mention","uri":"xmpp:z3r0@localhost"},'+ '{"begin":18,"end":24,"type":"mention","uri":"xmpp:gibson@localhost"},'+ '{"begin":25,"end":33,"type":"mention","uri":"xmpp:mr.robot@localhost"}]'); - [text, references] = view.model.parseForReferences('yo @gib') + [text, references] = view.model.parseTextForReferences('yo @gib') expect(text).toBe('yo @gib'); expect(references.length).toBe(0); - [text, references] = view.model.parseForReferences('yo @gibsonian') + [text, references] = view.model.parseTextForReferences('yo @gibsonian') expect(text).toBe('yo @gibsonian'); expect(references.length).toBe(0); - [text, references] = view.model.parseForReferences('@gibson') + [text, references] = view.model.parseTextForReferences('@gibson') expect(text).toBe('gibson'); expect(references.length).toBe(1); expect(JSON.stringify(references)) @@ -2190,9 +2236,9 @@ `xmlns='jabber:client'>`+ `hello z3r0 gibson mr.robot, how are you?`+ ``+ - ``+ - ``+ ``+ + ``+ + ``+ ``); done(); }).catch(_.partial(console.error, _)); diff --git a/src/converse-chatboxes.js b/src/converse-chatboxes.js index 6f943b452..33a78928a 100644 --- a/src/converse-chatboxes.js +++ b/src/converse-chatboxes.js @@ -299,6 +299,7 @@ older_versions.push(message.get('message')); message.save({ 'message': _converse.chatboxes.getMessageBody(stanza), + 'references': this.getReferencesFromStanza(stanza), 'older_versions': older_versions, 'edited': true }); @@ -459,6 +460,17 @@ }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); }, + getReferencesFromStanza (stanza) { + return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => { + return { + 'begin': ref.getAttribute('begin'), + 'end': ref.getAttribute('end'), + 'type': ref.getAttribute('type'), + 'uri': ref.getAttribute('uri') + }; + }); + }, + getMessageAttributesFromStanza (stanza, original_stanza) { /* Parses a passed in message stanza and returns an object * of attributes. @@ -488,6 +500,7 @@ 'is_delayed': !_.isNil(delay), 'is_spoiler': !_.isNil(spoiler), 'message': _converse.chatboxes.getMessageBody(stanza) || undefined, + 'references': this.getReferencesFromStanza(stanza), 'msgid': stanza.getAttribute('id'), 'time': delay ? delay.getAttribute('stamp') : moment().format(), 'type': stanza.getAttribute('type') diff --git a/src/converse-message-view.js b/src/converse-message-view.js index d88da1931..afdf23741 100644 --- a/src/converse-message-view.js +++ b/src/converse-message-view.js @@ -167,6 +167,7 @@ text = xss.filterXSS(text, {'whiteList': {}}); msg_content.innerHTML = _.flow( _.partial(u.geoUriToHttp, _, _converse.geouri_replacement), + _.partial(u.addMentions, _, this.model.get('references'), this.model.collection.chatbox), u.addHyperlinks, u.renderNewLines, _.partial(u.addEmoji, _converse, emojione, _) diff --git a/src/converse-muc.js b/src/converse-muc.js index 4fac4995e..2ca75088e 100644 --- a/src/converse-muc.js +++ b/src/converse-muc.js @@ -349,7 +349,7 @@ return; }, - parseForReferences (text) { + parseTextForReferences (text) { const refs = []; let index = 0; while (index < (text || '').length) { @@ -368,7 +368,7 @@ getOutgoingMessageAttributes (text, spoiler_hint) { const is_spoiler = this.get('composing_spoiler'); var references; - [text, references] = this.parseForReferences(text); + [text, references] = this.parseTextForReferences(text); return { 'from': `${this.get('jid')}/${this.get('nick')}`, diff --git a/src/utils/core.js b/src/utils/core.js index 39de2c164..35516347c 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -229,6 +229,25 @@ return encodeURI(decodeURI(url)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); }; + u.addMentions = function (text, references, chatbox) { + if (chatbox.get('message_type') !== 'groupchat') { + return text; + } + const nick = chatbox.get('nick'); + references + .sort((a, b) => b.begin - a.begin) + .forEach(ref => { + const mention = text.slice(ref.begin, ref.end) + chatbox; + if (mention === nick) { + text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end); + } else { + text = text.slice(0, ref.begin) + `${mention}` + text.slice(ref.end); + } + }); + return text; + }; + u.addHyperlinks = function (text) { return URI.withinString(text, function (url) { var uri = new URI(url);