diff --git a/CHANGES.md b/CHANGES.md
index a78b63d88c..2d1f427bd1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,7 @@
## 5.0.4
+- add message-forward (XEP-0297) capabilities
- Bugfix: Don't treat every duplicate message ID as a message correction; since some
clients don't use globally unique ID's this causes false positives.
- #1712: `TypeError: plugin._features is not a function`
diff --git a/locale/converse.pot b/locale/converse.pot
index c3eba95777..54e6780da4 100644
--- a/locale/converse.pot
+++ b/locale/converse.pot
@@ -1892,3 +1892,33 @@ msgstr ""
#: dist/converse-no-dependencies.js:54513
msgid "Re-sync your contacts"
msgstr ""
+
+msgid "You can only send a message to an existing contact or an opened room."
+msgstr ""
+
+msgid "forward this message"
+msgstr ""
+
+msgid "Destination:"
+msgstr ""
+
+msgid "Additional Message:"
+msgstr ""
+
+msgid "Original-Text"
+msgstr ""
+
+msgid "Optional: Add additional message here..."
+msgstr ""
+
+msgid "forward"
+msgstr ""
+
+msgid "Forwarded Message:"
+msgstr ""
+
+msgid "original author:"
+msgstr ""
+
+msgid "time:"
+msgstr ""
diff --git a/locale/de/LC_MESSAGES/converse.po b/locale/de/LC_MESSAGES/converse.po
index 6a5b2b6ecc..c4555897bb 100644
--- a/locale/de/LC_MESSAGES/converse.po
+++ b/locale/de/LC_MESSAGES/converse.po
@@ -2471,3 +2471,33 @@ msgstr "Resynchronisieren Sie Ihre Kontakte"
#~ msgid "Contact username"
#~ msgstr "Benutzername"
+
+msgid "You can only send a message to an existing contact or an opened room."
+msgstr "Sie können eine Nachricht nur an einen existieren Kontakt oder offenen Chatraum senden."
+
+msgid "forward this message"
+msgstr "Nachricht weiterleiten"
+
+msgid "Destination:"
+msgstr "Empfänger:"
+
+msgid "Additional Message:"
+msgstr "Zusätzliche Nachricht"
+
+msgid "Original-Text"
+msgstr "Ursprüngliche Nachricht"
+
+msgid "Optional: Add additional message here..."
+msgstr "Optional: Geben Sie hier eine zusätzliche Nachricht ein..."
+
+msgid "forward"
+msgstr "weiterleiten"
+
+msgid "Forwarded Message:"
+msgstr "Weitergeleitete Nachricht:"
+
+msgid "original author:"
+msgstr "Ursprünglicher Autor:"
+
+msgid "time:"
+msgstr "Zeit:"
\ No newline at end of file
diff --git a/sass/_autocomplete.scss b/sass/_autocomplete.scss
index 4e90e5ab17..6da6216825 100644
--- a/sass/_autocomplete.scss
+++ b/sass/_autocomplete.scss
@@ -10,6 +10,14 @@
.suggestion-box {
width: 100%;
}
+ .forward-message {
+ overflow: visible;
+ border: black;
+ height: 100px;
+ border-radius: 5px;
+ background-color: lightgrey;
+ padding: 5px;
+ }
}
.suggestion-box {
diff --git a/sass/_messages.scss b/sass/_messages.scss
index f43c968944..9c3f194d48 100644
--- a/sass/_messages.scss
+++ b/sass/_messages.scss
@@ -218,8 +218,9 @@
width: 100%;
}
}
-
+
.chat-msg__actions {
+ width: 50px;
.chat-msg__action {
height: var(--message-font-size);
font-size: var(--message-font-size);
@@ -227,10 +228,11 @@
border: none;
opacity: 0;
background: transparent;
+ width: 10px;
cursor: pointer;
- &:focus {
- display: block;
- }
+ display: block;
+ margin: 0px 0px 0px 10px;
+ float: right;
}
}
@@ -310,6 +312,24 @@
margin-right: 0.5em;
color: var(--message-receipt-color);
}
+
+ .forwarded-message {
+ white-space: normal;
+ background-color: lightblue;
+ border-radius: 5px;
+ padding: 5px;
+ margin-left: 5px;
+ }
+
+ .forwarded-message__content {
+ background-color: white;
+ border-radius: 5px;
+ padding-left: 5px;
+ }
+
+ .forwarded-message__header {
+ font-size: 11px;
+ }
}
}
diff --git a/spec/controlbox.js b/spec/controlbox.js
index 70a230723d..2760062ebd 100644
--- a/spec/controlbox.js
+++ b/spec/controlbox.js
@@ -148,7 +148,7 @@
``+
`dnd`+
`0`+
- ``+
+ ``+
``);
const first_child = view.el.querySelector('.xmpp-status span:first-child');
expect(u.hasClass('online', first_child)).toBe(false);
@@ -178,7 +178,7 @@
``+
`I am happy`+
`0`+
- ``+
+ ``+
``);
const first_child = view.el.querySelector('.xmpp-status span:first-child');
diff --git a/spec/messages.js b/spec/messages.js
index 417db6132b..ef69ef792e 100644
--- a/spec/messages.js
+++ b/spec/messages.js
@@ -12,50 +12,6 @@
describe("A Chat Message", function () {
- it("is rejected if it's an unencapsulated forwarded message",
- mock.initConverse(
- null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
- async function (done, _converse) {
-
- await test_utils.waitForRoster(_converse, 'current', 2);
- const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
- const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
- await test_utils.openChatBoxFor(_converse, contact_jid);
- expect(_converse.api.chats.get().length).toBe(2);
- const received_stanza = u.toStanza(`
-
- A most courteous exposition!
-
-
-
- Yet I should kill thee with much cherishing.
-
-
-
-
-
-
- `);
- _converse.connection._dataRecv(test_utils.createRequest(received_stanza));
- const view = _converse.api.chatviews.get(contact_jid);
- const sent_stanzas = _converse.connection.sent_stanzas;
- const sent_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('error')).pop());
- expect(Strophe.serialize(sent_stanza)).toBe(
- ``+
- ''+
- ''+
- ''+
- 'Forwarded messages not part of an encapsulating protocol are not supported'+
- ''+
- '');
- expect(_converse.api.chats.get().length).toBe(2);
- done();
- }));
-
it("can be sent as a correction by clicking the pencil icon",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@@ -82,7 +38,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');
@@ -159,7 +115,7 @@
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
);
await new Promise((resolve, reject) => 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(3);
// Test confirmation dialog
spyOn(window, 'confirm').and.returnValue(true);
diff --git a/spec/muc_messages.js b/spec/muc_messages.js
index 52f1c140f8..538fe7bb4e 100644
--- a/spec/muc_messages.js
+++ b/spec/muc_messages.js
@@ -11,42 +11,6 @@
describe("A Groupchat Message", function () {
- it("is rejected if it's an unencapsulated forwarded message",
- mock.initConverse(
- null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
- async function (done, _converse) {
-
- const muc_jid = 'lounge@montague.lit';
- await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
- const impersonated_jid = `${muc_jid}/alice`;
- const received_stanza = u.toStanza(`
-
-
-
-
- Yet I should kill thee with much cherishing.
-
-
-
- `);
- const view = _converse.api.chatviews.get(muc_jid);
- await view.model.onMessage(received_stanza);
- spyOn(_converse, 'log');
- _converse.connection._dataRecv(test_utils.createRequest(received_stanza));
- expect(_converse.log).toHaveBeenCalledWith(
- 'onMessage: Ignoring unencapsulated forwarded groupchat message',
- Strophe.LogLevel.WARN
- );
- expect(view.el.querySelectorAll('.chat-msg').length).toBe(0);
- expect(view.model.messages.length).toBe(0);
- done();
- }));
-
-
it("is specially marked when you are mentioned in it",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
diff --git a/spec/presence.js b/spec/presence.js
index aad8b2ccff..8da20cf8f2 100644
--- a/spec/presence.js
+++ b/spec/presence.js
@@ -46,7 +46,7 @@
``+
`Hello world`+
`0`+
- ``+
+ ``+
``
);
_converse.priority = 2;
@@ -56,7 +56,7 @@
`away`+
`Going jogging`+
`2`+
- ``+
+ ``+
``
);
@@ -67,7 +67,7 @@
`dnd`+
`Doing taxes`+
`0`+
- ``+
+ ``+
``
);
done();
@@ -95,7 +95,7 @@
.toBe(``+
`My custom status`+
`0`+
- ``+
+ ``+
``)
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true");
@@ -105,7 +105,7 @@
modal.el.querySelector('[type="submit"]').click();
expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString())
.toBe(`dndMy custom status0`+
- ``+
+ ``+
``)
done();
}));
diff --git a/src/converse-forward-message.js b/src/converse-forward-message.js
new file mode 100644
index 0000000000..b6b3cbd19f
--- /dev/null
+++ b/src/converse-forward-message.js
@@ -0,0 +1,341 @@
+// Converse.js
+// https://conversejs.org
+//
+// Copyright (c) 2013-2019, the Converse.js developers
+// Licensed under the Mozilla Public License (MPLv2)
+
+import "backbone.nativeview";
+import "converse-chatboxviews";
+import "converse-message-view";
+import "converse-modal";
+import converse from "@converse/headless/converse-core";
+import tpl_forward_message_modal from "templates/forward_message_modal.html";
+import tpl_forwarded_message_view from "templates/forwarded_message_view.html";
+
+const { $msg, dayjs, _, Strophe, utils, sizzle } = converse.env;
+const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;
+
+converse.plugins.add('converse-forward-message', {
+ /* Plugin dependencies are other plugins which might be
+ * overridden or relied upon, and therefore need to be loaded before
+ * this plugin.
+ *
+ * If the setting "strict_plugin_dependencies" is set to true,
+ * an error will be raised if the plugin is not found. By default it's
+ * false, which means these plugins are only loaded opportunistically.
+ *
+ * NB: These plugins need to have already been loaded via require.js.
+ */
+ dependencies: ["converse-chatview", "converse-message-view"],
+
+ overrides: {
+ MessageView: {
+ initialize () {
+ this.__super__.initialize.apply(this, arguments);
+ this.model.collection.on('rendered', this.renderForwardedMessage, this);
+ },
+ },
+
+ ChatBoxView: {
+ events: {
+ 'click .chat-msg__action-forward': 'onMessageForwardClicked',
+ },
+ },
+
+ ChatRoomView: {
+ events: {
+ 'click .chat-msg__action-forward': 'onMessageForwardClicked',
+ },
+
+ onMessageForwardClicked (ev) {
+ const { _converse } = this.__super__;
+ this.add_forward_modal = new _converse.AddForwardMessageModal(this.model, ev);
+ this.add_forward_modal.show(ev);
+ }
+ },
+
+ ChatBox: {
+ async getMessageAttributesFromStanza (stanza, original_stanza) {
+ let forwarded_attrs = [];
+
+ const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, original_stanza).length;
+ if (bare_forward) {
+ const forwarded_message_stanzas = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza);
+ const forwarded_message = forwarded_message_stanzas.pop();
+ const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, forwarded_message).pop();
+ // read all the attribtues from the forwarded-message and add them to the attrs-variable
+ forwarded_attrs = Object.assign({
+ 'is_forwarded_message': true,
+ 'original_time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
+ 'original_id': forwarded_message.querySelector('message').getAttribute('id'),
+ 'original_message': this.getMessageBody(forwarded_message),
+ 'original_from': forwarded_message.querySelector('message').getAttribute('from'),
+ 'original_to': forwarded_message.querySelector('message').getAttribute('to'),
+ 'original_type': forwarded_message.querySelector('message').getAttribute('type')
+ });
+
+ // remove forwarded message from stanza
+ // prevents reading the wrong values from the inner message inside the base-method
+ forwarded_message.parentElement.removeChild(forwarded_message);
+ }
+
+ // call base function to read attributes from stanza without forwarded message
+ // this allows reuse of the base-method, without code duplication
+ const attrs = await this.__super__.getMessageAttributesFromStanza.apply(this, arguments);
+ return Object.assign(attrs, forwarded_attrs);
+ },
+ }
+ },
+
+ initialize () {
+ /* The initialize function gets called as soon as the plugin is
+ * loaded by converse.js's plugin machinery.
+ */
+ const { _converse } = this,
+ { __ } = _converse;
+
+ const clickForwardMessage = {
+ onMessageForwardClicked (ev) {
+ const { _converse } = this.__super__;
+ this.add_forward_modal = new _converse.AddForwardMessageModal(this.model, ev);
+ this.add_forward_modal.show(ev);
+ },
+ };
+ Object.assign(_converse.ChatBoxView.prototype, clickForwardMessage);
+
+ const renderForwardedMessages = {
+ renderForwardedMessage (message) {
+ this.renderForwardButton(message);
+
+ if (!message.model.get('is_forwarded_message') || message.el.querySelector('.forwarded-message__content')) {
+ return;
+ }
+
+ const forwarded_message_element = this.createForwardedMessageHtmlElement(message.model);
+ // add msg content as innerText to preserve line endings
+ const text_content = forwarded_message_element.querySelector('.forwarded-message__content');
+ text_content.innerText = message.model.get('original_message');
+
+ // do not use await for this function call because then the forwarded message will be displayed
+ // many times in the chat-history
+ this.renderImageIfPresent(forwarded_message_element);
+ const msg_content = message.el.querySelector('.chat-msg__text');
+ msg_content.insertAdjacentElement('beforeend', forwarded_message_element);
+ },
+
+ renderForwardButton (message) {
+ const element = message.el.querySelector('.chat-msg__actions');
+ if (!_.isNil(element) && !message.el.querySelector('.chat-msg__action-forward')) {
+ element.insertAdjacentHTML('beforeend', '');
+ }
+ },
+
+ createForwardedMessageHtmlElement (model) {
+ const time = dayjs(model.get('original_time'));
+ return utils.stringToElement(tpl_forwarded_message_view(
+ Object.assign(
+ model.toJSON(), {
+ '__': __,
+ 'original_form': model.get('original_from'),
+ 'original_to': model.get('orginal_to'),
+ 'original_time': time.format('DD.MM.YYYY hh:mm'),
+ 'original_type': model.get('original_type')
+ }
+ )
+ ));
+ },
+
+ async renderImageIfPresent (forwarded_message_element) {
+ const forwarded_message = forwarded_message_element.querySelector('.forwarded-message__content');
+ if (forwarded_message.textContent.match(URL_REGEX)) {
+ // order of the calls below is important
+ forwarded_message.innerHTML = await this.transformBodyText(forwarded_message.textContent);
+ forwarded_message.innerHTML = await this.transformOOBURL(forwarded_message.textContent);
+ await utils.renderImageURLs(_converse, forwarded_message_element);
+ }
+ }
+ };
+ Object.assign(_converse.MessageView.prototype, renderForwardedMessages);
+
+ _converse.AddForwardMessageModal = _converse.BootstrapModal.extend({
+ events: {
+ 'submit form.forward-message-form': 'forwardMessage'
+ },
+
+ initialize (chat_model, target_element) {
+ this.message = this.getMessageTextFromTargetElement(target_element, chat_model);
+ this.model = chat_model;
+ _converse.BootstrapModal.prototype.initialize.apply(this, chat_model);
+ },
+
+ getMessageTextFromTargetElement (target_element, chat_model) {
+ const message_action_menu_element = target_element.target.parentElement;
+ const message_body_element = message_action_menu_element.parentElement;
+ const message_element = message_body_element.parentElement.parentElement;
+ return chat_model.messages.findWhere({'msgid': message_element.getAttribute('data-msgid')});
+ },
+
+ toHTML () {
+ return tpl_forward_message_modal(Object.assign(this.model.toJSON(), {
+ '__': __
+ }));
+ },
+
+ afterRender () {
+ const text_element = this.el.querySelector('.forward-message');
+ text_element.innerText = this.message.get('message');
+
+ this.el.addEventListener('shown.bs.modal', () => {
+ this.el.querySelector('input[name="receiver"]').focus();
+ }, false);
+
+ this.addAutocomplete();
+ },
+
+ addAutocomplete () {
+ const contacts = _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
+ let open_rooms;
+ if (_converse.rooms_list_view.model) {
+ open_rooms = _converse.rooms_list_view.model.map(i => ({'label': i.get('name'), 'value': i.get('jid')}));
+ }
+ const suggestion_list = contacts.concat(open_rooms);
+
+ if (this.invite_auto_complete) {
+ this.invite_auto_complete.destroy();
+ }
+ this.invite_auto_complete = new _converse.AutoComplete(this.el, {
+ 'min_chars': 1,
+ 'list': suggestion_list
+ });
+
+ // prevents suggestion-element to be displayed on load
+ const suggestion_element = this.el.querySelector('.suggestion-box__results');
+ suggestion_element.hidden = true;
+ },
+
+ forwardMessage (ev) {
+ ev.preventDefault();
+ const form_data = this.getJidFromModalForm(ev.target);
+
+ if (!this.isJidOpenMuc(form_data.receiver) && !this.isJidExistingContact(form_data.receiver)) {
+ alert(__("You can only send a message to an existing contact or an opened room."));
+ return;
+ }
+
+ this.modal.hide();
+ ev.target.reset();
+ this.send(form_data);
+ },
+
+ getJidFromModalForm (form) {
+ const data = new FormData(form);
+ const receiver = data.get('receiver');
+ const additional_message = data.get('additional_message');
+ const original_message = data.get('original_message');
+ return {
+ 'receiver': receiver,
+ 'additional_message': additional_message,
+ 'original_message': original_message
+ };
+ },
+
+ isJidOpenMuc (jid) {
+ const rooms = _converse.rooms_list_view.model.models.find(function (model) {
+ return model.get('jid') === jid;
+ });
+ return rooms !== undefined;
+ },
+
+ isJidExistingContact (jid) {
+ const contact = _converse.roster.models.find(function (model) {
+ return model.get('jid') === jid;
+ });
+ return contact !== undefined;
+ },
+
+ send (form_data) {
+ const msg_id = _converse.connection.getUniqueId();
+
+ const message = $msg({
+ 'from': _converse.connection.jid,
+ 'to': form_data.receiver,
+ 'type': this.getChatType(form_data),
+ 'id': msg_id
+ }).c('body').t(form_data.additional_message).up();
+
+ message.c('forwarded', {'xmlns': Strophe.NS.FORWARD})
+ .c('delay', {'xmlns': Strophe.NS.DELAY, 'stamp': this.message.get('time')}).up();
+
+ message.c('message', {
+ 'from': this.message.get('from'),
+ 'to': this.model.get('jid'),
+ 'id': this.message.get('id'),
+ 'type': this.message.get('type'),
+ 'xmlns': 'jabber:client'
+ }).c('body').t(this.message.get('message')).up().root();
+
+ message.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root();
+
+ _converse.api.send(message);
+ this.addForwardedMessageToChatHistory(form_data, msg_id);
+ },
+
+ getChatType (form_data) {
+ if (this.isJidOpenMuc(form_data.receiver)) {
+ return 'groupchat';
+ } else {
+ return 'chat';
+ }
+ },
+
+ async addForwardedMessageToChatHistory (form_data, msg_id) {
+ if (!this.isJidExistingContact(form_data.receiver)) {
+ return;
+ }
+
+ let chat = _converse.api.chats.get(form_data.receiver);
+ if (_.isNil(chat)) {
+ await _converse.api.chats.create(form_data.receiver, {'minimized': true});
+ chat = _converse.api.chats.get(form_data.receiver);
+ chat.save({'num_unread': chat.get('num_unread') + 1});
+ _converse.incrementMsgCounter();
+ } else {
+ const attrs = Object.assign({
+ 'is_archived': false,
+ 'is_delayed': false,
+ 'is_spoiler': false,
+ 'is_single_emoji': false,
+ 'message': form_data.additional_message,
+ 'msgid': msg_id,
+ 'type': this.getChatType(form_data),
+ 'is_forwarded_message': true,
+ 'original_time': this.message.get('time'),
+ 'original_id': this.message.get('id'),
+ 'original_message': this.message.get('message'),
+ 'original_from': this.message.get('from'),
+ 'original_to': this.message.get('to'),
+ 'original_type': this.message.get('type'),
+ });
+
+ attrs.from = _converse.bare_jid;
+ if (attrs.type === 'groupchat') {
+ attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
+ attrs.sender = attrs.nick === this.get('nick') ? 'me': 'them';
+ } else {
+ if (attrs.from === _converse.bare_jid) {
+ attrs.sender = 'me';
+ attrs.fullname = _converse.xmppstatus.get('fullname');
+ } else {
+ attrs.sender = 'them';
+ attrs.fullname = this.get('fullname')
+ }
+ }
+ const msg = chat.messages.create(attrs);
+ chat.incrementUnreadMsgCounter(msg);
+ }
+ }
+ });
+
+ _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.FORWARD));
+ }
+});
diff --git a/src/converse.js b/src/converse.js
index 496b23cccc..a58e69b20f 100644
--- a/src/converse.js
+++ b/src/converse.js
@@ -21,6 +21,7 @@ import "converse-push"; // XEP-0357 Push Notifications
import "converse-register"; // XEP-0077 In-band registration
import "converse-roomslist"; // Show currently open chat rooms
import "converse-rosterview";
+import "converse-forward-message"; // allows to redirect messages to other users or MUCs (XEP-0297)
import "converse-singleton";
import "converse-uniview";
/* END: Removable components */
@@ -51,6 +52,7 @@ const WHITELISTED_PLUGINS = [
'converse-register',
'converse-roomslist',
'converse-rosterview',
+ 'converse-forward-message',
'converse-singleton',
'converse-uniview'
];
diff --git a/src/headless/converse-chatboxes.js b/src/headless/converse-chatboxes.js
index c5ea4e3bac..068701b553 100644
--- a/src/headless/converse-chatboxes.js
+++ b/src/headless/converse-chatboxes.js
@@ -937,8 +937,7 @@ converse.plugins.add('converse-chatboxes', {
}
}
},
-
-
+
/**
* Parses a passed in message stanza and returns an object
* of attributes.
@@ -1191,13 +1190,6 @@ converse.plugins.add('converse-chatboxes', {
);
}
- const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
- if (bare_forward) {
- return this.rejectMessage(
- stanza,
- 'Forwarded messages not part of an encapsulating protocol are not supported'
- );
- }
let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
const is_carbon = u.isCarbonMessage(stanza);
if (is_carbon) {
diff --git a/src/headless/converse-muc.js b/src/headless/converse-muc.js
index 2658cdf457..2d696661f5 100644
--- a/src/headless/converse-muc.js
+++ b/src/headless/converse-muc.js
@@ -1536,13 +1536,6 @@ converse.plugins.add('converse-muc', {
*/
async onMessage (stanza) {
const original_stanza = stanza;
- const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
- if (bare_forward) {
- return _converse.log(
- 'onMessage: Ignoring unencapsulated forwarded groupchat message',
- Strophe.LogLevel.WARN
- );
- }
const is_carbon = u.isCarbonMessage(stanza);
if (is_carbon) {
// XEP-280: groupchat messages SHOULD NOT be carbon copied, so we're discarding it.
@@ -1709,7 +1702,6 @@ converse.plugins.add('converse-muc', {
}
},
-
/**
* Parses a stanza with type "error" and sets the proper
* `connection_status` value for this {@link _converse.ChatRoom} as
diff --git a/src/headless/utils/core.js b/src/headless/utils/core.js
index 60fe1c3750..e1e97e3e45 100644
--- a/src/headless/utils/core.js
+++ b/src/headless/utils/core.js
@@ -128,6 +128,7 @@ u.isEmptyMessage = function (attrs) {
}
return !attrs['oob_url'] &&
!attrs['file'] &&
+ !attrs['is_forwarded_message'] &&
!(attrs['is_encrypted'] && attrs['plaintext']) &&
!attrs['message'];
};
diff --git a/src/templates/forward_message_modal.html b/src/templates/forward_message_modal.html
new file mode 100644
index 0000000000..88a1637951
--- /dev/null
+++ b/src/templates/forward_message_modal.html
@@ -0,0 +1,34 @@
+
+
+
+
+
{{{o.__('forward this message')}}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/templates/forwarded_message_view.html b/src/templates/forwarded_message_view.html
new file mode 100644
index 0000000000..be6d079500
--- /dev/null
+++ b/src/templates/forwarded_message_view.html
@@ -0,0 +1,6 @@
+
+