Add support for XEP-0424 and XEP-0425
- Add support for switching ephemerality after message creation - Move more methods from ChatBox and ChatRoom to utils/stanza.js - Rename 'ephemeral' to 'is_ephemeral' since it's a boolean
This commit is contained in:
parent
4b3d427cff
commit
b4dafcc45b
14
CHANGES.md
14
CHANGES.md
|
@ -2,10 +2,9 @@
|
|||
|
||||
## 6.0.0 (Unreleased)
|
||||
|
||||
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
|
||||
- #1105: Preliminary support for storing persistent data in IndexedDB instead of localStorage
|
||||
- #1691: Fix `collection.chatbox is undefined` errors
|
||||
- #1772: `_converse.api.contact.add(jid, nick)` fails, says not a function
|
||||
- Add support for [XEP-0424 Message Retraction](http://localhost:3080/extensions/xep-0424.html)
|
||||
- Add support for [XEP-0425 Message Moderation](http://localhost:3080/extensions/xep-0425.html)
|
||||
- Prevent editing of sent file uploads.
|
||||
- Initial support for sending custom emojis. Currently only between Converse
|
||||
instances. Still working out a wire protocol for compatibility with other clients.
|
||||
To add custom emojis, edit the `emojis.json` file.
|
||||
|
@ -14,6 +13,13 @@
|
|||
- New config option [muc_mention_autocomplete_filter](https://conversejs.org/docs/html/configuration.html#muc_mention_autocomplete_filter)
|
||||
- New config option [muc_mention_autocomplete_show_avatar](https://conversejs.org/docs/html/configuration.html#muc_mention_autocomplete_show_avatar)
|
||||
|
||||
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
|
||||
- #1105: Preliminary support for storing persistent data in IndexedDB instead of localStorage
|
||||
- #1691: Fix `collection.chatbox is undefined` errors
|
||||
- #1733: New message notifications for a minimized chat stack on top of each other
|
||||
- #1757: Chats are hidden behind the controlbox on mobile
|
||||
- #1772 `_converse.api.contact.add(jid, nick)` fails, says not a function
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- In contrast to sessionStorage and localStorage, IndexedDB is an asynchronous database.
|
||||
|
|
12
README.md
12
README.md
|
@ -39,9 +39,9 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
|
|||
|
||||
## Features
|
||||
- Available as overlayed chat boxes or as a fullscreen application. See [inverse.chat](https://inverse.chat) for the fullscreen version.
|
||||
- Custom status messages
|
||||
- Desktop notifications
|
||||
- A [plugin architecture](https://conversejs.org/docs/html/plugin_development.html) based on [pluggable.js](https://conversejs.github.io/pluggable.js/)
|
||||
- Single-user and group chats
|
||||
- Contacts and groups
|
||||
- Multi-user chat rooms [XEP 45](https://xmpp.org/extensions/xep-0045.html)
|
||||
- Chatroom bookmarks [XEP 48](https://xmpp.org/extensions/xep-0048.html)
|
||||
- Direct invitations to chat rooms [XEP 249](https://xmpp.org/extensions/xep-0249.html)
|
||||
|
@ -50,9 +50,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
|
|||
- In-band registration [XEP 77](https://xmpp.org/extensions/xep-0077.html)
|
||||
- Roster item exchange [XEP 144](https://xmpp.org/extensions/tmp/xep-0144-1.1.html)
|
||||
- Chat statuses (online, busy, away, offline)
|
||||
- Custom status messages
|
||||
- Typing and state notifications [XEP 85](https://xmpp.org/extensions/xep-0085.html)
|
||||
- Desktop notifications
|
||||
- File sharing / HTTP File Upload [XEP 363](https://xmpp.org/extensions/xep-0363.html)
|
||||
- Messages appear in all connnected chat clients / Message Carbons [XEP 280](https://xmpp.org/extensions/xep-0280.html)
|
||||
- Third person "/me" messages [XEP 245](https://xmpp.org/extensions/xep-0245.html)
|
||||
|
@ -62,8 +60,10 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
|
|||
- Client state indication [XEP 352](https://xmpp.org/extensions/xep-0352.html)
|
||||
- Last Message Correction [XEP 308](https://xmpp.org/extensions/xep-0308.html)
|
||||
- OMEMO encrypted messaging [XEP 384](https://xmpp.org/extensions/xep-0384.html")
|
||||
- Supports anonymous logins, see the [anonymous login demo](https://conversejs.org/demo/anonymous.html).
|
||||
- Translated into 28 languages
|
||||
- Anonymous logins, see the [anonymous login demo](https://conversejs.org/demo/anonymous.html)
|
||||
- Message Retractions [XEP-424](https://xmpp.org/extensions/xep-0424.html)
|
||||
- Message Moderation [XEP-425](https://xmpp.org/extensions/xep-0425.html)
|
||||
- Translated into over 30 languages
|
||||
|
||||
## Integration into other frameworks
|
||||
|
||||
|
|
|
@ -1475,6 +1475,26 @@ not fulfilled.
|
|||
|
||||
Requires the `src/converse-notification.js` plugin.
|
||||
|
||||
show_retraction_warning
|
||||
-----------------------
|
||||
|
||||
* Default: ``true``
|
||||
|
||||
From `XEP-0424: Message Retraction <https://xmpp.org/extensions/xep-0424.html>`_:
|
||||
|
||||
::
|
||||
Due to the federated and extensible nature of XMPP it's not possible to remove a message with
|
||||
full certainty and a retraction can only be considered an unenforceable request for such removal.
|
||||
Clients which don't support message retraction are not obligated to enforce the request and
|
||||
people could have seen or copied the message contents already.
|
||||
|
||||
By default Converse shows a warning to users when they retract a message, to
|
||||
inform them that they don't have a guarantee that the message will be removed
|
||||
everywhere.
|
||||
|
||||
This warning isn't applicable to all deployments of Converse and can therefore
|
||||
be turned off by setting this config variable to ``false``.
|
||||
|
||||
use_system_emojis
|
||||
-----------------
|
||||
* Default: ``true``
|
||||
|
|
12
index.html
12
index.html
|
@ -171,8 +171,8 @@
|
|||
See <a href="https://inverse.chat" target="_blank" rel="noopener">inverse.chat</a> for the fullscreen version.
|
||||
</li>
|
||||
<li>A <a href="https://conversejs.org/docs/html/plugin_development.html" target="_blank" rel="noopener">plugin architecture</a> based on <a href="https://conversejs.github.io/pluggable.js/" target="_blank" rel="noopener">pluggable.js</a></li>
|
||||
<li>Single-user and group chat</li>
|
||||
<li>Contacts and groups</li>
|
||||
<li>Chat statuses (online, busy, away, offline)</li>
|
||||
<li>Desktop notifications</li>
|
||||
<li>Multi-user chatrooms (<a href="https://xmpp.org/extensions/xep-0045.html" target="_blank" rel="noopener">XEP 45</a>)</li>
|
||||
<li>Chatroom bookmarks (<a href="https://xmpp.org/extensions/xep-0048.html" target="_blank" rel="noopener">XEP 48</a>)</li>
|
||||
<li>Direct invitations to chat rooms (<a href="https://xmpp.org/extensions/xep-0249.html" target="_blank" rel="noopener">XEP 249</a>)</li>
|
||||
|
@ -180,10 +180,8 @@
|
|||
<li>Service discovery (<a href="https://xmpp.org/extensions/xep-0030.html" target="_blank" rel="noopener">XEP 30</a>)</li>
|
||||
<li>In-band registration (<a href="https://xmpp.org/extensions/xep-0077.html" target="_blank" rel="noopener">XEP 77</a>)</li>
|
||||
<li>Roster item exchange (<a href="https://xmpp.org/extensions/xep-0144.html" target="_blank" rel="noopener">XEP 144</a>)</li>
|
||||
<li>Chat statuses (online, busy, away, offline)</li>
|
||||
<li>Custom status messages</li>
|
||||
<li>Typing and chat state notifications (<a href="https://xmpp.org/extensions/xep-0085.html" target="_blank" rel="noopener">XEP 85</a>)</li>
|
||||
<li>Desktop notifications</li>
|
||||
<li>File sharing / HTTP File Upload (<a href="https://xmpp.org/extensions/xep-0363.html" target="_blank" rel="noopener">XEP 363</a>)</li>
|
||||
<li>Messages appear in all connected chat clients / Message Carbons (<a href="https://xmpp.org/extensions/xep-0280.html" target="_blank" rel="noopener">XEP 280</a>)</li>
|
||||
<li>Third person "/me" messages (<a href="https://xmpp.org/extensions/xep-0245.html" target="_blank" rel="noopener">XEP 245</a>)</li>
|
||||
|
@ -193,8 +191,10 @@
|
|||
<li>Client state indication (<a href="https://xmpp.org/extensions/xep-0352.html" target="_blank" rel="noopener">XEP 352</a>)</li>
|
||||
<li>Last Message Correction (<a href="https://xmpp.org/extensions/xep-0308.html" target="_blank" rel="noopener">XEP 308</a>)</li>
|
||||
<li>OMEMO encrypted messaging (<a href="https://xmpp.org/extensions/xep-0384.html" target="_blank" rel="noopener">XEP 384</a>)</li>
|
||||
<li>Supports anonymous logins, see the <a href="https://conversejs.org/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a>.</li>
|
||||
<li>Translated into 29 languages</li>
|
||||
<li>Anonymous logins, see the <a href="https://conversejs.org/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a></li>
|
||||
<li>Message Retractions (<a href="https://xmpp.org/extensions/xep-0424.html" target="_blank" rel="noopener">XEP 424</a>)</li>
|
||||
<li>Message Moderation (<a href="https://xmpp.org/extensions/xep-0425.html" target="_blank" rel="noopener">XEP 425</a>)</li>
|
||||
<li>Translated into over 30 languages</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -143,6 +143,9 @@
|
|||
&.badge {
|
||||
color: var(--chat-head-text-color);
|
||||
}
|
||||
&.chat-msg--retracted {
|
||||
color: var(--subdued-color);
|
||||
}
|
||||
}
|
||||
.disconnect-container {
|
||||
margin: 1em;
|
||||
|
|
|
@ -317,6 +317,16 @@ body.converse-fullscreen {
|
|||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
q {
|
||||
quotes: "“" "”" "‘" "’";
|
||||
}
|
||||
q:before {
|
||||
content: open-quote;
|
||||
}
|
||||
q:after {
|
||||
content: close-quote;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.chat-msg--retracted {
|
||||
.chat-msg__message {
|
||||
color: var(--subdued-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.chat-info {
|
||||
color: var(--chat-head-color);
|
||||
font-size: var(--message-font-size);
|
||||
|
@ -46,6 +52,9 @@
|
|||
font-size: 90%;
|
||||
padding: 0.17rem 1rem;
|
||||
|
||||
&.chat-msg--followup {
|
||||
margin-left: 2.75rem;
|
||||
}
|
||||
&.badge {
|
||||
color: var(--chat-head-text-color);
|
||||
}
|
||||
|
@ -60,6 +69,9 @@
|
|||
color: var(--error-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
.q {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-image {
|
||||
|
@ -225,7 +237,7 @@
|
|||
height: var(--message-font-size);
|
||||
font-size: var(--message-font-size);
|
||||
padding: 0;
|
||||
padding-left: 0.5em;
|
||||
padding-left: 0.75em;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
background: transparent;
|
||||
|
@ -336,6 +348,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.chat-info {
|
||||
&.chat-msg--followup {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
#conversejs {
|
||||
#converse-modals {
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 2em;
|
||||
.confirm {
|
||||
.form-group {
|
||||
p:first-child {
|
||||
font-size: 110%;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-container {
|
||||
|
|
|
@ -720,7 +720,6 @@
|
|||
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
|
||||
await test_utils.waitForRoster(_converse, 'current');
|
||||
// Send a message from a different resource
|
||||
spyOn(_converse, 'log');
|
||||
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
||||
const msg = $msg({
|
||||
|
@ -848,7 +847,6 @@
|
|||
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
|
||||
await test_utils.waitForRoster(_converse, 'current');
|
||||
// Send a message from a different resource
|
||||
spyOn(_converse, 'log');
|
||||
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
|
||||
const msg = $msg({
|
||||
|
|
|
@ -1037,7 +1037,7 @@
|
|||
|
||||
const view = _converse.chatboxviews.get(contact_jid);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('ephemeral')).toBe(false);
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
|
||||
expect(view.model.messages.at(0).get('type')).toBe('error');
|
||||
expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
expect(textarea.value).toBe('');
|
||||
|
||||
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
|
||||
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(1);
|
||||
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
|
||||
let action = view.el.querySelector('.chat-msg .chat-msg__action');
|
||||
expect(action.getAttribute('title')).toBe('Edit this message');
|
||||
|
||||
|
@ -160,7 +160,7 @@
|
|||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
|
||||
);
|
||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
||||
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(1);
|
||||
expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2);
|
||||
|
||||
// Test confirmation dialog
|
||||
spyOn(window, 'confirm').and.returnValue(true);
|
||||
|
|
|
@ -5232,6 +5232,7 @@
|
|||
const textarea = view.el.querySelector('.chat-textarea');
|
||||
textarea.value = 'Hello world';
|
||||
view.onFormSubmitted(new Event('submit'));
|
||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
||||
|
||||
const stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client" type="error" to="troll@montague.lit/resource" from="trollbox@montague.lit">
|
||||
|
@ -5240,6 +5241,7 @@
|
|||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
await new Promise(resolve => view.once('messageInserted', resolve));
|
||||
|
||||
expect(view.el.querySelector('.chat-error').textContent.trim()).toBe(
|
||||
"Your message was not delivered because you're not allowed to send messages in this groupchat.");
|
||||
done();
|
||||
|
|
|
@ -0,0 +1,900 @@
|
|||
(function (root, factory) {
|
||||
define([
|
||||
"jasmine",
|
||||
"mock",
|
||||
"test-utils"
|
||||
], factory);
|
||||
} (this, function (jasmine, mock, test_utils) {
|
||||
"use strict";
|
||||
const { Strophe, $iq } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
|
||||
async function sendAndThenRetractMessage (_converse, view) {
|
||||
view.model.sendMessage('hello world');
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
const msg_obj = view.model.messages.at(0);
|
||||
const reflection_stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client"
|
||||
from="${msg_obj.get('from')}"
|
||||
to="${_converse.connection.jid}"
|
||||
type="groupchat">
|
||||
<msg_body>${msg_obj.get('message')}</msg_body>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0"
|
||||
id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
|
||||
by="lounge@montague.lit"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="${msg_obj.get('origin_id')}"/>
|
||||
</message>`);
|
||||
await view.model.onMessage(reflection_stanza);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500);
|
||||
|
||||
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
|
||||
retract_button.click();
|
||||
await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
|
||||
const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
|
||||
submit_button.click();
|
||||
const sent_stanzas = _converse.connection.sent_stanzas;
|
||||
return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
|
||||
}
|
||||
|
||||
|
||||
describe("Message Retractions", function () {
|
||||
|
||||
describe("A groupchat message retraction", function () {
|
||||
|
||||
it("is not applied if it's not from the right author",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
|
||||
const received_stanza = u.toStanza(`
|
||||
<message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
|
||||
<body>Hello world</body>
|
||||
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
|
||||
</message>
|
||||
`);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
await view.model.onMessage(received_stanza);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||
|
||||
const retraction_stanza = u.toStanza(`
|
||||
<message type="groupchat" id='retraction-id-1' from="${muc_jid}/mallory" to="${muc_jid}/romeo">
|
||||
<apply-to id="stanza-id-1" xmlns="urn:xmpp:fasten:0">
|
||||
<retract xmlns="urn:xmpp:message-retract:0" />
|
||||
</apply-to>
|
||||
</message>
|
||||
`);
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
|
||||
expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||
expect(view.model.messages.length).toBe(2);
|
||||
expect(view.model.messages.at(1).get('retracted')).toBeTruthy();
|
||||
expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy();
|
||||
expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true);
|
||||
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||
done();
|
||||
}));
|
||||
|
||||
it("can be received before the message it pertains to",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const date = (new Date()).toISOString();
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
|
||||
const retraction_stanza = u.toStanza(`
|
||||
<message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
|
||||
<apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
|
||||
<retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
|
||||
</apply-to>
|
||||
</message>
|
||||
`);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
spyOn(converse.env.log, 'warn');
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
|
||||
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
|
||||
await u.waitUntil(() => view.model.messages.length === 1);
|
||||
expect(view.model.handleRetraction.calls.first().returnValue).toBe(true);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
|
||||
expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true);
|
||||
|
||||
const received_stanza = u.toStanza(`
|
||||
<message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
|
||||
<body>Hello world</body>
|
||||
<delay xmlns='urn:xmpp:delay' stamp='${date}'/>
|
||||
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="origin-id-1"/>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(received_stanza));
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
|
||||
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
|
||||
const message = view.model.messages.at(0)
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('dangling_retraction')).toBe(false);
|
||||
expect(message.get('origin_id')).toBe('origin-id-1');
|
||||
expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1');
|
||||
expect(message.get('time')).toBe(date);
|
||||
expect(message.get('type')).toBe('groupchat');
|
||||
expect(view.model.handleRetraction.calls.all().pop().returnValue).toBe(true);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("A message retraction", function () {
|
||||
|
||||
it("can be received before the message it pertains to",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const date = (new Date()).toISOString();
|
||||
await test_utils.waitForRoster(_converse, 'current', 1);
|
||||
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
const view = await test_utils.openChatBoxFor(_converse, contact_jid);
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
|
||||
const retraction_stanza = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}"
|
||||
to="${_converse.bare_jid}"
|
||||
from="${contact_jid}"
|
||||
type="chat"
|
||||
xmlns="jabber:client">
|
||||
<apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
|
||||
<retract xmlns="urn:xmpp:message-retract:0"/>
|
||||
</apply-to>
|
||||
</message>
|
||||
`);
|
||||
|
||||
const promise = new Promise(resolve => _converse.api.listen.on('messageAdded', resolve));
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
|
||||
await u.waitUntil(() => view.model.messages.length === 1);
|
||||
await promise;
|
||||
const message = view.model.messages.at(0);
|
||||
expect(message.get('dangling_retraction')).toBe(true);
|
||||
expect(message.get('is_ephemeral')).toBe(false);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
|
||||
|
||||
const stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client"
|
||||
to="${_converse.bare_jid}"
|
||||
type="chat"
|
||||
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
|
||||
from="${contact_jid}">
|
||||
<body>Hello world</body>
|
||||
<delay xmlns='urn:xmpp:delay' stamp='${date}'/>
|
||||
<markable xmlns="urn:xmpp:chat-markers:0"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
|
||||
</message>`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('dangling_retraction')).toBe(false);
|
||||
expect(message.get('origin_id')).toBe('2e972ea0-0050-44b7-a830-f6638a2595b3');
|
||||
expect(message.get('time')).toBe(date);
|
||||
expect(message.get('type')).toBe('chat');
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("A Received Chat Message", function () {
|
||||
|
||||
it("can be followed up by a retraction",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
await test_utils.waitForRoster(_converse, 'current', 1);
|
||||
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
const view = await test_utils.openChatBoxFor(_converse, contact_jid);
|
||||
|
||||
let stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client"
|
||||
to="${_converse.bare_jid}"
|
||||
type="chat"
|
||||
id="29132ea0-0121-2897-b121-36638c259554"
|
||||
from="${contact_jid}">
|
||||
<body>😊</body>
|
||||
<markable xmlns="urn:xmpp:chat-markers:0"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="29132ea0-0121-2897-b121-36638c259554"/>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" id="kxViLhgbnNMcWv10" by="${_converse.bare_jid}"/>
|
||||
</message>`);
|
||||
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
await u.waitUntil(() => view.model.messages.length === 1);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
|
||||
stanza = u.toStanza(`
|
||||
<message xmlns="jabber:client"
|
||||
to="${_converse.bare_jid}"
|
||||
type="chat"
|
||||
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
|
||||
from="${contact_jid}">
|
||||
<body>This message will be retracted</body>
|
||||
<markable xmlns="urn:xmpp:chat-markers:0"/>
|
||||
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="${_converse.bare_jid}"/>
|
||||
</message>`);
|
||||
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
await u.waitUntil(() => view.model.messages.length === 2);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2);
|
||||
|
||||
const retraction_stanza = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}"
|
||||
to="${_converse.bare_jid}"
|
||||
from="${contact_jid}"
|
||||
type="chat"
|
||||
xmlns="jabber:client">
|
||||
<apply-to id="2e972ea0-0050-44b7-a830-f6638a2595b3" xmlns="urn:xmpp:fasten:0">
|
||||
<retract xmlns="urn:xmpp:message-retract:0"/>
|
||||
</apply-to>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
|
||||
expect(view.model.messages.length).toBe(2);
|
||||
|
||||
const message = view.model.messages.at(1);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
|
||||
expect(msg_el.textContent.trim()).toBe('Mercutio has retracted this message');
|
||||
expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("A Sent Chat Message", function () {
|
||||
|
||||
it("can be retracted by its author",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
await test_utils.waitForRoster(_converse, 'current', 1);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
const view = await test_utils.openChatBoxFor(_converse, contact_jid);
|
||||
|
||||
view.model.sendMessage('hello world');
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
|
||||
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
|
||||
retract_button.click();
|
||||
await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
|
||||
const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
|
||||
submit_button.click();
|
||||
|
||||
const sent_stanzas = _converse.connection.sent_stanzas;
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
|
||||
const msg_obj = view.model.messages.at(0);
|
||||
const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
|
||||
expect(Strophe.serialize(retraction_stanza)).toBe(
|
||||
`<message id="${retraction_stanza.getAttribute('id')}" to="${contact_jid}" type="chat" xmlns="jabber:client">`+
|
||||
`<store xmlns="urn:xmpp:hints"/>`+
|
||||
`<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
|
||||
`<retract xmlns="urn:xmpp:message-retract:0"/>`+
|
||||
`</apply-to>`+
|
||||
`</message>`);
|
||||
|
||||
const message = view.model.messages.at(0);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
|
||||
expect(el.textContent.trim()).toBe('Romeo Montague has retracted this message');
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe("A Received Groupchat Message", function () {
|
||||
|
||||
it("can be followed up by a retraction by the author",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
|
||||
const received_stanza = u.toStanza(`
|
||||
<message to='${_converse.jid}' from='${muc_jid}/eve' type='groupchat' id='${_converse.connection.getUniqueId()}'>
|
||||
<body>Hello world</body>
|
||||
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
|
||||
<origin-id xmlns='urn:xmpp:sid:0' id='origin-id-1' by='${muc_jid}'/>
|
||||
</message>
|
||||
`);
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
await view.model.onMessage(received_stanza);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||
|
||||
const retraction_stanza = u.toStanza(`
|
||||
<message type="groupchat" id='retraction-id-1' from="${muc_jid}/eve" to="${muc_jid}/romeo">
|
||||
<apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
|
||||
<retract by="${muc_jid}/eve" xmlns="urn:xmpp:message-retract:0" />
|
||||
</apply-to>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction_stanza));
|
||||
|
||||
// We opportunistically save the message as retracted, even before receiving the retraction message
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
|
||||
expect(msg_el.textContent.trim()).toBe('eve has retracted this message');
|
||||
expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
|
||||
done();
|
||||
}));
|
||||
|
||||
|
||||
it("can be retracted by a moderator, with the IQ response received before the retraction message",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
const occupant = view.model.getOwnOccupant();
|
||||
expect(occupant.get('role')).toBe('moderator');
|
||||
|
||||
const received_stanza = u.toStanza(`
|
||||
<message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
|
||||
<body>Visit this site to get free Bitcoin!</body>
|
||||
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
|
||||
</message>
|
||||
`);
|
||||
await view.model.onMessage(received_stanza);
|
||||
await u.waitUntil(() => view.model.messages.length === 1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
|
||||
const reason = "This content is inappropriate for this forum!"
|
||||
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
|
||||
retract_button.click();
|
||||
|
||||
await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
|
||||
|
||||
const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
|
||||
reason_input.value = 'This content is inappropriate for this forum!';
|
||||
const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
|
||||
submit_button.click();
|
||||
|
||||
const sent_IQs = _converse.connection.IQ_stanzas;
|
||||
const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
|
||||
const message = view.model.messages.at(0);
|
||||
const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
|
||||
|
||||
expect(Strophe.serialize(stanza)).toBe(
|
||||
`<iq id="${stanza.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
|
||||
`<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">`+
|
||||
`<moderate xmlns="urn:xmpp:message-moderate:0">`+
|
||||
`<retract xmlns="urn:xmpp:message-retract:0"/>`+
|
||||
`<reason>This content is inappropriate for this forum!</reason>`+
|
||||
`</moderate>`+
|
||||
`</apply-to>`+
|
||||
`</iq>`);
|
||||
|
||||
const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
|
||||
_converse.connection._dataRecv(test_utils.createRequest(result_iq));
|
||||
|
||||
// We opportunistically save the message as retracted, even before receiving the retraction message
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
|
||||
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
|
||||
const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message');
|
||||
expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has retracted this message from mallory');
|
||||
|
||||
const qel = msg_el.querySelector('q');
|
||||
expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');
|
||||
|
||||
// The server responds with a retraction message
|
||||
const retraction = u.toStanza(`
|
||||
<message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
|
||||
<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
|
||||
<moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retract xmlns='urn:xmpp:message-retract:0' />
|
||||
<reason>${reason}</reason>
|
||||
</moderated>
|
||||
</apply-to>
|
||||
</message>`);
|
||||
await view.model.onMessage(retraction);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
|
||||
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
|
||||
done();
|
||||
}));
|
||||
|
||||
|
||||
it("can be retracted by a moderator, with the retraction message received before the IQ response",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
const occupant = view.model.getOwnOccupant();
|
||||
expect(occupant.get('role')).toBe('moderator');
|
||||
|
||||
const received_stanza = u.toStanza(`
|
||||
<message to='${_converse.jid}' from='${muc_jid}/mallory' type='groupchat' id='${_converse.connection.getUniqueId()}'>
|
||||
<body>Visit this site to get free Bitcoin!</body>
|
||||
<stanza-id xmlns='urn:xmpp:sid:0' id='stanza-id-1' by='${muc_jid}'/>
|
||||
</message>
|
||||
`);
|
||||
await view.model.onMessage(received_stanza);
|
||||
await u.waitUntil(() => view.model.messages.length === 1);
|
||||
|
||||
const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract'));
|
||||
retract_button.click();
|
||||
await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal')));
|
||||
|
||||
const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]');
|
||||
const reason = "This content is inappropriate for this forum!"
|
||||
reason_input.value = reason;
|
||||
const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]');
|
||||
submit_button.click();
|
||||
|
||||
const sent_IQs = _converse.connection.IQ_stanzas;
|
||||
const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop());
|
||||
const message = view.model.messages.at(0);
|
||||
const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
|
||||
// The server responds with a retraction message
|
||||
const retraction = u.toStanza(`
|
||||
<message type="groupchat" id='retraction-id-1' from="${muc_jid}" to="${muc_jid}/romeo">
|
||||
<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
|
||||
<moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retract xmlns='urn:xmpp:message-retract:0' />
|
||||
<reason>${reason}</reason>
|
||||
</moderated>
|
||||
</apply-to>
|
||||
</message>`);
|
||||
await view.model.onMessage(retraction);
|
||||
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(msg_el.textContent).toBe('romeo has retracted this message from mallory');
|
||||
const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
|
||||
expect(qel.textContent).toBe('This content is inappropriate for this forum!');
|
||||
|
||||
const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'});
|
||||
_converse.connection._dataRecv(test_utils.createRequest(result_iq));
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
|
||||
expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid);
|
||||
expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe("A Sent Groupchat Message", function () {
|
||||
|
||||
it("can be retracted by its author",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
const occupant = view.model.getOwnOccupant();
|
||||
expect(occupant.get('role')).toBe('moderator');
|
||||
occupant.save('role', 'member');
|
||||
const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
|
||||
const msg_obj = view.model.messages.at(0);
|
||||
expect(Strophe.serialize(retraction_stanza)).toBe(
|
||||
`<message id="${retraction_stanza.getAttribute('id')}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">`+
|
||||
`<store xmlns="urn:xmpp:hints"/>`+
|
||||
`<apply-to id="${msg_obj.get('origin_id')}" xmlns="urn:xmpp:fasten:0">`+
|
||||
`<retract xmlns="urn:xmpp:message-retract:0"/>`+
|
||||
`</apply-to>`+
|
||||
`</message>`);
|
||||
|
||||
const message = view.model.messages.at(0);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_ephemeral')).toBe(false);
|
||||
|
||||
const stanza_id = message.get(`stanza_id ${muc_jid}`);
|
||||
// The server responds with a retraction message
|
||||
const reflection = u.toStanza(`
|
||||
<message type="groupchat" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${muc_jid}/romeo">
|
||||
<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
|
||||
<moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retract xmlns='urn:xmpp:message-retract:0' />
|
||||
</moderated>
|
||||
</apply-to>
|
||||
</message>`);
|
||||
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
_converse.connection._dataRecv(test_utils.createRequest(reflection));
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1);
|
||||
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent).toBe('romeo has retracted this message');
|
||||
done();
|
||||
}));
|
||||
|
||||
it("can be retracted by its author, causing an error message in response",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
const occupant = view.model.getOwnOccupant();
|
||||
expect(occupant.get('role')).toBe('moderator');
|
||||
occupant.save('role', 'member');
|
||||
const retraction_stanza = await sendAndThenRetractMessage(_converse, view);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent.trim()).toBe('romeo has retracted this message');
|
||||
|
||||
const message = view.model.messages.at(0);
|
||||
const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
|
||||
// The server responds with an error message
|
||||
const error = u.toStanza(`
|
||||
<message type="error" id="${retraction_stanza.getAttribute('id')}" from="${muc_jid}" to="${view.model.get('jid')}/romeo">
|
||||
<error by='${muc_jid}' type='auth'>
|
||||
<forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
</error>
|
||||
<apply-to id="${stanza_id}" xmlns="urn:xmpp:fasten:0">
|
||||
<moderated by='${_converse.bare_jid}' xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retract xmlns='urn:xmpp:message-retract:0' />
|
||||
</moderated>
|
||||
</apply-to>
|
||||
</message>`);
|
||||
|
||||
_converse.connection._dataRecv(test_utils.createRequest(error));
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1);
|
||||
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||
|
||||
expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
|
||||
const errmsg = view.el.querySelector('.chat-error');
|
||||
expect(errmsg.textContent).toBe("Sorry, something went wrong while trying to retract your message.");
|
||||
done();
|
||||
}));
|
||||
|
||||
it("can be retracted by its author, causing an timeout error in response",
|
||||
mock.initConverse(
|
||||
['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
_converse.STANZA_TIMEOUT = 1;
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.api.chatviews.get(muc_jid);
|
||||
const occupant = view.model.getOwnOccupant();
|
||||
expect(occupant.get('role')).toBe('moderator');
|
||||
occupant.save('role', 'member');
|
||||
await sendAndThenRetractMessage(_converse, view);
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1);
|
||||
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent.trim()).toBe('romeo has retracted this message');
|
||||
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1);
|
||||
|
||||
await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
expect(view.model.messages.at(0).get('retracted')).toBeFalsy();
|
||||
expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy();
|
||||
|
||||
const error_messages = view.el.querySelectorAll('.chat-error');
|
||||
expect(error_messages.length).toBe(2);
|
||||
expect(error_messages[0].textContent).toBe("Sorry, something went wrong while trying to retract your message.");
|
||||
expect(error_messages[1].textContent).toBe("Timeout Error: No response from server");
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe("when archived", function () {
|
||||
|
||||
it("may be returned as a tombstone message",
|
||||
mock.initConverse(
|
||||
['discoInitialized'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
await test_utils.waitForRoster(_converse, 'current', 1);
|
||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
await test_utils.openChatBoxFor(_converse, contact_jid);
|
||||
await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]);
|
||||
const sent_IQs = _converse.connection.IQ_stanzas;
|
||||
const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
|
||||
const queryid = stanza.querySelector('query').getAttribute('queryid');
|
||||
const view = _converse.chatboxviews.get(contact_jid);
|
||||
const first_id = u.getUniqueId();
|
||||
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
const first_message = u.toStanza(`
|
||||
<message id='${u.getUniqueId()}' to='${_converse.jid}'>
|
||||
<result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${first_id}">
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:01:15Z'/>
|
||||
<message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-0">
|
||||
<origin-id xmlns='urn:xmpp:sid:0' id="origin-id-0"/>
|
||||
<body>😊</body>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(first_message));
|
||||
|
||||
const tombstone = u.toStanza(`
|
||||
<message id='${u.getUniqueId()}' to='${_converse.jid}'>
|
||||
<result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${u.getUniqueId()}">
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
|
||||
<message type="chat" from="${contact_jid}" to="${_converse.bare_jid}" id="message-id-1">
|
||||
<origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
|
||||
<retracted stamp='2019-09-20T23:09:32Z' xmlns='urn:xmpp:message-retract:0'/>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(tombstone));
|
||||
|
||||
const last_id = u.getUniqueId();
|
||||
const retraction = u.toStanza(`
|
||||
<message id='${u.getUniqueId()}' to='${_converse.jid}'>
|
||||
<result xmlns='urn:xmpp:mam:2' queryid='${queryid}' id="${last_id}">
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<delay xmlns='urn:xmpp:delay' stamp='2019-09-20T23:08:25Z'/>
|
||||
<message from="${contact_jid}" to='${_converse.bare_jid}' id='retract-message-1'>
|
||||
<apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
|
||||
<retract xmlns='urn:xmpp:message-retract:0'/>
|
||||
</apply-to>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction));
|
||||
|
||||
const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
|
||||
.c('fin', {'xmlns': 'urn:xmpp:mam:2'})
|
||||
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
|
||||
.c('first', {'index': '0'}).t(first_id).up()
|
||||
.c('last').t(last_id).up()
|
||||
.c('count').t('2');
|
||||
_converse.connection._dataRecv(test_utils.createRequest(iq_result));
|
||||
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3);
|
||||
|
||||
expect(view.model.messages.length).toBe(2);
|
||||
const message = view.model.messages.at(1);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_tombstone')).toBe(true);
|
||||
expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
|
||||
expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(false);
|
||||
expect(view.model.handleRetraction.calls.all()[2].returnValue).toBe(true);
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(2);
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent.trim()).toBe('Mercutio has retracted this message');
|
||||
expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false);
|
||||
done();
|
||||
}));
|
||||
|
||||
it("may be returned as a tombstone groupchat message",
|
||||
mock.initConverse(
|
||||
['discoInitialized'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.chatboxviews.get(muc_jid);
|
||||
|
||||
const sent_IQs = _converse.connection.IQ_stanzas;
|
||||
const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
|
||||
const queryid = stanza.querySelector('query').getAttribute('queryid');
|
||||
|
||||
const first_id = u.getUniqueId();
|
||||
const tombstone = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
|
||||
<message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
|
||||
<origin-id xmlns='urn:xmpp:sid:0' id="origin-id-1"/>
|
||||
<retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0"/>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
spyOn(view.model, 'handleRetraction').and.callThrough();
|
||||
const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
|
||||
_converse.connection._dataRecv(test_utils.createRequest(tombstone));
|
||||
|
||||
const last_id = u.getUniqueId();
|
||||
const retraction = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
|
||||
<message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="retract-message-1">
|
||||
<apply-to id="origin-id-1" xmlns="urn:xmpp:fasten:0">
|
||||
<retract xmlns="urn:xmpp:message-retract:0"/>
|
||||
</apply-to>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction));
|
||||
|
||||
const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
|
||||
.c('fin', {'xmlns': 'urn:xmpp:mam:2'})
|
||||
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
|
||||
.c('first', {'index': '0'}).t(first_id).up()
|
||||
.c('last').t(last_id).up()
|
||||
.c('count').t('2');
|
||||
_converse.connection._dataRecv(test_utils.createRequest(iq_result));
|
||||
|
||||
await promise;
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
let message = view.model.messages.at(0);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_tombstone')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2);
|
||||
expect(view.model.handleRetraction.calls.first().returnValue).toBe(false);
|
||||
expect(view.model.handleRetraction.calls.all()[1].returnValue).toBe(true);
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
message = view.model.messages.at(0);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_tombstone')).toBe(true);
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent.trim()).toBe('eve has retracted this message');
|
||||
done();
|
||||
}));
|
||||
|
||||
it("may be returned as a tombstone moderated groupchat message",
|
||||
mock.initConverse(
|
||||
['discoInitialized', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
|
||||
const view = _converse.chatboxviews.get(muc_jid);
|
||||
|
||||
const sent_IQs = _converse.connection.IQ_stanzas;
|
||||
const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop());
|
||||
const queryid = stanza.querySelector('query').getAttribute('queryid');
|
||||
|
||||
const first_id = u.getUniqueId();
|
||||
const tombstone = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="stanza-id">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
|
||||
<message type="groupchat" from="${muc_jid}/eve" to="${_converse.bare_jid}" id="message-id-1">
|
||||
<moderated by="${muc_jid}/bob" stamp="2019-09-20T23:09:32Z" xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retracted xmlns="urn:xmpp:message-retract:0"/>
|
||||
<reason>This message contains inappropriate content</reason>
|
||||
</moderated>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
spyOn(view.model, 'handleModeration').and.callThrough();
|
||||
const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve));
|
||||
_converse.connection._dataRecv(test_utils.createRequest(tombstone));
|
||||
|
||||
const last_id = u.getUniqueId();
|
||||
const retraction = u.toStanza(`
|
||||
<message id="${u.getUniqueId()}" to="${_converse.jid}" from="${muc_jid}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="${queryid}" id="${last_id}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay xmlns="urn:xmpp:delay" stamp="2019-09-20T23:08:25Z"/>
|
||||
<message type="groupchat" from="${muc_jid}" to="${_converse.bare_jid}" id="retract-message-1">
|
||||
<apply-to id="stanza-id" xmlns="urn:xmpp:fasten:0">
|
||||
<moderated by="${muc_jid}/bob" xmlns='urn:xmpp:message-moderate:0'>
|
||||
<retract xmlns="urn:xmpp:message-retract:0"/>
|
||||
<reason>This message contains inappropriate content</reason>
|
||||
</moderated>
|
||||
</apply-to>
|
||||
</message>
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>
|
||||
`);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(retraction));
|
||||
|
||||
const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')})
|
||||
.c('fin', {'xmlns': 'urn:xmpp:mam:2'})
|
||||
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
|
||||
.c('first', {'index': '0'}).t(first_id).up()
|
||||
.c('last').t(last_id).up()
|
||||
.c('count').t('2');
|
||||
_converse.connection._dataRecv(test_utils.createRequest(iq_result));
|
||||
|
||||
await promise;
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
let message = view.model.messages.at(0);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_tombstone')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => view.model.handleModeration.calls.count() === 2);
|
||||
expect(view.model.handleModeration.calls.first().returnValue).toBe(false);
|
||||
expect(view.model.handleModeration.calls.all()[1].returnValue).toBe(true);
|
||||
|
||||
expect(view.model.messages.length).toBe(1);
|
||||
message = view.model.messages.at(0);
|
||||
expect(message.get('retracted')).toBeTruthy();
|
||||
expect(message.get('is_tombstone')).toBe(true);
|
||||
expect(message.get('moderation_reason')).toBe("This message contains inappropriate content");
|
||||
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
|
||||
|
||||
expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1);
|
||||
const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div');
|
||||
expect(el.textContent.trim()).toBe('A moderator has retracted this message from eve');
|
||||
const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q');
|
||||
expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
|
||||
done();
|
||||
}));
|
||||
});
|
||||
})
|
||||
}));
|
|
@ -65,6 +65,7 @@ converse.plugins.add('converse-chatview', {
|
|||
'auto_focus': true,
|
||||
'message_limit': 0,
|
||||
'show_send_button': false,
|
||||
'show_retraction_warning': true,
|
||||
'show_toolbar': true,
|
||||
'time_format': 'HH:mm',
|
||||
'visible_toolbar_buttons': {
|
||||
|
@ -226,6 +227,7 @@ converse.plugins.add('converse-chatview', {
|
|||
events: {
|
||||
'change input.fileupload': 'onFileSelection',
|
||||
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
|
||||
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
|
||||
'click .chatbox-navback': 'showControlBox',
|
||||
'click .close-chatbox-button': 'close',
|
||||
'click .new-msgs-indicator': 'viewUnreadMessages',
|
||||
|
@ -622,8 +624,8 @@ converse.plugins.add('converse-chatview', {
|
|||
return this.trigger('messageInserted', view.el);
|
||||
}
|
||||
}
|
||||
const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date(),
|
||||
previous_msg_date = this.getLastMessageDate(current_msg_date);
|
||||
const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date();
|
||||
const previous_msg_date = this.getLastMessageDate(current_msg_date);
|
||||
|
||||
if (previous_msg_date === null) {
|
||||
this.content.insertAdjacentElement('afterbegin', view.el);
|
||||
|
@ -649,9 +651,8 @@ converse.plugins.add('converse-chatview', {
|
|||
* followup message or not.
|
||||
*
|
||||
* Followup messages are subsequent ones written by the same
|
||||
* author with no other conversation elements inbetween and
|
||||
* posted within 10 minutes of one another.
|
||||
*
|
||||
* author with no other conversation elements in between and
|
||||
* which were posted within 10 minutes of one another.
|
||||
* @private
|
||||
* @method _converse.ChatBoxView#markFollowups
|
||||
* @param { HTMLElement } el - The message element
|
||||
|
@ -730,11 +731,9 @@ converse.plugins.add('converse-chatview', {
|
|||
// We already have a view for this message
|
||||
return;
|
||||
}
|
||||
if (!u.isNewMessage(message) && u.isEmptyMessage(message)) {
|
||||
// Ignore archived or delayed messages without any text to show.
|
||||
return message.destroy();
|
||||
if (!message.get('dangling_retraction')) {
|
||||
await this.showMessage(message);
|
||||
}
|
||||
await this.showMessage(message);
|
||||
/**
|
||||
* Triggered once a message has been added to a chatbox.
|
||||
* @event _converse#messageAdded
|
||||
|
@ -914,6 +913,45 @@ converse.plugins.add('converse-chatview', {
|
|||
this.insertIntoTextArea('', true, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retract one of your messages in this chat
|
||||
* @private
|
||||
* @method _converse.ChatBoxView#retractOwnMessage
|
||||
* @param { _converse.Message } message - The message which we're retracting.
|
||||
*/
|
||||
retractOwnMessage(message) {
|
||||
this.model.sendRetractionMessage(message);
|
||||
message.save({
|
||||
'retracted': (new Date()).toISOString(),
|
||||
'retracted_id': message.get('origin_id'),
|
||||
'is_ephemeral': true
|
||||
});
|
||||
},
|
||||
|
||||
async onMessageRetractButtonClicked (ev) {
|
||||
ev.preventDefault();
|
||||
const msg_el = u.ancestor(ev.target, '.message');
|
||||
const msgid = msg_el.getAttribute('data-msgid');
|
||||
const time = msg_el.getAttribute('data-isodate');
|
||||
const message = this.model.messages.findWhere({msgid, time});
|
||||
if (message.get('sender') !== 'me') {
|
||||
return log.error("onMessageEditButtonClicked called for someone else's 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.");
|
||||
|
||||
const messages = [__('Are you sure you want to retract this message?')];
|
||||
if (_converse.show_retraction_warning) {
|
||||
messages[1] = retraction_warning;
|
||||
}
|
||||
const result = await _converse.api.confirm(__('Confirm'), messages);
|
||||
if (result) {
|
||||
this.retractOwnMessage(message);
|
||||
}
|
||||
},
|
||||
|
||||
onMessageEditButtonClicked (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import tpl_message_versions_modal from "templates/message_versions_modal.html";
|
|||
import tpl_spinner from "templates/spinner.html";
|
||||
import xss from "xss/dist/xss";
|
||||
|
||||
const { dayjs } = converse.env;
|
||||
const { Strophe, dayjs } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
|
||||
|
@ -140,22 +140,20 @@ converse.plugins.add('converse-message-view', {
|
|||
} else {
|
||||
await this.renderChatMessage();
|
||||
}
|
||||
if (is_followup) {
|
||||
u.addClass('chat-msg--followup', this.el);
|
||||
}
|
||||
is_followup && u.addClass('chat-msg--followup', this.el);
|
||||
return this.el;
|
||||
},
|
||||
|
||||
async onChanged (item) {
|
||||
// Jot down whether it was edited because the `changed`
|
||||
// attr gets removed when this.render() gets called further
|
||||
// down.
|
||||
// attr gets removed when this.render() gets called further down.
|
||||
const edited = item.changed.edited;
|
||||
if (this.model.changed.progress) {
|
||||
return this.renderFileUploadProgresBar();
|
||||
}
|
||||
const isValidChange = prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop);
|
||||
if (['correcting', 'message', 'type', 'upload', 'received', 'editable'].filter(isValidChange).length) {
|
||||
const props = ['moderated', 'retracted', 'correcting', 'message', 'type', 'upload', 'received', 'editable'];
|
||||
if (props.filter(isValidChange).length) {
|
||||
await this.debouncedRender();
|
||||
}
|
||||
if (edited) {
|
||||
|
@ -243,19 +241,22 @@ converse.plugins.add('converse-message-view', {
|
|||
const time = dayjs(this.model.get('time'));
|
||||
const role = this.model.vcard ? this.model.vcard.get('role') : null;
|
||||
const roles = role ? role.split(',') : [];
|
||||
const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
|
||||
|
||||
const msg = u.stringToElement(tpl_message(
|
||||
Object.assign(
|
||||
this.model.toJSON(), {
|
||||
'__': __,
|
||||
'is_groupchat_message': this.model.get('type') === 'groupchat',
|
||||
'occupant': this.model.occupant,
|
||||
'is_me_message': this.model.isMeCommand(),
|
||||
'roles': roles,
|
||||
'pretty_time': time.format(_converse.time_format),
|
||||
'time': time.toISOString(),
|
||||
__,
|
||||
is_retracted,
|
||||
'extra_classes': this.getExtraMessageClasses(),
|
||||
'is_groupchat_message': this.model.get('type') === 'groupchat',
|
||||
'is_me_message': this.model.isMeCommand(),
|
||||
'label_show': __('Show more'),
|
||||
'occupant': this.model.occupant,
|
||||
'pretty_time': time.format(_converse.time_format),
|
||||
'retraction_text': is_retracted ? this.getRetractionText() : null,
|
||||
'roles': roles,
|
||||
'time': time.toISOString(),
|
||||
'username': this.model.getDisplayName()
|
||||
})
|
||||
));
|
||||
|
@ -265,11 +266,13 @@ converse.plugins.add('converse-message-view', {
|
|||
msg.querySelector('.chat-msg__media').innerHTML = this.transformOOBURL(url);
|
||||
}
|
||||
|
||||
const text = this.model.getMessageText();
|
||||
const msg_content = msg.querySelector('.chat-msg__text');
|
||||
if (text && text !== url) {
|
||||
msg_content.innerHTML = await this.transformBodyText(text);
|
||||
await u.renderImageURLs(_converse, msg_content);
|
||||
if (!is_retracted) {
|
||||
const text = this.model.getMessageText();
|
||||
const msg_content = msg.querySelector('.chat-msg__text');
|
||||
if (text && text !== url) {
|
||||
msg_content.innerHTML = await this.transformBodyText(text);
|
||||
await u.renderImageURLs(_converse, msg_content);
|
||||
}
|
||||
}
|
||||
if (this.model.get('type') !== 'headline') {
|
||||
this.renderAvatar(msg);
|
||||
|
@ -292,6 +295,25 @@ converse.plugins.add('converse-message-view', {
|
|||
return this.replaceElement(msg);
|
||||
},
|
||||
|
||||
getRetractionText () {
|
||||
const username = this.model.getDisplayName();
|
||||
let retraction_text = __('A message by %1$s has been retracted', username);
|
||||
if (this.model.get('type') === 'groupchat') {
|
||||
const retracted_by_mod = this.model.get('moderated_by');
|
||||
if (retracted_by_mod) {
|
||||
const chatbox = this.model.collection.chatbox;
|
||||
if (!this.model.mod) {
|
||||
this.model.mod =
|
||||
chatbox.occupants.findOccupant({'jid': retracted_by_mod}) ||
|
||||
chatbox.occupants.findOccupant({'nick': Strophe.getResourceFromJid(retracted_by_mod)});
|
||||
}
|
||||
const modname = this.model.mod ? this.model.mod.getDisplayName() : 'A moderator';
|
||||
retraction_text = __('%1$s has retracted this message from %2$s', modname , username);
|
||||
}
|
||||
}
|
||||
return retraction_text;
|
||||
},
|
||||
|
||||
renderErrorMessage () {
|
||||
const msg = u.stringToElement(
|
||||
tpl_info(Object.assign(this.model.toJSON(), {
|
||||
|
@ -304,8 +326,8 @@ converse.plugins.add('converse-message-view', {
|
|||
|
||||
renderChatStateNotification () {
|
||||
let text;
|
||||
const from = this.model.get('from'),
|
||||
name = this.model.getDisplayName();
|
||||
const from = this.model.get('from');
|
||||
const name = this.model.getDisplayName();
|
||||
|
||||
if (this.model.get('chat_state') === _converse.COMPOSING) {
|
||||
if (this.model.get('sender') === 'me') {
|
||||
|
@ -354,8 +376,10 @@ converse.plugins.add('converse-message-view', {
|
|||
},
|
||||
|
||||
getExtraMessageClasses () {
|
||||
let extra_classes = this.model.get('is_delayed') && 'delayed' || '';
|
||||
|
||||
const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
|
||||
const extra_classes = [
|
||||
...(this.model.get('is_delayed') ? ['delayed'] : []), ...(is_retracted ? ['chat-msg--retracted'] : [])
|
||||
];
|
||||
if (this.model.get('type') === 'groupchat') {
|
||||
if (this.model.occupant) {
|
||||
extra_classes += ` ${this.model.occupant.get('role') || ''} ${this.model.occupant.get('affiliation') || ''}`;
|
||||
|
|
|
@ -12,6 +12,7 @@ import converse from "@converse/headless/converse-core";
|
|||
import { isString } from "lodash";
|
||||
import tpl_alert from "templates/alert.html";
|
||||
import tpl_alert_modal from "templates/alert_modal.html";
|
||||
import tpl_prompt from "templates/prompt.html";
|
||||
|
||||
const { Backbone, sizzle } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
@ -21,6 +22,7 @@ converse.plugins.add('converse-modal', {
|
|||
|
||||
initialize () {
|
||||
const { _converse } = this;
|
||||
const { __ } = _converse;
|
||||
|
||||
_converse.BootstrapModal = Backbone.VDOMView.extend({
|
||||
|
||||
|
@ -79,18 +81,69 @@ converse.plugins.add('converse-modal', {
|
|||
}
|
||||
});
|
||||
|
||||
_converse.Alert = _converse.BootstrapModal.extend({
|
||||
_converse.Confirm = _converse.BootstrapModal.extend({
|
||||
events: {
|
||||
'submit .confirm': 'onConfimation'
|
||||
},
|
||||
|
||||
initialize () {
|
||||
this.confirmation = u.getResolveablePromise();
|
||||
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
|
||||
this.listenTo(this.model, 'change', this.render)
|
||||
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
|
||||
},
|
||||
|
||||
toHTML () {
|
||||
return tpl_prompt(Object.assign({__}, this.model.toJSON()));
|
||||
},
|
||||
|
||||
afterRender () {
|
||||
if (!this.close_handler_registered) {
|
||||
this.el.addEventListener('closed.bs.modal', () => {
|
||||
if (!this.confirmation.isResolved) {
|
||||
this.confirmation.reject()
|
||||
}
|
||||
}, false);
|
||||
this.close_handler_registered = true;
|
||||
}
|
||||
},
|
||||
|
||||
onConfimation (ev) {
|
||||
ev.preventDefault();
|
||||
this.confirmation.resolve(true);
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
_converse.Prompt = _converse.Confirm.extend({
|
||||
toHTML () {
|
||||
return tpl_prompt(Object.assign({__}, this.model.toJSON()));
|
||||
},
|
||||
|
||||
onConfimation (ev) {
|
||||
ev.preventDefault();
|
||||
const form_data = new FormData(ev.target);
|
||||
this.confirmation.resolve(form_data.get('reason'));
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
_converse.Alert = _converse.BootstrapModal.extend({
|
||||
initialize () {
|
||||
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
|
||||
this.listenTo(this.model, 'change', this.render)
|
||||
},
|
||||
|
||||
toHTML () {
|
||||
return tpl_alert_modal(this.model.toJSON());
|
||||
return tpl_alert_modal(
|
||||
Object.assign({__}, this.model.toJSON()));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/************************ BEGIN Event Listeners ************************/
|
||||
_converse.api.listen.on('afterTearDown', () => {
|
||||
if (!_converse.chatboxviews) {
|
||||
return;
|
||||
|
@ -104,40 +157,112 @@ converse.plugins.add('converse-modal', {
|
|||
|
||||
/************************ BEGIN API ************************/
|
||||
// We extend the default converse.js API to add methods specific to MUC chat rooms.
|
||||
let alert;
|
||||
let alert, prompt, confirm;
|
||||
|
||||
Object.assign(_converse.api, {
|
||||
|
||||
/**
|
||||
* Show a confirm modal to the user.
|
||||
* @method _converse.api.confirm
|
||||
* @param { String } title - The header text for the confirmation dialog
|
||||
* @param { (String[]|String) } messages - The text to show to the user
|
||||
* @returns { Promise } A promise which resolves with true or false
|
||||
*/
|
||||
async confirm (title, messages=[]) {
|
||||
if (isString(messages)) {
|
||||
messages = [messages];
|
||||
}
|
||||
if (confirm === undefined) {
|
||||
const model = new Backbone.Model({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'type': 'confirm'
|
||||
})
|
||||
confirm = new _converse.Confirm({model});
|
||||
} else {
|
||||
confirm.model.set({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'type': 'confirm'
|
||||
});
|
||||
}
|
||||
confirm.show();
|
||||
try {
|
||||
return await confirm.confirmation;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a prompt modal to the user.
|
||||
* @method _converse.api.prompt
|
||||
* @param { String } title - The header text for the prompt
|
||||
* @param { (String[]|String) } messages - The prompt text to show to the user
|
||||
* @param { String } placeholder - The placeholder text for the prompt input
|
||||
* @returns { Promise } A promise which resolves with the text provided by the
|
||||
* user or `false` if the user canceled the prompt.
|
||||
*/
|
||||
async prompt (title, messages=[], placeholder='') {
|
||||
if (isString(messages)) {
|
||||
messages = [messages];
|
||||
}
|
||||
if (prompt === undefined) {
|
||||
const model = new Backbone.Model({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'placeholder': placeholder,
|
||||
'type': 'prompt'
|
||||
})
|
||||
prompt = new _converse.Prompt({model});
|
||||
} else {
|
||||
prompt.model.set({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'type': 'prompt'
|
||||
});
|
||||
}
|
||||
prompt.show();
|
||||
try {
|
||||
return await prompt.confirmation;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an alert modal to the user.
|
||||
* @method _converse.api.alert
|
||||
* @param { ('info'|'warn'|'error') } type - The type of alert.
|
||||
* @returns { String } title - The header text for the alert.
|
||||
* @returns { (String[]|String) } messages - The alert text to show to the user.
|
||||
* @param { String } title - The header text for the alert.
|
||||
* @param { (String[]|String) } messages - The alert text to show to the user.
|
||||
*/
|
||||
alert (type, title, messages) {
|
||||
if (isString(messages)) {
|
||||
messages = [messages];
|
||||
}
|
||||
let level;
|
||||
if (type === 'error') {
|
||||
type = 'alert-danger';
|
||||
level = 'alert-danger';
|
||||
} else if (type === 'info') {
|
||||
type = 'alert-info';
|
||||
level = 'alert-info';
|
||||
} else if (type === 'warn') {
|
||||
type = 'alert-warning';
|
||||
level = 'alert-warning';
|
||||
}
|
||||
|
||||
if (alert === undefined) {
|
||||
const model = new Backbone.Model({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'type': type
|
||||
'level': level,
|
||||
'type': 'alert'
|
||||
})
|
||||
alert = new _converse.Alert({'model': model});
|
||||
alert = new _converse.Alert({model});
|
||||
} else {
|
||||
alert.model.set({
|
||||
'title': title,
|
||||
'messages': messages,
|
||||
'type': type
|
||||
'level': level
|
||||
});
|
||||
}
|
||||
alert.show();
|
||||
|
|
|
@ -41,7 +41,6 @@ import tpl_rooms_results from "templates/rooms_results.html";
|
|||
import tpl_spinner from "templates/spinner.html";
|
||||
import xss from "xss/dist/xss";
|
||||
|
||||
|
||||
const { Backbone, Strophe, sizzle, _, $iq, $pres } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
|
@ -108,6 +107,7 @@ converse.plugins.add('converse-muc-views', {
|
|||
'auto_list_rooms': false,
|
||||
'cache_muc_messages': true,
|
||||
'locked_muc_nickname': false,
|
||||
'show_retraction_warning': true,
|
||||
'muc_disable_slash_commands': false,
|
||||
'muc_show_join_leave': true,
|
||||
'muc_show_join_leave_status': true,
|
||||
|
@ -630,6 +630,7 @@ converse.plugins.add('converse-muc-views', {
|
|||
events: {
|
||||
'change input.fileupload': 'onFileSelection',
|
||||
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
|
||||
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
|
||||
'click .chatbox-navback': 'showControlBox',
|
||||
'click .close-chatbox-button': 'close',
|
||||
'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
|
||||
|
@ -724,8 +725,7 @@ converse.plugins.add('converse-muc-views', {
|
|||
},
|
||||
|
||||
renderChatArea () {
|
||||
/* Render the UI container in which groupchat messages will appear.
|
||||
*/
|
||||
// Render the UI container in which groupchat messages will appear.
|
||||
if (this.el.querySelector('.chat-area') === null) {
|
||||
const container_el = this.el.querySelector('.chatroom-body');
|
||||
container_el.insertAdjacentHTML(
|
||||
|
@ -811,6 +811,101 @@ converse.plugins.add('converse-muc-views', {
|
|||
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
|
||||
},
|
||||
|
||||
async onMessageRetractButtonClicked (ev) {
|
||||
ev.preventDefault();
|
||||
const msg_el = u.ancestor(ev.target, '.message');
|
||||
const msgid = msg_el.getAttribute('data-msgid');
|
||||
const time = msg_el.getAttribute('data-isodate');
|
||||
const message = this.model.messages.findWhere({msgid, time});
|
||||
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 (message.get('sender') === 'me') {
|
||||
const messages = [__('Are you sure you want to retract this message?')];
|
||||
if (_converse.show_retraction_warning) {
|
||||
messages[1] = retraction_warning;
|
||||
}
|
||||
const result = await _converse.api.confirm(__('Confirm'), messages);
|
||||
if (result) {
|
||||
this.retractOwnMessage(message);
|
||||
}
|
||||
} else {
|
||||
let messages = [
|
||||
__('You are about to retract this message.'),
|
||||
__('You may optionally include a message, explaining the reason for the retraction.')
|
||||
];
|
||||
if (_converse.show_retraction_warning) {
|
||||
messages = [messages[0], retraction_warning, messages[1]]
|
||||
}
|
||||
const reason = await _converse.api.prompt(
|
||||
__('Message Retraction'),
|
||||
messages,
|
||||
__('Optional reason')
|
||||
);
|
||||
if (reason !== false) {
|
||||
this.retractOtherMessage(message, reason);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Retract one of your messages in this groupchat.
|
||||
* @private
|
||||
* @method _converse.ChatRoomView#retractOwnMessage
|
||||
* @param { _converse.Message } message - The message which we're retracting.
|
||||
*/
|
||||
retractOwnMessage(message) {
|
||||
this.model.sendRetractionMessage(message)
|
||||
.catch(e => {
|
||||
message.save({
|
||||
'retracted': undefined,
|
||||
'retracted_id': undefined
|
||||
});
|
||||
const errmsg = __('Sorry, something went wrong while trying to retract your message.');
|
||||
if (u.isErrorStanza(e)) {
|
||||
this.showErrorMessage(errmsg);
|
||||
} else {
|
||||
this.showErrorMessage(errmsg);
|
||||
this.showErrorMessage(e.message);
|
||||
}
|
||||
log.error(e);
|
||||
});
|
||||
message.save({
|
||||
'retracted': (new Date()).toISOString(),
|
||||
'retracted_id': message.get('origin_id')
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.sendRetractionIQ(message, reason);
|
||||
if (result === null) {
|
||||
const err_msg = __(`A timeout occurred while trying to retract the message`);
|
||||
_converse.api.alert('error', __('Error'), err_msg);
|
||||
_converse.log(err_msg, Strophe.LogLevel.WARN);
|
||||
} else if (u.isErrorStanza(result)) {
|
||||
const err_msg = __(`Sorry, you're not allowed to retract this message.`);
|
||||
_converse.api.alert('error', __('Error'), err_msg);
|
||||
_converse.log(err_msg, Strophe.LogLevel.WARN);
|
||||
_converse.log(result, Strophe.LogLevel.WARN);
|
||||
} else {
|
||||
message.save({
|
||||
'moderated': 'retracted',
|
||||
'moderated_by': _converse.bare_jid,
|
||||
'moderated_id': message.get('msgid'),
|
||||
'moderation_reason': reason
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showModeratorToolsModal (affiliation) {
|
||||
if (!this.verifyRoles(['moderator'])) {
|
||||
return;
|
||||
|
@ -2193,7 +2288,7 @@ converse.plugins.add('converse-muc-views', {
|
|||
* @namespace _converse.api.roomviews
|
||||
* @memberOf _converse.api
|
||||
*/
|
||||
'roomviews': {
|
||||
roomviews: {
|
||||
/**
|
||||
* Retrieves a groupchat (aka chatroom) view. The chat should already be open.
|
||||
*
|
||||
|
|
|
@ -121,6 +121,7 @@ converse.plugins.add('converse-notification', {
|
|||
|
||||
_converse.areDesktopNotificationsEnabled = function () {
|
||||
return _converse.supports_html5_notification &&
|
||||
|
||||
_converse.show_desktop_notifications &&
|
||||
Notification.permission === "granted";
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import "./utils/stanza";
|
||||
import { get, isObject, isString, propertyOf } from "lodash";
|
||||
import { get, isObject, isString, pick } from "lodash";
|
||||
import converse from "./converse-core";
|
||||
import filesize from "filesize";
|
||||
import log from "./log";
|
||||
import stanza_utils from "./utils/stanza";
|
||||
|
||||
const { $msg, Backbone, Strophe, dayjs, sizzle, utils } = converse.env;
|
||||
const { $msg, Backbone, Strophe, sizzle, utils } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ converse.plugins.add('converse-chat', {
|
|||
*
|
||||
* NB: These plugins need to have already been loaded via require.js.
|
||||
*/
|
||||
dependencies: ["stanza-utils", "converse-chatboxes", "converse-disco"],
|
||||
dependencies: ["converse-chatboxes", "converse-disco"],
|
||||
|
||||
initialize () {
|
||||
/* The initialize function gets called as soon as the plugin is
|
||||
|
@ -29,7 +29,6 @@ converse.plugins.add('converse-chat', {
|
|||
*/
|
||||
const { _converse } = this;
|
||||
const { __ } = _converse;
|
||||
const { stanza_utils } = _converse;
|
||||
|
||||
// Configuration values for this plugin
|
||||
// ====================================
|
||||
|
@ -75,7 +74,7 @@ converse.plugins.add('converse-chat', {
|
|||
return {
|
||||
'msgid': u.getUniqueId(),
|
||||
'time': (new Date()).toISOString(),
|
||||
'ephemeral': false
|
||||
'is_ephemeral': false
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -86,17 +85,36 @@ converse.plugins.add('converse-chat', {
|
|||
ModelWithContact.prototype.initialize.apply(this, arguments);
|
||||
this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
|
||||
}
|
||||
|
||||
if (this.get('file')) {
|
||||
this.on('change:put', this.uploadFile, this);
|
||||
}
|
||||
if (this.isEphemeral()) {
|
||||
window.setTimeout(this.safeDestroy.bind(this), 10000);
|
||||
}
|
||||
this.setTimerForEphemeralMessage();
|
||||
await _converse.api.trigger('messageInitialized', this, {'Synchronous': true});
|
||||
this.initialized.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets an auto-destruct timer for this message, if it's is_ephemeral.
|
||||
* @private
|
||||
* @method _converse.Message#setTimerForEphemeralMessage
|
||||
* @returns { Boolean } - Indicates whether the message is
|
||||
* ephemeral or not, and therefore whether the timer was set or not.
|
||||
*/
|
||||
setTimerForEphemeralMessage () {
|
||||
const setTimer = () => {
|
||||
this.ephemeral_timer = window.setTimeout(this.safeDestroy.bind(this), 10000);
|
||||
}
|
||||
if (this.isEphemeral()) {
|
||||
setTimer();
|
||||
return true;
|
||||
} else {
|
||||
this.on('change:is_ephemeral',
|
||||
() => this.isEphemeral() ? setTimer() : clearTimeout(this.ephemeral_timer)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
safeDestroy () {
|
||||
try {
|
||||
this.destroy()
|
||||
|
@ -110,7 +128,7 @@ converse.plugins.add('converse-chat', {
|
|||
},
|
||||
|
||||
isEphemeral () {
|
||||
return this.isOnlyChatStateNotification() || this.get('ephemeral');
|
||||
return this.get('is_ephemeral') || u.isOnlyChatStateNotification(this);
|
||||
},
|
||||
|
||||
getDisplayName () {
|
||||
|
@ -171,7 +189,7 @@ converse.plugins.add('converse-chat', {
|
|||
return this.save({
|
||||
'type': 'error',
|
||||
'message': __("Sorry, could not determine upload URL."),
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
}
|
||||
const slot = stanza.querySelector('slot');
|
||||
|
@ -184,7 +202,7 @@ converse.plugins.add('converse-chat', {
|
|||
return this.save({
|
||||
'type': 'error',
|
||||
'message': __("Sorry, could not determine file upload URL."),
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -223,7 +241,7 @@ converse.plugins.add('converse-chat', {
|
|||
'type': 'error',
|
||||
'upload': _converse.FAILURE,
|
||||
'message': message,
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
};
|
||||
xhr.open('PUT', this.get('put'), true);
|
||||
|
@ -338,17 +356,21 @@ converse.plugins.add('converse-chat', {
|
|||
const message = await this.getDuplicateMessage(stanza);
|
||||
if (message) {
|
||||
this.updateMessage(message, original_stanza);
|
||||
} else {
|
||||
if (
|
||||
!this.handleReceipt (stanza, from_jid) &&
|
||||
!this.handleChatMarker(stanza, from_jid)
|
||||
} else if (
|
||||
!this.handleReceipt (stanza, from_jid) &&
|
||||
!this.handleChatMarker(stanza, from_jid)
|
||||
) {
|
||||
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
|
||||
if (this.handleRetraction(attrs)) {
|
||||
return;
|
||||
}
|
||||
this.setEditable(attrs, attrs.time, stanza);
|
||||
if (attrs['chat_state'] ||
|
||||
attrs['retracted'] || // Retraction received *before* the message
|
||||
!u.isEmptyMessage(attrs)
|
||||
) {
|
||||
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
|
||||
this.setEditable(attrs, attrs.time, stanza);
|
||||
if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
|
||||
const msg = this.correctMessage(attrs) || this.messages.create(attrs);
|
||||
this.incrementUnreadMsgCounter(msg);
|
||||
}
|
||||
const msg = this.handleCorrection(attrs) || this.messages.create(attrs);
|
||||
this.incrementUnreadMsgCounter(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -517,28 +539,90 @@ converse.plugins.add('converse-chat', {
|
|||
return true;
|
||||
},
|
||||
|
||||
retractMessage (attrs) {
|
||||
if (!attrs.moderated !== 'retracted' && !attrs.retracted) {
|
||||
return;
|
||||
isSameUser (jid1, jid2) {
|
||||
return u.isSameBareJID(jid1, jid2);
|
||||
},
|
||||
|
||||
/**
|
||||
* Looks whether we already have a retraction for this
|
||||
* incoming message. If so, it's considered "dangling" because it
|
||||
* probably hasn't been applied to anything yet, given that the
|
||||
* relevant message is only coming in now.
|
||||
* @private
|
||||
* @method _converse.ChatBox#findDanglingRetraction
|
||||
* @param { object } attrs - Attributes representing a received
|
||||
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
|
||||
* @returns { _converse.Message }
|
||||
*/
|
||||
findDanglingRetraction (attrs) {
|
||||
if (!attrs.origin_id || !this.messages.length) {
|
||||
return null;
|
||||
}
|
||||
const message = this.messages.findWhere({'msgid': attrs.replaced_id, 'from': attrs.from});
|
||||
if (!message) {
|
||||
return;
|
||||
// Only look for dangling retractions if there are newer
|
||||
// messages than this one, since retractions come after.
|
||||
if (this.messages.last().get('time') > attrs.time) {
|
||||
// Search from latest backwards
|
||||
const messages = Array.from(this.messages.models);
|
||||
messages.reverse();
|
||||
return messages.find(
|
||||
({attributes}) =>
|
||||
attributes.retracted_id === attrs.origin_id &&
|
||||
attributes.from === attrs.from &&
|
||||
!attributes.moderated_by
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine whether the passed in message attributes represent a
|
||||
* Handles message retraction based on the passed in attributes.
|
||||
* @private
|
||||
* @method _converse.ChatBox#handleRetraction
|
||||
* @param { object } attrs - Attributes representing a received
|
||||
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
|
||||
* @returns { Boolean } Returns `true` or `false` depending on
|
||||
* whether a message was retracted or not.
|
||||
*/
|
||||
handleRetraction (attrs) {
|
||||
const RETRACTION_ATTRIBUTES = ['retracted', 'retracted_id'];
|
||||
if (attrs.retracted) {
|
||||
if (attrs.is_tombstone) {
|
||||
return false;
|
||||
}
|
||||
const message = this.messages.findWhere({'origin_id': attrs.retracted_id, 'from': attrs.from});
|
||||
if (!message) {
|
||||
attrs['dangling_retraction'] = true;
|
||||
this.messages.create(attrs);
|
||||
return true;
|
||||
}
|
||||
message.save(pick(attrs, RETRACTION_ATTRIBUTES));
|
||||
return true;
|
||||
} else {
|
||||
// Check if we have dangling retraction
|
||||
const message = this.findDanglingRetraction(attrs);
|
||||
if (message) {
|
||||
const retraction_attrs = pick(message.attributes, RETRACTION_ATTRIBUTES);
|
||||
const new_attrs = Object.assign({'dangling_retraction': false}, attrs, retraction_attrs);
|
||||
delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created
|
||||
message.save(new_attrs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines whether the passed in message attributes represent a
|
||||
* message which corrects a previously received message, or an
|
||||
* older message which has already been corrected.
|
||||
* In both cases, update the corrected message accordingly.
|
||||
* @private
|
||||
* @method _converse.ChatBox#correctMessage
|
||||
* @method _converse.ChatBox#handleCorrection
|
||||
* @param { object } attrs - Attributes representing a received
|
||||
* message, as returned by
|
||||
* {@link _converse.ChatBox.getMessageAttributesFromStanza}
|
||||
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
|
||||
* @returns { _converse.Message|undefined } Returns the corrected
|
||||
* message or `undefined` if not applicable.
|
||||
*/
|
||||
correctMessage (attrs) {
|
||||
handleCorrection (attrs) {
|
||||
if (!attrs.replaced_id || !attrs.from) {
|
||||
return;
|
||||
}
|
||||
|
@ -604,6 +688,30 @@ converse.plugins.add('converse-chat', {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message stanza to retract a message in this chat
|
||||
* @private
|
||||
* @method _converse.ChatBox#sendRetractionMessage
|
||||
* @param { _converse.Message } message - The message which we're retracting.
|
||||
*/
|
||||
sendRetractionMessage (message) {
|
||||
const origin_id = message.get('origin_id');
|
||||
if (!origin_id) {
|
||||
throw new Error("Can't retract message without a XEP-0359 Origin ID");
|
||||
}
|
||||
const msg = $msg({
|
||||
'id': u.getUniqueId(),
|
||||
'to': this.get('jid'),
|
||||
'type': "chat"
|
||||
})
|
||||
.c('store', {xmlns: Strophe.NS.HINTS}).up()
|
||||
.c("apply-to", {
|
||||
'id': origin_id,
|
||||
'xmlns': Strophe.NS.FASTEN
|
||||
}).c('retract', {xmlns: Strophe.NS.RETRACT})
|
||||
return _converse.connection.send(msg);
|
||||
},
|
||||
|
||||
sendMarker(to_jid, id, type) {
|
||||
const stanza = $msg({
|
||||
'from': _converse.connection.jid,
|
||||
|
@ -849,7 +957,7 @@ converse.plugins.add('converse-chat', {
|
|||
this.messages.create({
|
||||
'message': __("Sorry, looks like file upload is not supported by your server."),
|
||||
'type': 'error',
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -861,7 +969,7 @@ converse.plugins.add('converse-chat', {
|
|||
this.messages.create({
|
||||
'message': __("Sorry, looks like file upload is not supported by your server."),
|
||||
'type': 'error',
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -871,7 +979,7 @@ converse.plugins.add('converse-chat', {
|
|||
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
|
||||
file.name, filesize(max_file_size)),
|
||||
'type': 'error',
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
});
|
||||
} else {
|
||||
const attrs = Object.assign(
|
||||
|
@ -890,46 +998,19 @@ converse.plugins.add('converse-chat', {
|
|||
},
|
||||
|
||||
/**
|
||||
* Parses a passed in message stanza and returns an object
|
||||
* of attributes.
|
||||
* Parses a passed in message stanza and returns an object of attributes.
|
||||
* @private
|
||||
* @method _converse.ChatBox#getMessageAttributesFromStanza
|
||||
* @param { XMLElement } stanza - The message stanza
|
||||
* @param { XMLElement } delay - The <delay> node from the stanza, if there was one.
|
||||
* @param { XMLElement } original_stanza - The original stanza, that contains the
|
||||
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
||||
* @returns { Object }
|
||||
*/
|
||||
async getMessageAttributesFromStanza (stanza, original_stanza) {
|
||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||
const text = stanza_utils.getMessageBody(stanza) || undefined;
|
||||
const chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
|
||||
stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
|
||||
stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
|
||||
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
|
||||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
'chat_state': chat_state,
|
||||
'is_archived': stanza_utils.isArchived(original_stanza),
|
||||
'is_delayed': !!delay,
|
||||
'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
|
||||
'message': text,
|
||||
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
||||
'references': stanza_utils.getReferences(stanza),
|
||||
'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
|
||||
'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
|
||||
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
|
||||
'type': stanza.getAttribute('type')
|
||||
},
|
||||
stanza_utils.getStanzaIDs(original_stanza),
|
||||
stanza_utils.getSenderAttributes(stanza, this),
|
||||
stanza_utils.getOutOfBandAttributes(stanza),
|
||||
stanza_utils.getMessageFasteningAttributes(stanza),
|
||||
stanza_utils.getSpoilerAttributes(stanza),
|
||||
stanza_utils.getCorrectionAttributes(stanza, original_stanza)
|
||||
);
|
||||
getMessageAttributesFromStanza (stanza, original_stanza) {
|
||||
// XXX: Eventually we want to get rid of this pass-through
|
||||
// method but currently we still need it because converse-omemo
|
||||
// overrides it.
|
||||
return stanza_utils.getMessageAttributesFromStanza(stanza, original_stanza, this, _converse);
|
||||
},
|
||||
|
||||
maybeShow () {
|
||||
|
|
|
@ -37,16 +37,19 @@ Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
|
|||
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
|
||||
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
|
||||
Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
|
||||
Strophe.addNamespace('FASTEN', 'urn:xmpp:fasten:0');
|
||||
Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0');
|
||||
Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
|
||||
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
|
||||
Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1');
|
||||
Strophe.addNamespace('MAM', 'urn:xmpp:mam:2');
|
||||
Strophe.addNamespace('MODERATE', 'urn:xmpp:message-moderate:0');
|
||||
Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
|
||||
Strophe.addNamespace('OMEMO', 'eu.siacs.conversations.axolotl');
|
||||
Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
|
||||
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
|
||||
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
|
||||
Strophe.addNamespace('RETRACT', 'urn:xmpp:message-retract:0');
|
||||
Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
|
||||
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
|
||||
Strophe.addNamespace('SID', 'urn:xmpp:sid:0');
|
||||
|
@ -92,8 +95,7 @@ const CORE_PLUGINS = [
|
|||
'converse-rsm',
|
||||
'converse-smacks',
|
||||
'converse-status',
|
||||
'converse-vcard',
|
||||
'stanza-utils'
|
||||
'converse-vcard'
|
||||
];
|
||||
|
||||
|
||||
|
@ -103,7 +105,7 @@ const CORE_PLUGINS = [
|
|||
* @global
|
||||
* @namespace _converse
|
||||
*/
|
||||
// XXX: Strictly speaking _converse is not a global, but we need to set it as
|
||||
// Strictly speaking _converse is not a global, but we need to set it as
|
||||
// such to get JSDoc to create the correct document site strucure.
|
||||
const _converse = {
|
||||
'templates': {},
|
||||
|
@ -142,6 +144,10 @@ class TimeoutError extends Error {}
|
|||
_converse.TimeoutError = TimeoutError;
|
||||
|
||||
|
||||
class IllegalMessage extends Error {}
|
||||
_converse.IllegalMessage = IllegalMessage;
|
||||
|
||||
|
||||
// Make converse pluggable
|
||||
pluggable.enable(_converse, '_converse', 'pluggable');
|
||||
|
||||
|
@ -187,7 +193,7 @@ _converse.LOGOUT = 'logout';
|
|||
_converse.OPENED = 'opened';
|
||||
_converse.PREBIND = 'prebind';
|
||||
|
||||
_converse.IQ_TIMEOUT = 20000;
|
||||
_converse.STANZA_TIMEOUT = 10000;
|
||||
|
||||
_converse.CONNECTION_STATUS = {
|
||||
0: 'ERROR',
|
||||
|
@ -1694,7 +1700,7 @@ _converse.api = {
|
|||
* or is rejected when we receive an `error` stanza.
|
||||
*/
|
||||
sendIQ (stanza, timeout, reject=true) {
|
||||
timeout = timeout || _converse.IQ_TIMEOUT;
|
||||
timeout = timeout || _converse.STANZA_TIMEOUT;
|
||||
let promise;
|
||||
if (reject) {
|
||||
promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout));
|
||||
|
|
|
@ -55,7 +55,6 @@ converse.plugins.add('converse-mam', {
|
|||
});
|
||||
|
||||
const MAMEnabledChat = {
|
||||
|
||||
/**
|
||||
* Fetches messages that might have been archived *after*
|
||||
* the last archived message in our local cache.
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
*/
|
||||
import "./converse-disco";
|
||||
import "./converse-emoji";
|
||||
import { clone, get, intersection, invoke, isElement, isObject, isString, uniq, zipObject } from "lodash";
|
||||
import { clone, get, intersection, invoke, isElement, isObject, isString, pick, uniq, zipObject } from "lodash";
|
||||
import converse from "./converse-core";
|
||||
import log from "./log";
|
||||
import muc_utils from "./utils/muc";
|
||||
|
@ -252,9 +252,7 @@ converse.plugins.add('converse-muc', {
|
|||
if (this.get('file')) {
|
||||
this.on('change:put', this.uploadFile, this);
|
||||
}
|
||||
if (this.isEphemeral()) {
|
||||
window.setTimeout(this.safeDestroy.bind(this), 10000);
|
||||
} else {
|
||||
if (!this.setTimerForEphemeralMessage()) {
|
||||
this.setOccupant();
|
||||
this.setVCard();
|
||||
}
|
||||
|
@ -510,9 +508,8 @@ converse.plugins.add('converse-muc', {
|
|||
},
|
||||
|
||||
removeHandlers () {
|
||||
/* Remove the presence and message handlers that were
|
||||
* registered for this groupchat.
|
||||
*/
|
||||
// Remove the presence and message handlers that were
|
||||
// registered for this groupchat.
|
||||
if (this.message_handler) {
|
||||
if (_converse.connection) {
|
||||
_converse.connection.deleteHandler(this.message_handler);
|
||||
|
@ -571,15 +568,96 @@ converse.plugins.add('converse-muc', {
|
|||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message stanza to the XMPP server and expects a reflection
|
||||
* or error message within a specific timeout period.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#sendTimedMessage
|
||||
* @param { _converse.Message|XMLElement } message
|
||||
* @returns { Promise<XMLElement>|Promise<_converse.TimeoutError> } Returns a promise
|
||||
* which resolves with the reflected message stanza or rejects
|
||||
* with an error stanza or with a {@link _converse.TimeoutError}.
|
||||
*/
|
||||
sendTimedMessage (el) {
|
||||
if (typeof(el.tree) === "function") {
|
||||
el = el.tree();
|
||||
}
|
||||
let id = el.getAttribute('id');
|
||||
if (!id) { // inject id if not found
|
||||
id = this.getUniqueId("sendIQ");
|
||||
el.setAttribute("id", id);
|
||||
}
|
||||
const promise = u.getResolveablePromise();
|
||||
const timeoutHandler = _converse.connection.addTimedHandler(
|
||||
_converse.STANZA_TIMEOUT,
|
||||
() => {
|
||||
_converse.connection.deleteHandler(handler);
|
||||
promise.reject(new _converse.TimeoutError("Timeout Error: No response from server"));
|
||||
return false;
|
||||
}
|
||||
);
|
||||
const handler = _converse.connection.addHandler(stanza => {
|
||||
timeoutHandler && _converse.connection.deleteTimedHandler(timeoutHandler);
|
||||
if (stanza.getAttribute('type') === 'groupchat') {
|
||||
promise.resolve(stanza);
|
||||
} else {
|
||||
promise.reject(stanza);
|
||||
}
|
||||
}, null, 'message', ['error', 'groupchat'], id);
|
||||
_converse.api.send(el)
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message stanza to retract a message in this groupchat.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#sendRetractionMessage
|
||||
* @param { _converse.Message } message - The message which we're retracting.
|
||||
*/
|
||||
sendRetractionMessage (message) {
|
||||
const origin_id = message.get('origin_id');
|
||||
if (!origin_id) {
|
||||
throw new Error("Can't retract message without a XEP-0359 Origin ID");
|
||||
}
|
||||
const msg = $msg({
|
||||
'id': u.getUniqueId(),
|
||||
'to': this.get('jid'),
|
||||
'type': "groupchat"
|
||||
})
|
||||
.c('store', {xmlns: Strophe.NS.HINTS}).up()
|
||||
.c("apply-to", {
|
||||
'id': origin_id,
|
||||
'xmlns': Strophe.NS.FASTEN
|
||||
}).c('retract', {xmlns: Strophe.NS.RETRACT});
|
||||
return this.sendTimedMessage(msg);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an IQ stanza to the XMPP server to retract a message in this groupchat.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#sendRetractionIQ
|
||||
* @param { _converse.Message } message - The message which we're retracting.
|
||||
* @param { string } [reason] - The reason for retracting the message.
|
||||
*/
|
||||
sendRetractionIQ (message, reason) {
|
||||
const iq = $iq({'to': this.get('jid'), 'type': "set"})
|
||||
.c("apply-to", {
|
||||
'id': message.get(`stanza_id ${this.get('jid')}`),
|
||||
'xmlns': Strophe.NS.FASTEN
|
||||
}).c('moderate', {xmlns: Strophe.NS.MODERATE})
|
||||
.c('retract', {xmlns: Strophe.NS.RETRACT}).up()
|
||||
.c('reason').t(reason);
|
||||
return _converse.api.sendIQ(iq, null, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
|
||||
* to be confused with the {@link _converse.ChatRoom#destroy}
|
||||
* method, which simply removes the room from the local browser storage cache.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#sendDestroyIQ
|
||||
* @param { string } [reason] - The reason for destroying the groupchat
|
||||
* @param { string } [new_jid] - The JID of the new groupchat which
|
||||
* replaces this one.
|
||||
* @param { string } [reason] - The reason for destroying the groupchat.
|
||||
* @param { string } [new_jid] - The JID of the new groupchat which replaces this one.
|
||||
*/
|
||||
sendDestroyIQ (reason, new_jid) {
|
||||
const destroy = $build("destroy");
|
||||
|
@ -1320,6 +1398,38 @@ converse.plugins.add('converse-muc', {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given two JIDs, which can be either user JIDs or MUC occupant JIDs,
|
||||
* determine whether they belong to the same user.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#isSameUser
|
||||
* @param { String } jid1
|
||||
* @param { String } jid2
|
||||
* @returns { Boolean }
|
||||
*/
|
||||
isSameUser (jid1, jid2) {
|
||||
const bare_jid1 = Strophe.getBareJidFromJid(jid1);
|
||||
const bare_jid2 = Strophe.getBareJidFromJid(jid2);
|
||||
const resource1 = Strophe.getResourceFromJid(jid1);
|
||||
const resource2 = Strophe.getResourceFromJid(jid2);
|
||||
if (u.isSameBareJID(jid1, jid2)) {
|
||||
if (bare_jid1 === this.get('jid')) {
|
||||
// MUC JIDs
|
||||
return resource1 === resource2;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
const occupant1 = (bare_jid1 === this.get('jid')) ?
|
||||
this.occupants.findOccupant({'nick': resource1}) :
|
||||
this.occupants.findOccupant({'jid': bare_jid1});
|
||||
|
||||
const occupant2 = (bare_jid2 === this.get('jid')) ?
|
||||
this.occupants.findOccupant({'nick': resource2}) :
|
||||
this.occupants.findOccupant({'jid': bare_jid2});
|
||||
return occupant1 === occupant2;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a subject change and return `true` if so.
|
||||
|
@ -1460,14 +1570,89 @@ converse.plugins.add('converse-muc', {
|
|||
return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, stanza);
|
||||
},
|
||||
|
||||
getErrorMessage (stanza) {
|
||||
if (sizzle(`forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
||||
return __("Your message was not delivered because you're not allowed to send messages in this groupchat.");
|
||||
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
||||
return __("Your message was not delivered because you're not present in the groupchat.");
|
||||
} else {
|
||||
return _converse.ChatBox.prototype.getErrorMessage.call(this, stanza);
|
||||
/**
|
||||
* Looks whether we already have a moderation message for this
|
||||
* incoming message. If so, it's considered "dangling" because
|
||||
* it probably hasn't been applied to anything yet, given that
|
||||
* the relevant message is only coming in now.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#findDanglingModeration
|
||||
* @param { object } attrs - Attributes representing a received
|
||||
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
|
||||
* @returns { _converse.ChatRoomMessage }
|
||||
*/
|
||||
findDanglingModeration (attrs) {
|
||||
if (!this.messages.length) {
|
||||
return null;
|
||||
}
|
||||
// Only look for dangling moderation if there are newer
|
||||
// messages than this one, since moderation come after.
|
||||
if (this.messages.last().get('time') > attrs.time) {
|
||||
// Search from latest backwards
|
||||
const messages = Array.from(this.messages.models);
|
||||
const stanza_id = attrs[`stanza_id ${this.get('jid')}`];
|
||||
if (!stanza_id) {
|
||||
return null;
|
||||
}
|
||||
messages.reverse();
|
||||
return messages.find(
|
||||
({attributes}) =>
|
||||
attributes.moderation === 'retraction' &&
|
||||
attributes.moderated_id === stanza_id &&
|
||||
attributes.moderated_by
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles message moderation based on the passed in attributes.
|
||||
* @private
|
||||
* @method _converse.ChatRoom#handleModeration
|
||||
* @param { object } attrs - Attributes representing a received
|
||||
* message, as returned by {@link stanza_utils.getMessageAttributesFromStanza}
|
||||
* @returns { Boolean } Returns `true` or `false` depending on
|
||||
* whether a message was moderated or not.
|
||||
*/
|
||||
handleModeration (attrs) {
|
||||
const MODERATION_ATTRIBUTES = [
|
||||
'moderated',
|
||||
'moderated_by',
|
||||
'moderated_id',
|
||||
'moderation_reason'
|
||||
];
|
||||
if (attrs.moderated === 'retracted') {
|
||||
const query = {};
|
||||
const key = `stanza_id ${this.get('jid')}`;
|
||||
query[key] = attrs.moderated_id;
|
||||
const message = this.messages.findWhere(query);
|
||||
if (!message) {
|
||||
attrs['dangling_moderation'] = true;
|
||||
this.messages.create(attrs);
|
||||
return true;
|
||||
}
|
||||
message.save(pick(attrs, MODERATION_ATTRIBUTES));
|
||||
return true;
|
||||
} else {
|
||||
// Check if we have dangling moderation message
|
||||
const message = this.findDanglingModeration(attrs);
|
||||
if (message) {
|
||||
const moderation_attrs = pick(message.attributes, MODERATION_ATTRIBUTES);
|
||||
const new_attrs = Object.assign({'dangling_moderation': false}, attrs, moderation_attrs);
|
||||
delete new_attrs['id']; // Delete id, otherwise a new cache entry gets created
|
||||
message.save(new_attrs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
createMessageObject (attrs) {
|
||||
return new Promise((success, reject) => {
|
||||
this.messages.create(
|
||||
attrs,
|
||||
{ success, 'error': (m, e) => reject(e) }
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1477,21 +1662,17 @@ converse.plugins.add('converse-muc', {
|
|||
* @param { XMLElement } stanza - The message stanza.
|
||||
*/
|
||||
async onMessage (stanza) {
|
||||
const original_stanza = stanza;
|
||||
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
|
||||
if (bare_forward) {
|
||||
if (sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length) {
|
||||
return log.warn('onMessage: Ignoring unencapsulated forwarded groupchat message');
|
||||
}
|
||||
const is_carbon = u.isCarbonMessage(stanza);
|
||||
if (is_carbon) {
|
||||
// XEP-280: groupchat messages SHOULD NOT be carbon copied, so we're discarding it.
|
||||
if (u.isCarbonMessage(stanza)) {
|
||||
return log.warn(
|
||||
'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+
|
||||
'according to the XEP groupchat messages SHOULD NOT be carbon copied'
|
||||
);
|
||||
}
|
||||
const is_mam = u.isMAMMessage(stanza);
|
||||
if (is_mam) {
|
||||
const original_stanza = stanza;
|
||||
if (u.isMAMMessage(stanza)) {
|
||||
if (original_stanza.getAttribute('from') === this.get('jid')) {
|
||||
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
|
||||
stanza = sizzle(selector, stanza).pop();
|
||||
|
@ -1499,7 +1680,6 @@ converse.plugins.add('converse-muc', {
|
|||
return log.warn(`onMessage: Ignoring alleged MAM groupchat message from ${stanza.getAttribute('from')}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.createInfoMessages(stanza);
|
||||
this.fetchFeaturesIfConfigurationChanged(stanza);
|
||||
|
||||
|
@ -1510,20 +1690,18 @@ converse.plugins.add('converse-muc', {
|
|||
if (message || stanza_utils.isReceipt(stanza) || stanza_utils.isChatMarker(stanza)) {
|
||||
return _converse.api.trigger('message', {'stanza': original_stanza});
|
||||
}
|
||||
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
|
||||
this.setEditable(attrs, attrs.time);
|
||||
if (attrs.nick &&
|
||||
!this.subjectChangeHandled(attrs) &&
|
||||
!this.ignorableCSN(attrs) &&
|
||||
(attrs['chat_state'] || !u.isEmptyMessage(attrs))) {
|
||||
|
||||
const msg = this.correctMessage(attrs) ||
|
||||
await new Promise((success, reject) => {
|
||||
this.messages.create(
|
||||
attrs,
|
||||
{ success, 'erorr': (m, e) => reject(e) }
|
||||
)
|
||||
});
|
||||
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
|
||||
if (this.handleRetraction(attrs) ||
|
||||
this.handleModeration(attrs) ||
|
||||
this.subjectChangeHandled(attrs) ||
|
||||
this.ignorableCSN(attrs)) {
|
||||
return _converse.api.trigger('message', {'stanza': original_stanza});
|
||||
}
|
||||
this.setEditable(attrs, attrs.time);
|
||||
|
||||
if (attrs.nick && (attrs.is_tombstone || u.isNewMessage(attrs) || !u.isEmptyMessage(attrs))) {
|
||||
const msg = this.handleCorrection(attrs) || await this.createMessageObject(attrs);
|
||||
this.incrementUnreadMsgCounter(msg);
|
||||
}
|
||||
_converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
|
||||
|
@ -1539,7 +1717,7 @@ converse.plugins.add('converse-muc', {
|
|||
const attrs = {
|
||||
'type': 'error',
|
||||
'message': text,
|
||||
'ephemeral': true
|
||||
'is_ephemeral': true
|
||||
}
|
||||
this.messages.create(attrs);
|
||||
}
|
||||
|
@ -2121,7 +2299,7 @@ converse.plugins.add('converse-muc', {
|
|||
* @namespace _converse.api.rooms
|
||||
* @memberOf _converse.api
|
||||
*/
|
||||
'rooms': {
|
||||
rooms: {
|
||||
/**
|
||||
* Creates a new MUC chatroom (aka groupchat)
|
||||
*
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import * as strophe from 'strophe.js/src/core';
|
||||
import { get, propertyOf } from "lodash";
|
||||
import dayjs from 'dayjs';
|
||||
import log from '@converse/headless/log';
|
||||
import sizzle from 'sizzle';
|
||||
import u from '@converse/headless/utils/core';
|
||||
|
||||
|
@ -38,19 +40,19 @@ const stanza_utils = {
|
|||
* Extract the XEP-0359 stanza IDs from the passed in stanza
|
||||
* and return a map containing them.
|
||||
* @private
|
||||
* @method _converse.stanza_utils#getStanzaIDs
|
||||
* @method stanza_utils#getStanzaIDs
|
||||
* @param { XMLElement } stanza - The message stanza
|
||||
* @returns { Object }
|
||||
*/
|
||||
getStanzaIDs (stanza) {
|
||||
getStanzaIDs (stanza, original_stanza) {
|
||||
const attrs = {};
|
||||
const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
|
||||
if (stanza_ids.length) {
|
||||
stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
|
||||
}
|
||||
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
|
||||
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
|
||||
if (result) {
|
||||
const by_jid = stanza.getAttribute('from');
|
||||
const by_jid = original_stanza.getAttribute('from');
|
||||
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
|
||||
}
|
||||
|
||||
|
@ -64,35 +66,91 @@ const stanza_utils = {
|
|||
return attrs;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a passed in message stanza and returns an object of known attributes related to
|
||||
* XEP-0422 Message Fastening.
|
||||
* @private
|
||||
* @method _converse.stanza_utils#getMessageFasteningAttributes
|
||||
/** @method stanza_utils#getModerationAttributes
|
||||
* @param { XMLElement } stanza - The message stanza
|
||||
* @param { XMLElement } original_stanza - The original stanza, that contains the
|
||||
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
||||
* @param { _converse.ChatRoom } room - The MUC in which the moderation stanza is received.
|
||||
* @returns { Object }
|
||||
*/
|
||||
getMessageFasteningAttributes (stanza) {
|
||||
const substanza = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
||||
if (substanza === null) {
|
||||
return {};
|
||||
}
|
||||
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, substanza).pop();
|
||||
if (moderated) {
|
||||
const retracted = !!sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).length;
|
||||
return {
|
||||
'moderated': retracted ? 'retracted' : 'unknown',
|
||||
'moderated_by': moderated.get('by'),
|
||||
'moderated_reason': get(moderated.querySelector('reason'), 'textContent')
|
||||
getModerationAttributes (stanza, original_stanza, room) {
|
||||
const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
||||
if (fastening) {
|
||||
const applies_to_id = fastening.getAttribute('id');
|
||||
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
|
||||
if (moderated) {
|
||||
const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).pop();
|
||||
if (retracted) {
|
||||
const from = stanza.getAttribute('from');
|
||||
if (from !== room.get('jid')) {
|
||||
log.warn("getModerationAttributes: ignore moderation stanza that's not from the MUC!");
|
||||
log.error(original_stanza);
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'moderated': 'retracted',
|
||||
'moderated_by': moderated.getAttribute('by'),
|
||||
'moderated_id': applies_to_id,
|
||||
'moderation_reason': get(moderated.querySelector('reason'), 'textContent')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE}"]`, stanza).pop();
|
||||
if (tombstone) {
|
||||
const retracted = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT}"]`, tombstone).pop();
|
||||
if (retracted) {
|
||||
return {
|
||||
'is_tombstone': true,
|
||||
'retracted': tombstone.getAttribute('stamp'),
|
||||
'moderated_by': tombstone.getAttribute('by'),
|
||||
'moderation_reason': get(tombstone.querySelector('reason'), 'textContent')
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @method stanza_utils#getRetractionAttributes
|
||||
* @param { XMLElement } stanza - The message stanza
|
||||
* @param { XMLElement } original_stanza - The original stanza, that contains the
|
||||
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
||||
* @returns { Object }
|
||||
*/
|
||||
getRetractionAttributes (stanza, original_stanza) {
|
||||
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
||||
if (fastening) {
|
||||
const applies_to_id = fastening.getAttribute('id');
|
||||
const retracted = sizzle(`> retract[xmlns="${Strophe.NS.RETRACT}"]`, fastening).pop();
|
||||
if (retracted) {
|
||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString();
|
||||
return {
|
||||
'retracted': time,
|
||||
'retracted_id': applies_to_id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tombstone = sizzle(`> retracted[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
|
||||
if (tombstone) {
|
||||
return {
|
||||
'retracted': tombstone.getAttribute('stamp'),
|
||||
'is_tombstone': true
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
getReferences (stanza) {
|
||||
const text = propertyOf(stanza.querySelector('body'))('textContent');
|
||||
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
|
||||
const begin = ref.getAttribute('begin'),
|
||||
end = ref.getAttribute('end');
|
||||
const begin = ref.getAttribute('begin');
|
||||
const end = ref.getAttribute('end');
|
||||
return {
|
||||
'begin': begin,
|
||||
'end': end,
|
||||
|
@ -105,8 +163,7 @@ const stanza_utils = {
|
|||
|
||||
|
||||
getSenderAttributes (stanza, chatbox, _converse) {
|
||||
const type = stanza.getAttribute('type');
|
||||
if (type === 'groupchat') {
|
||||
if (u.isChatRoom(chatbox)) {
|
||||
const from = stanza.getAttribute('from');
|
||||
const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
|
||||
return {
|
||||
|
@ -152,20 +209,107 @@ const stanza_utils = {
|
|||
return {};
|
||||
},
|
||||
|
||||
getCorrectionAttributes (stanza) {
|
||||
getCorrectionAttributes (stanza, original_stanza) {
|
||||
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
|
||||
if (el) {
|
||||
const replaced_id = el.getAttribute('id');
|
||||
const msgid = replaced_id;
|
||||
if (replaced_id) {
|
||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString();
|
||||
return {
|
||||
msgid,
|
||||
replaced_id,
|
||||
'edited': new Date().toISOString()
|
||||
'edited': time
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
getErrorMessage (stanza, is_muc, _converse) {
|
||||
const { __ } = _converse;
|
||||
if (is_muc) {
|
||||
if (sizzle(`forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
||||
return __("Your message was not delivered because you're not allowed to send messages in this groupchat.");
|
||||
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
|
||||
return __("Your message was not delivered because you're not present in the groupchat.");
|
||||
}
|
||||
}
|
||||
const error = stanza.querySelector('error');
|
||||
return propertyOf(error.querySelector('text'))('textContent') ||
|
||||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a message stanza, return the text contained in its body.
|
||||
* @private
|
||||
* @method stanza_utils#getMessageBody
|
||||
* @param { XMLElement } stanza
|
||||
* @param { Boolean } is_muc
|
||||
* @param { _converse } _converse
|
||||
*/
|
||||
getMessageBody (stanza, is_muc, _converse) {
|
||||
const type = stanza.getAttribute('type');
|
||||
if (type === 'error') {
|
||||
return stanza_utils.getErrorMessage(stanza, is_muc, _converse);
|
||||
} else {
|
||||
const body = stanza.querySelector('body');
|
||||
if (body) {
|
||||
return body.textContent.trim();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getChatState (stanza) {
|
||||
return stanza.getElementsByTagName('composing').length && 'composing' ||
|
||||
stanza.getElementsByTagName('paused').length && 'paused' ||
|
||||
stanza.getElementsByTagName('inactive').length && 'inactive' ||
|
||||
stanza.getElementsByTagName('active').length && 'active' ||
|
||||
stanza.getElementsByTagName('gone').length && 'gone';
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a passed in message stanza and returns an object of attributes.
|
||||
* @private
|
||||
* @method stanza_utils#getMessageAttributesFromStanza
|
||||
* @param { XMLElement } stanza - The message stanza
|
||||
* @param { XMLElement } original_stanza - The original stanza, that contains the
|
||||
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
||||
* @param { _converse.ChatBox|_converse.ChatRoom } chatbox
|
||||
* @param { _converse } _converse
|
||||
* @returns { Object }
|
||||
*/
|
||||
async getMessageAttributesFromStanza (stanza, original_stanza, chatbox, _converse) {
|
||||
const is_muc = u.isChatRoom(chatbox);
|
||||
let attrs = Object.assign(
|
||||
stanza_utils.getStanzaIDs(stanza, original_stanza),
|
||||
stanza_utils.getRetractionAttributes(stanza, original_stanza),
|
||||
is_muc ? stanza_utils.getModerationAttributes(stanza, original_stanza, chatbox) : {},
|
||||
);
|
||||
const text = stanza_utils.getMessageBody(stanza, is_muc, _converse) || undefined;
|
||||
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
||||
attrs = Object.assign(
|
||||
{
|
||||
'chat_state': stanza_utils.getChatState(stanza),
|
||||
'is_archived': stanza_utils.isArchived(original_stanza),
|
||||
'is_delayed': !!delay,
|
||||
'is_single_emoji': text ? await u.isOnlyEmojis(text) : false,
|
||||
'message': text,
|
||||
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
|
||||
'references': stanza_utils.getReferences(stanza),
|
||||
'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
|
||||
'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
|
||||
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
|
||||
'type': stanza.getAttribute('type')
|
||||
},
|
||||
attrs,
|
||||
stanza_utils.getSenderAttributes(stanza, chatbox, _converse),
|
||||
stanza_utils.getOutOfBandAttributes(stanza),
|
||||
stanza_utils.getSpoilerAttributes(stanza),
|
||||
stanza_utils.getCorrectionAttributes(stanza, original_stanza)
|
||||
)
|
||||
return attrs;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header {{{o.type}}}">
|
||||
<div class="modal-header {{{o.level}}}">
|
||||
<h5 class="modal-title">{{{o.title}}}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
|
|
|
@ -15,27 +15,37 @@
|
|||
</span>
|
||||
<div class="chat-msg__body chat-msg__body--{{{o.type}}} {{{o.received ? 'chat-msg__body--received' : '' }}} {{{o.is_delayed ? 'chat-msg__body--delayed' : '' }}}">
|
||||
<div class="chat-msg__message">
|
||||
{[ if (o.is_spoiler) { ]}
|
||||
<div class="chat-msg__spoiler-hint">
|
||||
<span class="spoiler-hint">{{{o.spoiler_hint}}}</span>
|
||||
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>{{{o.label_show}}}</a>
|
||||
</div>
|
||||
{[ if (o.is_retracted) { ]}
|
||||
<div>{{{o.retraction_text}}}</div>
|
||||
{[ if (o.moderation_reason) { ]}<q class="chat-msg--retracted__reason">{{{o.moderation_reason}}}</q>{[ } ]}
|
||||
{[ } else { ]}
|
||||
{[ if (o.is_spoiler) { ]}
|
||||
<div class="chat-msg__spoiler-hint">
|
||||
<span class="spoiler-hint">{{{o.spoiler_hint}}}</span>
|
||||
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>{{{o.label_show}}}</a>
|
||||
</div>
|
||||
{[ } ]}
|
||||
|
||||
{[ if (o.subject) { ]}
|
||||
<div class="chat-msg__subject">{{{ o.subject }}}</div>
|
||||
{[ } ]}
|
||||
<div class="chat-msg__text
|
||||
{[ if (o.is_single_emoji) { ]} chat-msg__text--larger{[ } ]}
|
||||
{[ if (o.is_spoiler) { ]} spoiler collapsed{[ } ]}"><!-- message gets added here via renderMessage --></div>
|
||||
<div class="chat-msg__media"></div>
|
||||
{[ } ]}
|
||||
{[ if (o.subject) { ]}
|
||||
<div class="chat-msg__subject">{{{ o.subject }}}</div>
|
||||
{[ } ]}
|
||||
<div class="chat-msg__text
|
||||
{[ if (o.is_single_emoji) { ]} chat-msg__text--larger{[ } ]}
|
||||
{[ if (o.is_spoiler) { ]} spoiler collapsed{[ } ]}"><!-- message gets added here via renderMessage --></div>
|
||||
<div class="chat-msg__media"></div>
|
||||
</div>
|
||||
{[ if (o.received && !o.is_me_message && !o.is_groupchat_message) { ]} <span class="fa fa-check chat-msg__receipt"></span> {[ } ]}
|
||||
{[ if (o.edited) { ]} <i title="{{{o.__('This message has been edited')}}}" class="fa fa-edit chat-msg__edit-modal"></i> {[ } ]}
|
||||
{[ if (o.editable) { ]}
|
||||
<div class="chat-msg__actions">
|
||||
<div class="chat-msg__actions">
|
||||
{[ if (o.editable) { ]}
|
||||
<button class="chat-msg__action chat-msg__action-edit fa fa-pencil-alt" title="{{{o.__('Edit this message')}}}"></button>
|
||||
</div>
|
||||
{[ } ]}
|
||||
{[ } ]}
|
||||
<!-- FIXME -->
|
||||
{[ if ((o.sender === 'me' || o.is_groupchat_message) && true) { ]}
|
||||
<button class="chat-msg__action chat-msg__action-retract fa fa-trash-alt" title="{{{o.__('Retract this message')}}}"></button>
|
||||
{[ } ]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header {{{o.level}}}">
|
||||
<h5 class="modal-title">{{{o.title}}}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="converse-form converse-form--modal confirm" action="#">
|
||||
<div class="form-group">
|
||||
{[o.messages.forEach(function (message) { ]}
|
||||
<p>{{{message}}}</p>
|
||||
{[ }) ]}
|
||||
</div>
|
||||
{[ if (o.type === 'prompt') { ]}
|
||||
<div class="form-group">
|
||||
<input type="text" name="reason" class="form-control" placeholder="{{{o.placeholder}}}"/>
|
||||
</div>
|
||||
{[ } ]}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">{{{o.__('OK')}}}</button>
|
||||
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="{{{o.__('Cancel')}}}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -293,6 +293,8 @@ u.ancestor = function (el, selector) {
|
|||
* Return the element's siblings until one matches the selector.
|
||||
* @private
|
||||
* @method u#nextUntil
|
||||
* @param { HTMLElement } el
|
||||
* @param { String } selector
|
||||
*/
|
||||
u.nextUntil = function (el, selector) {
|
||||
const matches = [];
|
||||
|
|
|
@ -55,6 +55,7 @@ var specs = [
|
|||
"spec/user-details-modal",
|
||||
"spec/messages",
|
||||
"spec/muc_messages",
|
||||
"spec/retractions",
|
||||
"spec/muc",
|
||||
"spec/modtools",
|
||||
"spec/room_registration",
|
||||
|
|
|
@ -18,17 +18,13 @@
|
|||
});
|
||||
converse.initialize({
|
||||
auto_away: 300,
|
||||
auto_login: true,
|
||||
auto_register_muc_nickname: true,
|
||||
bosh_service_url: 'http://chat.example.org:5380/http-bind/',
|
||||
debug: true,
|
||||
enable_smacks: true,
|
||||
i18n: 'en',
|
||||
jid: 'klaus.dresner@chat.example.org',
|
||||
message_archiving: 'always',
|
||||
muc_domain: 'conference.chat.example.org',
|
||||
muc_respect_autojoin: true,
|
||||
password: 'secret',
|
||||
view_mode: 'fullscreen',
|
||||
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
|
||||
whitelisted_plugins: ['converse-debug'],
|
||||
|
|
Loading…
Reference in New Issue