diff --git a/CHANGES.md b/CHANGES.md index ff054a503..fd9e023ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Move the `converse-oauth` plugin to the [community-plugins](https://github.com/conversejs/community-plugins) - Don't apply message corrections when the MUC occupant-id doesn't match. - Update `nick` attribute on ChatRoom when user nickname changes +- Restrict editing of MUC messages to ones with the same XEP-0421 occupant ID - #2936: Fix documentation about enable_smacks option, which is true by default. ## 9.1.1 (2022-05-05) diff --git a/src/headless/plugins/chat/model.js b/src/headless/plugins/chat/model.js index 3360ddd6c..31c4df181 100644 --- a/src/headless/plugins/chat/model.js +++ b/src/headless/plugins/chat/model.js @@ -234,7 +234,7 @@ const ChatBox = ModelWithContact.extend({ this.notifications.set('chat_state', attrs.chat_state); } if (u.shouldCreateMessage(attrs)) { - const msg = handleCorrection(this, attrs) || await this.createMessage(attrs); + const msg = await handleCorrection(this, attrs) || await this.createMessage(attrs); this.notifications.set({'chat_state': null}); this.handleUnreadMessage(msg); } diff --git a/src/headless/plugins/muc/muc.js b/src/headless/plugins/muc/muc.js index d7ee88a73..940731c33 100644 --- a/src/headless/plugins/muc/muc.js +++ b/src/headless/plugins/muc/muc.js @@ -2280,7 +2280,7 @@ const ChatRoomMixin = { this.updateNotifications(attrs.nick, attrs.chat_state); } if (u.shouldCreateGroupchatMessage(attrs)) { - const msg = handleCorrection(this, attrs) || (await this.createMessage(attrs)); + const msg = await handleCorrection(this, attrs) || (await this.createMessage(attrs)); this.removeNotification(attrs.nick, ['composing', 'paused']); this.handleUnreadMessage(msg); } diff --git a/src/headless/plugins/muc/parsers.js b/src/headless/plugins/muc/parsers.js index 6fc52fe60..c78c6dc6a 100644 --- a/src/headless/plugins/muc/parsers.js +++ b/src/headless/plugins/muc/parsers.js @@ -245,12 +245,15 @@ export async function parseMUCMessage (stanza, chatbox) { const from_real_jid = attrs.is_archived && getJIDFromMUCUserData(stanza, attrs) || chatbox.occupants.findOccupant(attrs)?.get('jid'); + const own_occupant_id = chatbox.get('occupant_id'); + const is_me = attrs.occupant_id && own_occupant_id ? own_occupant_id === attrs.occupant_id : attrs.nick === chatbox.get('nick'); + attrs = Object.assign( { from_real_jid, 'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false, 'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs), - 'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead - 'sender': attrs.nick === chatbox.get('nick') ? 'me' : 'them', + 'message': attrs.body || attrs.error, // TODO: Should only be used for error and info messages + 'sender': is_me ? 'me' : 'them', }, attrs); if (attrs.is_archived && original_stanza.getAttribute('from') !== attrs.from_muc) { diff --git a/src/headless/shared/chat/utils.js b/src/headless/shared/chat/utils.js index bf255501b..5f4e6d894 100644 --- a/src/headless/shared/chat/utils.js +++ b/src/headless/shared/chat/utils.js @@ -72,7 +72,7 @@ export function getMediaURLs (arr, text, offset=0) { * @returns { _converse.Message|undefined } Returns the corrected * message or `undefined` if not applicable. */ -export function handleCorrection (model, attrs) { +export async function handleCorrection (model, attrs) { if (!attrs.replace_id || !attrs.from) { return; } @@ -84,7 +84,8 @@ export function handleCorrection (model, attrs) { const message = model.messages.models.find(query); if (!message) { - return; + attrs['older_versions'] = []; + return await model.createMessage(attrs); // eslint-disable-line no-return-await } const older_versions = message.get('older_versions') || {}; diff --git a/src/plugins/muc-views/tests/corrections.js b/src/plugins/muc-views/tests/corrections.js index 670d67bcd..d0c51dba4 100644 --- a/src/plugins/muc-views/tests/corrections.js +++ b/src/plugins/muc-views/tests/corrections.js @@ -296,10 +296,11 @@ describe('A Groupchat Message XEP-0308 correction ', function () { from="lounge@montague.lit/newguy" to="_converse.connection.jid" type="groupchat" - id="${msg_id}"> + id="${u.getUniqueId()}"> But soft, what light through yonder chimney breaks? + ` ); @@ -309,7 +310,7 @@ describe('A Groupchat Message XEP-0308 correction ', function () { expect(model.messages.at(0).get('edited')).toBeFalsy(); expect(model.messages.at(1).get('body')).toBe('But soft, what light through yonder chimney breaks?'); - expect(model.messages.at(1).get('edited')).toBeFalsy(); + expect(model.messages.at(1).get('edited')).toBeTruthy(); await model.handleMessageStanza( stx` @@ -317,9 +318,10 @@ describe('A Groupchat Message XEP-0308 correction ', function () { from="lounge@montague.lit/newguy" to="_converse.connection.jid" type="groupchat" - id="${msg_id}"> + id="${u.getUniqueId()}"> But soft, what light through yonder hatch breaks? + ` ); @@ -329,13 +331,91 @@ describe('A Groupchat Message XEP-0308 correction ', function () { expect(model.messages.at(0).get('edited')).toBeFalsy(); expect(model.messages.at(1).get('body')).toBe('But soft, what light through yonder chimney breaks?'); - expect(model.messages.at(1).get('edited')).toBeFalsy(); + expect(model.messages.at(1).get('edited')).toBeTruthy(); expect(model.messages.at(2).get('body')).toBe('But soft, what light through yonder hatch breaks?'); - expect(model.messages.at(2).get('edited')).toBeFalsy(); + expect(model.messages.at(2).get('edited')).toBeTruthy(); const message_els = Array.from(view.querySelectorAll('.chat-msg')); expect(message_els.reduce((acc, m) => acc && u.hasClass('chat-msg--followup', m), true)).toBe(false); }) ); + + it( + "cannot be edited if it's from a different occupant id", + mock.initConverse([], {}, async function (_converse) { + const nick = 'romeo'; + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.OCCUPANTID]; + const model = await mock.openAndEnterChatRoom(_converse, muc_jid, nick, features); + + expect(model.get('occupant_id')).toBe(model.occupants.at(0).get('occupant_id')); + + const msg_id = u.getUniqueId(); + await model.handleMessageStanza( + stx` + + + But soft, what light through yonder airlock breaks? + + ` + ); + + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.querySelectorAll('.chat-msg').length); + expect(model.messages.at(0).get('body')).toBe('But soft, what light through yonder airlock breaks?'); + + await model.handleMessageStanza( + stx` + + + But soft, what light through yonder chimney breaks? + + + ` + ); + + expect(model.messages.at(0).get('body')).toBe('But soft, what light through yonder chimney breaks?'); + expect(model.messages.at(0).get('edited')).toBeTruthy(); + + await model.handleMessageStanza( + stx` + + + But soft, what light through yonder hatch breaks? + + + ` + ); + + await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 2); + expect(model.messages.length).toBe(2); + expect(model.messages.at(0).get('body')).toBe('But soft, what light through yonder chimney breaks?'); + expect(model.messages.at(0).get('edited')).toBeTruthy(); + expect(model.messages.at(0).get('editable')).toBeTruthy(); + + expect(model.messages.at(1).get('body')).toBe('But soft, what light through yonder hatch breaks?'); + expect(model.messages.at(1).get('edited')).toBeTruthy(); + expect(model.messages.at(1).get('editable')).toBeFalsy(); + + const message_els = Array.from(view.querySelectorAll('.chat-msg')); + expect(message_els.reduce((acc, m) => acc && u.hasClass('chat-msg--followup', m), true)).toBe(false); + + // We can edit our own message, but not the other + expect(message_els[0].querySelector('converse-dropdown .chat-msg__action-edit')).toBeDefined(); + expect(message_els[1].querySelector('converse-dropdown .chat-msg__action-edit')).toBe(null); + }) + ); }); diff --git a/src/shared/components/message-versions.js b/src/shared/components/message-versions.js index 2af7634b2..d97e80996 100644 --- a/src/shared/components/message-versions.js +++ b/src/shared/components/message-versions.js @@ -1,9 +1,13 @@ import { CustomElement } from './element.js'; import { api, converse } from '@converse/headless/core'; import { html } from 'lit'; +import { __ } from 'i18n'; +import './styles/message-versions.scss'; const { dayjs } = converse.env; +const tpl_older_version = (k, older_versions) => html`

: ${older_versions[k]}

`; + export class MessageVersions extends CustomElement { @@ -15,13 +19,15 @@ export class MessageVersions extends CustomElement { render () { const older_versions = this.model.get('older_versions'); + const keys = Object.keys(older_versions); return html` -

Older versions

- ${ Object.keys(older_versions).map( - k => html`

: ${older_versions[k]}

`) } + ${ keys.length ? + html`

${__('Older versions')}

${keys.map(k => tpl_older_version(k, older_versions))}` : + html`

${__('No older versions found')}

` + }
-

Current version

-

${this.model.getMessageText()}

`; +

${__('Current version')}

+

: ${this.model.getMessageText()}

`; } } diff --git a/src/shared/components/styles/message-versions.scss b/src/shared/components/styles/message-versions.scss new file mode 100644 index 000000000..407b4eb8a --- /dev/null +++ b/src/shared/components/styles/message-versions.scss @@ -0,0 +1,7 @@ +.conversejs { + converse-message-versions { + time { + font-weight: bold; + } + } +} diff --git a/src/shared/styles/messages.scss b/src/shared/styles/messages.scss index e8aa5b978..94b09ab70 100644 --- a/src/shared/styles/messages.scss +++ b/src/shared/styles/messages.scss @@ -3,12 +3,6 @@ color: var(--subdued-color); } - .older-msg { - time { - font-weight: bold; - } - } - .message { .show-msg-author-modal { align-self: flex-start; // Don't expand height to that of largest sibling