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 @@ + \ 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 @@ + +
+ {{{o.__('Forwarded Message:')}}} +
{{{ o.original_message }}}
+ {{{o.__('original author:')}}} {{{ o.original_from }}} ({{{o.__('time:')}}} {{{ o.original_time }}}) +
diff --git a/src/templates/message.html b/src/templates/message.html index 127b94f074..e07ba0a89a 100644 --- a/src/templates/message.html +++ b/src/templates/message.html @@ -31,11 +31,11 @@ {[ if (o.received && !o.is_me_message && !o.is_groupchat_message) { ]} {[ } ]} {[ if (o.edited) { ]} {[ } ]} - {[ if (o.type !== 'headline' && o.sender === 'me') { ]} -
+
+ {[ if (o.type !== 'headline' && o.sender === 'me') { ]} -
- {[ } ]} + {[ } ]} +