Commit b4dafcc4 authored by JC Brand's avatar JC Brand

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
parent 4b3d427c
...@@ -2,10 +2,9 @@ ...@@ -2,10 +2,9 @@
## 6.0.0 (Unreleased) ## 6.0.0 (Unreleased)
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now. - Add support for [XEP-0424 Message Retraction](http://localhost:3080/extensions/xep-0424.html)
- #1105: Preliminary support for storing persistent data in IndexedDB instead of localStorage - Add support for [XEP-0425 Message Moderation](http://localhost:3080/extensions/xep-0425.html)
- #1691: Fix `collection.chatbox is undefined` errors - Prevent editing of sent file uploads.
- #1772: `_converse.api.contact.add(jid, nick)` fails, says not a function
- Initial support for sending custom emojis. Currently only between Converse - Initial support for sending custom emojis. Currently only between Converse
instances. Still working out a wire protocol for compatibility with other clients. instances. Still working out a wire protocol for compatibility with other clients.
To add custom emojis, edit the `emojis.json` file. To add custom emojis, edit the `emojis.json` file.
...@@ -14,6 +13,13 @@ ...@@ -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_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) - 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 ### Breaking changes
- In contrast to sessionStorage and localStorage, IndexedDB is an asynchronous database. - In contrast to sessionStorage and localStorage, IndexedDB is an asynchronous database.
......
...@@ -39,9 +39,9 @@ which shows you how to use the CDN (content delivery network) to quickly get a d ...@@ -39,9 +39,9 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
## Features ## Features
- Available as overlayed chat boxes or as a fullscreen application. See [inverse.chat](https://inverse.chat) for the fullscreen version. - 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/) - 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) - Multi-user chat rooms [XEP 45](https://xmpp.org/extensions/xep-0045.html)
- Chatroom bookmarks [XEP 48](https://xmpp.org/extensions/xep-0048.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) - 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 ...@@ -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) - 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) - Roster item exchange [XEP 144](https://xmpp.org/extensions/tmp/xep-0144-1.1.html)
- Chat statuses (online, busy, away, offline) - Chat statuses (online, busy, away, offline)
- Custom status messages
- Typing and state notifications [XEP 85](https://xmpp.org/extensions/xep-0085.html) - 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) - 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) - 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) - 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 ...@@ -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) - Client state indication [XEP 352](https://xmpp.org/extensions/xep-0352.html)
- Last Message Correction [XEP 308](https://xmpp.org/extensions/xep-0308.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") - 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). - Anonymous logins, see the [anonymous login demo](https://conversejs.org/demo/anonymous.html)
- Translated into 28 languages - 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 ## Integration into other frameworks
......
...@@ -1475,6 +1475,26 @@ not fulfilled. ...@@ -1475,6 +1475,26 @@ not fulfilled.
Requires the `src/converse-notification.js` plugin. 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 use_system_emojis
----------------- -----------------
* Default: ``true`` * Default: ``true``
......
...@@ -171,8 +171,8 @@ ...@@ -171,8 +171,8 @@
See <a href="https://inverse.chat" target="_blank" rel="noopener">inverse.chat</a> for the fullscreen version. See <a href="https://inverse.chat" target="_blank" rel="noopener">inverse.chat</a> for the fullscreen version.
</li> </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>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>Chat statuses (online, busy, away, offline)</li>
<li>Contacts and groups</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>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>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> <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 @@ ...@@ -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>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>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>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>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>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>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>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> <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 @@ ...@@ -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>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>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>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>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>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> </ul>
</div> </div>
</div> </div>
......
...@@ -143,6 +143,9 @@ ...@@ -143,6 +143,9 @@
&.badge { &.badge {
color: var(--chat-head-text-color); color: var(--chat-head-text-color);
} }
&.chat-msg--retracted {
color: var(--subdued-color);
}
} }
.disconnect-container { .disconnect-container {
margin: 1em; margin: 1em;
......
...@@ -317,6 +317,16 @@ body.converse-fullscreen { ...@@ -317,6 +317,16 @@ body.converse-fullscreen {
color: var(--gray-color); color: var(--gray-color);
} }
q {
quotes: "“" "”" "‘" "’";
}
q:before {
content: open-quote;
}
q:after {
content: close-quote;
}
.modal { .modal {
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
......
...@@ -39,6 +39,12 @@ ...@@ -39,6 +39,12 @@
} }
} }
&.chat-msg--retracted {
.chat-msg__message {
color: var(--subdued-color);
}
}
&.chat-info { &.chat-info {
color: var(--chat-head-color); color: var(--chat-head-color);
font-size: var(--message-font-size); font-size: var(--message-font-size);
...@@ -46,6 +52,9 @@ ...@@ -46,6 +52,9 @@
font-size: 90%; font-size: 90%;
padding: 0.17rem 1rem; padding: 0.17rem 1rem;
&.chat-msg--followup {
margin-left: 2.75rem;
}
&.badge { &.badge {
color: var(--chat-head-text-color); color: var(--chat-head-text-color);
} }
...@@ -60,6 +69,9 @@ ...@@ -60,6 +69,9 @@
color: var(--error-color); color: var(--error-color);
font-weight: bold; font-weight: bold;
} }
.q {
font-style: italic;
}
} }
.chat-image { .chat-image {
...@@ -225,7 +237,7 @@ ...@@ -225,7 +237,7 @@
height: var(--message-font-size); height: var(--message-font-size);
font-size: var(--message-font-size); font-size: var(--message-font-size);
padding: 0; padding: 0;
padding-left: 0.5em; padding-left: 0.75em;
border: none; border: none;
opacity: 0; opacity: 0;
background: transparent; background: transparent;
...@@ -336,6 +348,11 @@ ...@@ -336,6 +348,11 @@
} }
} }
} }
&.chat-info {
&.chat-msg--followup {
margin-left: 0;
}
}
} }
} }
......
#conversejs { #conversejs {
#converse-modals { #converse-modals {
.modal-body { .modal-body {
margin-bottom: 2em; margin-bottom: 2em;
.confirm {
.form-group {
p:first-child {
font-size: 110%;
font-weight: bold;
}
}
}
} }
.scrollable-container { .scrollable-container {
......
...@@ -720,7 +720,6 @@ ...@@ -720,7 +720,6 @@
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
await test_utils.waitForRoster(_converse, 'current'); await test_utils.waitForRoster(_converse, 'current');
// Send a message from a different resource // Send a message from a different resource
spyOn(_converse, 'log');
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await test_utils.openChatBoxFor(_converse, recipient_jid); const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
const msg = $msg({ const msg = $msg({
...@@ -848,7 +847,6 @@ ...@@ -848,7 +847,6 @@
await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'));
await test_utils.waitForRoster(_converse, 'current'); await test_utils.waitForRoster(_converse, 'current');
// Send a message from a different resource // Send a message from a different resource
spyOn(_converse, 'log');
const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await test_utils.openChatBoxFor(_converse, recipient_jid); const view = await test_utils.openChatBoxFor(_converse, recipient_jid);
const msg = $msg({ const msg = $msg({
......
...@@ -1037,7 +1037,7 @@ ...@@ -1037,7 +1037,7 @@
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
expect(view.model.messages.length).toBe(1); 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('type')).toBe('error');
expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.'); expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.');
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); 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'); let action = view.el.querySelector('.chat-msg .chat-msg__action');
expect(action.getAttribute('title')).toBe('Edit this message'); expect(action.getAttribute('title')).toBe('Edit this message');
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
); );
await new Promise(resolve => view.once('messageInserted', resolve)); 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 // Test confirmation dialog
spyOn(window, 'confirm').and.returnValue(true); spyOn(window, 'confirm').and.returnValue(true);
......
...@@ -5232,6 +5232,7 @@ ...@@ -5232,6 +5232,7 @@
const textarea = view.el.querySelector('.chat-textarea'); const textarea = view.el.querySelector('.chat-textarea');
textarea.value = 'Hello world'; textarea.value = 'Hello world';
view.onFormSubmitted(new Event('submit')); view.onFormSubmitted(new Event('submit'));
await new Promise(resolve => view.once('messageInserted', resolve));
const stanza = u.toStanza(` const stanza = u.toStanza(`
<message xmlns="jabber:client" type="error" to="troll@montague.lit/resource" from="trollbox@montague.lit"> <message xmlns="jabber:client" type="error" to="troll@montague.lit/resource" from="trollbox@montague.lit">
...@@ -5240,6 +5241,7 @@ ...@@ -5240,6 +5241,7 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelector('.chat-error').textContent.trim()).toBe( 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."); "Your message was not delivered because you're not allowed to send messages in this groupchat.");
done(); done();
......
This diff is collapsed.
...@@ -65,6 +65,7 @@ converse.plugins.add('converse-chatview', { ...@@ -65,6 +65,7 @@ converse.plugins.add('converse-chatview', {
'auto_focus': true, 'auto_focus': true,
'message_limit': 0, 'message_limit': 0,
'show_send_button': false, 'show_send_button': false,
'show_retraction_warning': true,
'show_toolbar': true, 'show_toolbar': true,
'time_format': 'HH:mm', 'time_format': 'HH:mm',
'visible_toolbar_buttons': { 'visible_toolbar_buttons': {
...@@ -226,6 +227,7 @@ converse.plugins.add('converse-chatview', { ...@@ -226,6 +227,7 @@ converse.plugins.add('converse-chatview', {
events: { events: {
'change input.fileupload': 'onFileSelection', 'change input.fileupload': 'onFileSelection',
'click .chat-msg__action-edit': 'onMessageEditButtonClicked', 'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
'click .chatbox-navback': 'showControlBox', 'click .chatbox-navback': 'showControlBox',
'click .close-chatbox-button': 'close', 'click .close-chatbox-button': 'close',
'click .new-msgs-indicator': 'viewUnreadMessages', 'click .new-msgs-indicator': 'viewUnreadMessages',
...@@ -622,8 +624,8 @@ converse.plugins.add('converse-chatview', { ...@@ -622,8 +624,8 @@ converse.plugins.add('converse-chatview', {
return this.trigger('messageInserted', view.el); return this.trigger('messageInserted', view.el);
} }
} }
const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date(), const current_msg_date = dayjs(view.model.get('time')).toDate() || new Date();
previous_msg_date = this.getLastMessageDate(current_msg_date); const previous_msg_date = this.getLastMessageDate(current_msg_date);
if (previous_msg_date === null) { if (previous_msg_date === null) {
this.content.insertAdjacentElement('afterbegin', view.el); this.content.insertAdjacentElement('afterbegin', view.el);
...@@ -649,9 +651,8 @@ converse.plugins.add('converse-chatview', { ...@@ -649,9 +651,8 @@ converse.plugins.add('converse-chatview', {
* followup message or not. * followup message or not.
* *
* Followup messages are subsequent ones written by the same * Followup messages are subsequent ones written by the same
* author with no other conversation elements inbetween and * author with no other conversation elements in between and
* posted within 10 minutes of one another. * which were posted within 10 minutes of one another.
*
* @private * @private
* @method _converse.ChatBoxView#markFollowups * @method _converse.ChatBoxView#markFollowups
* @param { HTMLElement } el - The message element * @param { HTMLElement } el - The message element
...@@ -730,11 +731,9 @@ converse.plugins.add('converse-chatview', { ...@@ -730,11 +731,9 @@ converse.plugins.add('converse-chatview', {
// We already have a view for this message // We already have a view for this message
return; return;
} }
if (!u.isNewMessage(message) && u.isEmptyMessage(message)) { if (!message.get('dangling_retraction')) {
// Ignore archived or delayed messages without any text to show. await this.showMessage(message);
return message.destroy();
} }
await this.showMessage(message);
/** /**
* Triggered once a message has been added to a chatbox. * Triggered once a message has been added to a chatbox.
* @event _converse#messageAdded * @event _converse#messageAdded
...@@ -914,6 +913,45 @@ converse.plugins.add('converse-chatview', { ...@@ -914,6 +913,45 @@ converse.plugins.add('converse-chatview', {
this.insertIntoTextArea('', true, false); 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) { onMessageEditButtonClicked (ev) {
ev.preventDefault(); ev.preventDefault();
......
...@@ -21,7 +21,7 @@ import tpl_message_versions_modal from "templates/message_versions_modal.html"; ...@@ -21,7 +21,7 @@ import tpl_message_versions_modal from "templates/message_versions_modal.html";
import tpl_spinner from "templates/spinner.html"; import tpl_spinner from "templates/spinner.html";
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
const { dayjs } = converse.env; const { Strophe, dayjs } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -140,22 +140,20 @@ converse.plugins.add('converse-message-view', { ...@@ -140,22 +140,20 @@ converse.plugins.add('converse-message-view', {
} else { } else {
await this.renderChatMessage(); await this.renderChatMessage();
} }
if (is_followup) { is_followup && u.addClass('chat-msg--followup', this.el);
u.addClass('chat-msg--followup', this.el);
}
return this.el; return this.el;
}, },
async onChanged (item) { async onChanged (item) {
// Jot down whether it was edited because the `changed` // Jot down whether it was edited because the `changed`
// attr gets removed when this.render() gets called further // attr gets removed when this.render() gets called further down.
// down.
const edited = item.changed.edited; const edited = item.changed.edited;
if (this.model.changed.progress) { if (this.model.changed.progress) {
return this.renderFileUploadProgresBar(); return this.renderFileUploadProgresBar();
} }
const isValidChange = prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop); 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(); await this.debouncedRender();
} }
if (edited) { if (edited) {
...@@ -243,19 +241,22 @@ converse.plugins.add('converse-message-view', { ...@@ -243,19 +241,22 @@ converse.plugins.add('converse-message-view', {
const time = dayjs(this.model.get('time')); const time = dayjs(this.model.get('time'));
const role = this.model.vcard ? this.model.vcard.get('role') : null; const role = this.model.vcard ? this.model.vcard.get('role') : null;
const roles = role ? role.split(',') : []; const roles = role ? role.split(',') : [];
const is_retracted = this.model.get('retracted') || this.model.get('moderated') === 'retracted';
const msg = u.stringToElement(tpl_message( const msg = u.stringToElement(tpl_message(
Object.assign( Object.assign(
this.model.toJSON(), { this.model.toJSON(), {
'__': __, __,
is_retracted,
'extra_classes': this.getExtraMessageClasses(),
'is_groupchat_message': this.model.get('type') === 'groupchat', 'is_groupchat_message': this.model.get('type') === 'groupchat',
'occupant': this.model.occupant,
'is_me_message': this.model.isMeCommand(), 'is_me_message': this.model.isMeCommand(),
'roles': roles, 'label_show': __('Show more'),
'occupant': this.model.occupant,
'pretty_time': time.format(_converse.time_format), 'pretty_time': time.format(_converse.time_format),
'retraction_text': is_retracted ? this.getRetractionText() : null,
'roles': roles,
'time': time.toISOString(), 'time': time.toISOString(),
'extra_classes': this.getExtraMessageClasses(),
'label_show': __('Show more'),
'username': this.model.getDisplayName() 'username': this.model.getDisplayName()
}) })
)); ));
...@@ -265,11 +266,13 @@ converse.plugins.add('converse-message-view', { ...@@ -265,11 +266,13 @@ converse.plugins.add('converse-message-view', {
msg.querySelector('.chat-msg__media').innerHTML = this.transformOOBURL(url); msg.querySelector('.chat-msg__media').innerHTML = this.transformOOBURL(url);
} }
const text = this.model.getMessageText(); if (!is_retracted) {
const msg_content = msg.querySelector('.chat-msg__text'); const text = this.model.getMessageText();
if (text && text !== url) { const msg_content = msg.querySelector('.chat-msg__text');
msg_content.innerHTML = await this.transformBodyText(text); if (text && text !== url) {
await u.renderImageURLs(_converse, msg_content); msg_content.innerHTML = await this.transformBodyText(text);
await u.renderImageURLs(_converse, msg_content);
}
} }
if (this.model.get('type') !== 'headline') { if (this.model.get('type') !== 'headline') {
this.renderAvatar(msg); this.renderAvatar(msg);
...@@ -292,6 +295,25 @@ converse.plugins.add('converse-message-view', { ...@@ -292,6 +295,25 @@ converse.plugins.add('converse-message-view', {
return this.replaceElement(msg); 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 () { renderErrorMessage () {
const msg = u.stringToElement( const msg = u.stringToElement(
tpl_info(Object.assign(this.model.toJSON(), { tpl_info(Object.assign(this.model.toJSON(), {
...@@ -304,8 +326,8 @@ converse.plugins.add('converse-message-view', { ...@@ -304,8 +326,8 @@ converse.plugins.add('converse-message-view', {
renderChatStateNotification () { renderChatStateNotification () {
let text; let text;
const from = this.model.get('from'), const from = this.model.get('from');
name = this.model.getDisplayName(); const name = this.model.getDisplayName();
if (this.model.get('chat_state') === _converse.COMPOSING) { if (this.model.get('chat_state') === _converse.COMPOSING) {
if (this.model.get('sender') === 'me') { if (this.model.get('sender') === 'me') {
...@@ -354,8 +376,10 @@ converse.plugins.add('converse-message-view', { ...@@ -354,8 +376,10 @@ converse.plugins.add('converse-message-view', {
}, },
getExtraMessageClasses () { 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.get('type') === 'groupchat') {
if (this.model.occupant) { if (this.model.occupant) {
extra_classes += ` ${this.model.occupant.get('role') || ''} ${this.model.occupant.get('affiliation') || ''}`; extra_classes += ` ${this.model.occupant.get('role') || ''} ${this.model.occupant.get('affiliation') || ''}`;
......
...@@ -12,6 +12,7 @@ import converse from "@converse/headless/converse-core"; ...@@ -12,6 +12,7 @@ import converse from "@converse/headless/converse-core";
import { isString } from "lodash"; import { isString } from "lodash";
import tpl_alert from "templates/alert.html"; import tpl_alert from "templates/alert.html";
import tpl_alert_modal from "templates/alert_modal.html"; import tpl_alert_modal from "templates/alert_modal.html";
import tpl_prompt from "templates/prompt.html";
const { Backbone, sizzle } = converse.env; const { Backbone, sizzle } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -21,6 +22,7 @@ converse.plugins.add('converse-modal', { ...@@ -21,6 +22,7 @@ converse.plugins.add('converse-modal', {
initialize () { initialize () {
const { _converse } = this; const { _converse } = this;
const { __ } = _converse;
_converse.BootstrapModal = Backbone.VDOMView.extend({ _converse.BootstrapModal = Backbone.VDOMView.extend({
...@@ -79,18 +81,69 @@ converse.plugins.add('converse-modal', { ...@@ -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 () { initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments); _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render) this.listenTo(this.model, 'change', this.render)
}, },
toHTML () { 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', () => { _converse.api.listen.on('afterTearDown', () => {
if (!_converse.chatboxviews) { if (!_converse.chatboxviews) {
return; return;
...@@ -104,40 +157,112 @@ converse.plugins.add('converse-modal', { ...@@ -104,40 +157,112 @@ converse.plugins.add('converse-modal', {
/************************ BEGIN API ************************/ /************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms. // 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, { 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. * Show an alert modal to the user.
* @method _converse.api.alert * @method _converse.api.alert
* @param { ('info'|'warn'|'error') } type - The type of alert. * @param { ('info'|'warn'|'error') } type - The type of alert.
* @returns { String } title - The header text for the alert. * @param { String } title - The header text for the alert.
* @returns { (String[]|String) } messages - The alert text to show to the user. * @param { (String[]|String) } messages - The alert text to show to the user.
*/ */
alert (type, title, messages) { alert (type, title, messages) {
if (isString(messages)) { if (isString(messages)) {
messages = [messages]; messages = [messages];
} }
let level;
if (type === 'error') { if (type === 'error') {
type = 'alert-danger'; level = 'alert-danger';
} else if (type === 'info') { } else if (type === 'info') {
type = 'alert-info'; level = 'alert-info';
} else if (type === 'warn') { } else if (type === 'warn') {
type = 'alert-warning'; level = 'alert-warning';
} }
if (alert === undefined) { if (alert === undefined) {
const model = new Backbone.Model({ const model = new Backbone.Model({
'title': title, 'title': title,
'messages': messages, 'messages': messages,
'type': type 'level': level,
'type': 'alert'
}) })
alert = new _converse.Alert({'model': model}); alert = new _converse.Alert({model});
} else { } else {
alert.model.set({ alert.model.set({
'title': title, 'title': title,
'messages': messages, 'messages': messages,
'type': type 'level': level
}); });
} }
alert.show(); alert.show();
......
...@@ -41,7 +41,6 @@ import tpl_rooms_results from "templates/rooms_results.html"; ...@@ -41,7 +41,6 @@ import tpl_rooms_results from "templates/rooms_results.html";
import tpl_spinner from "templates/spinner.html"; import tpl_spinner from "templates/spinner.html";
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
const { Backbone, Strophe, sizzle, _, $iq, $pres } = converse.env; const { Backbone, Strophe, sizzle, _, $iq, $pres } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -108,6 +107,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -108,6 +107,7 @@ converse.plugins.add('converse-muc-views', {
'auto_list_rooms': false, 'auto_list_rooms': false,
'cache_muc_messages': true, 'cache_muc_messages': true,
'locked_muc_nickname': false, 'locked_muc_nickname': false,
'show_retraction_warning': true,
'muc_disable_slash_commands': false, 'muc_disable_slash_commands': false,
'muc_show_join_leave': true, 'muc_show_join_leave': true,
'muc_show_join_leave_status': true, 'muc_show_join_leave_status': true,
...@@ -630,6 +630,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -630,6 +630,7 @@ converse.plugins.add('converse-muc-views', {
events: { events: {
'change input.fileupload': 'onFileSelection', 'change input.fileupload': 'onFileSelection',
'click .chat-msg__action-edit': 'onMessageEditButtonClicked', 'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
'click .chatbox-navback': 'showControlBox', 'click .chatbox-navback': 'showControlBox',
'click .close-chatbox-button': 'close', 'click .close-chatbox-button': 'close',
'click .configure-chatroom-button': 'getAndRenderConfigurationForm', 'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
...@@ -724,8 +725,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -724,8 +725,7 @@ converse.plugins.add('converse-muc-views', {
}, },
renderChatArea () { 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) { if (this.el.querySelector('.chat-area') === null) {
const container_el = this.el.querySelector('.chatroom-body'); const container_el = this.el.querySelector('.chatroom-body');
container_el.insertAdjacentHTML( container_el.insertAdjacentHTML(
...@@ -811,6 +811,101 @@ converse.plugins.add('converse-muc-views', { ...@@ -811,6 +811,101 @@ converse.plugins.add('converse-muc-views', {
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev); 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) { showModeratorToolsModal (affiliation) {
if (!this.verifyRoles(['moderator'])) { if (!this.verifyRoles(['moderator'])) {
return; return;
...@@ -2193,7 +2288,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -2193,7 +2288,7 @@ converse.plugins.add('converse-muc-views', {
* @namespace _converse.api.roomviews * @namespace _converse.api.roomviews
* @memberOf _converse.api * @memberOf _converse.api
*/ */
'roomviews': { roomviews: {
/** /**
* Retrieves a groupchat (aka chatroom) view. The chat should already be open. * Retrieves a groupchat (aka chatroom) view. The chat should already be open.
* *
......
...@@ -121,6 +121,7 @@ converse.plugins.add('converse-notification', { ...@@ -121,6 +121,7 @@ converse.plugins.add('converse-notification', {
_converse.areDesktopNotificationsEnabled = function () { _converse.areDesktopNotificationsEnabled = function () {
return _converse.supports_html5_notification && return _converse.supports_html5_notification &&
_converse.show_desktop_notifications && _converse.show_desktop_notifications &&
Notification.permission === "granted"; Notification.permission === "granted";
}; };
......
This diff is collapsed.
...@@ -37,16 +37,19 @@ Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); ...@@ -37,16 +37,19 @@ Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); Strophe.addNamespace('DELAY', 'urn:xmpp:delay');
Strophe.addNamespace('FASTEN', 'urn:xmpp:fasten:0');
Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0'); Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0');
Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0');
Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1'); Strophe.addNamespace('IDLE', 'urn:xmpp:idle:1');
Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); 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('NICK', 'http://jabber.org/protocol/nick');
Strophe.addNamespace('OMEMO', 'eu.siacs.conversations.axolotl'); Strophe.addNamespace('OMEMO', 'eu.siacs.conversations.axolotl');
Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob'); Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob');
Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
Strophe.addNamespace('REGISTER', 'jabber:iq:register'); Strophe.addNamespace('REGISTER', 'jabber:iq:register');
Strophe.addNamespace('RETRACT', 'urn:xmpp:message-retract:0');
Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
Strophe.addNamespace('SID', 'urn:xmpp:sid:0'); Strophe.addNamespace('SID', 'urn:xmpp:sid:0');
...@@ -92,8 +95,7 @@ const CORE_PLUGINS = [ ...@@ -92,8 +95,7 @@ const CORE_PLUGINS = [
'converse-rsm', 'converse-rsm',
'converse-smacks', 'converse-smacks',
'converse-status', 'converse-status',
'converse-vcard', 'converse-vcard'
'stanza-utils'
]; ];
...@@ -103,7 +105,7 @@ const CORE_PLUGINS = [ ...@@ -103,7 +105,7 @@ const CORE_PLUGINS = [
* @global * @global
* @namespace _converse * @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. // such to get JSDoc to create the correct document site strucure.
const _converse = { const _converse = {
'templates': {}, 'templates': {},
...@@ -142,6 +144,10 @@ class TimeoutError extends Error {} ...@@ -142,6 +144,10 @@ class TimeoutError extends Error {}
_converse.TimeoutError = TimeoutError; _converse.TimeoutError = TimeoutError;
class IllegalMessage extends Error {}
_converse.IllegalMessage = IllegalMessage;
// Make converse pluggable // Make converse pluggable
pluggable.enable(_converse, '_converse', 'pluggable'); pluggable.enable(_converse, '_converse', 'pluggable');
...@@ -187,7 +193,7 @@ _converse.LOGOUT = 'logout'; ...@@ -187,7 +193,7 @@ _converse.LOGOUT = 'logout';
_converse.OPENED = 'opened'; _converse.OPENED = 'opened';
_converse.PREBIND = 'prebind'; _converse.PREBIND = 'prebind';
_converse.IQ_TIMEOUT = 20000; _converse.STANZA_TIMEOUT = 10000;
_converse.CONNECTION_STATUS = { _converse.CONNECTION_STATUS = {
0: 'ERROR', 0: 'ERROR',
...@@ -1694,7 +1700,7 @@ _converse.api = { ...@@ -1694,7 +1700,7 @@ _converse.api = {
* or is rejected when we receive an `error` stanza. * or is rejected when we receive an `error` stanza.
*/ */
sendIQ (stanza, timeout, reject=true) { sendIQ (stanza, timeout, reject=true) {
timeout = timeout || _converse.IQ_TIMEOUT; timeout = timeout || _converse.STANZA_TIMEOUT;
let promise; let promise;
if (reject) { if (reject) {
promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout)); promise = new Promise((resolve, reject) => _converse.connection.sendIQ(stanza, resolve, reject, timeout));
......
...@@ -55,7 +55,6 @@ converse.plugins.add('converse-mam', { ...@@ -55,7 +55,6 @@ converse.plugins.add('converse-mam', {
}); });
const MAMEnabledChat = { const MAMEnabledChat = {
/** /**
* Fetches messages that might have been archived *after* * Fetches messages that might have been archived *after*
* the last archived message in our local cache. * the last archived message in our local cache.
......
This diff is collapsed.
This diff is collapsed.
<div class="modal" tabindex="-1" role="dialog"> <div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header {{{o.type}}}"> <div class="modal-header {{{o.level}}}">
<h5 class="modal-title">{{{o.title}}}</h5> <h5 class="modal-title">{{{o.title}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
......
...@@ -15,27 +15,37 @@ ...@@ -15,27 +15,37 @@
</span> </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__body chat-msg__body--{{{o.type}}} {{{o.received ? 'chat-msg__body--received' : '' }}} {{{o.is_delayed ? 'chat-msg__body--delayed' : '' }}}">
<div class="chat-msg__message"> <div class="chat-msg__message">
{[ if (o.is_spoiler) { ]} {[ if (o.is_retracted) { ]}
<div class="chat-msg__spoiler-hint"> <div>{{{o.retraction_text}}}</div>
<span class="spoiler-hint">{{{o.spoiler_hint}}}</span> {[ if (o.moderation_reason) { ]}<q class="chat-msg--retracted__reason">{{{o.moderation_reason}}}</q>{[ } ]}
<a class="badge badge-info spoiler-toggle" data-toggle-state="closed" href="#"><i class="fa fa-eye"></i>{{{o.label_show}}}</a> {[ } else { ]}
</div> {[ 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> </div>
{[ if (o.received && !o.is_me_message && !o.is_groupchat_message) { ]} <span class="fa fa-check chat-msg__receipt"></span> {[ } ]} {[ 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.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> <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> </div>
</div> </div>
<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) { ...@@ -293,6 +293,8 @@ u.ancestor = function (el, selector) {
* Return the element's siblings until one matches the selector. * Return the element's siblings until one matches the selector.
* @private * @private
* @method u#nextUntil * @method u#nextUntil
* @param { HTMLElement } el
* @param { String } selector
*/ */
u.nextUntil = function (el, selector) { u.nextUntil = function (el, selector) {
const matches = []; const matches = [];
......
...@@ -55,6 +55,7 @@ var specs = [ ...@@ -55,6 +55,7 @@ var specs = [
"spec/user-details-modal", "spec/user-details-modal",
"spec/messages", "spec/messages",
"spec/muc_messages", "spec/muc_messages",
"spec/retractions",
"spec/muc", "spec/muc",
"spec/modtools", "spec/modtools",
"spec/room_registration", "spec/room_registration",
......
...@@ -18,17 +18,13 @@ ...@@ -18,17 +18,13 @@
}); });
converse.initialize({ converse.initialize({
auto_away: 300, auto_away: 300,
auto_login: true,
auto_register_muc_nickname: true, auto_register_muc_nickname: true,
bosh_service_url: 'http://chat.example.org:5380/http-bind/',
debug: true, debug: true,
enable_smacks: true, enable_smacks: true,
i18n: 'en', i18n: 'en',
jid: 'klaus.dresner@chat.example.org',
message_archiving: 'always', message_archiving: 'always',
muc_domain: 'conference.chat.example.org', muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true, muc_respect_autojoin: true,
password: 'secret',
view_mode: 'fullscreen', view_mode: 'fullscreen',
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket', websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
whitelisted_plugins: ['converse-debug'], whitelisted_plugins: ['converse-debug'],
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment