Skip to content

Commit

Permalink
Create a more minimalist retraction message
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Feb 7, 2025
1 parent 3951a7c commit 147be37
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 55 deletions.
8 changes: 8 additions & 0 deletions src/headless/plugins/chat/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) {
return text.startsWith('/me ');
}

/**
* @returns {boolean}
*/
isRetracted () {
return this.get('retracted') || this.get('moderated') === 'retracted';
}

/**
* Returns a boolean indicating whether this message is considered a followup
* message from the previous one. Followup messages are shown grouped together
Expand All @@ -161,6 +168,7 @@ class Message extends ModelWithContact(ColorAwareModel(Model)) {
}
const date = dayjs(this.get('time'));
return this.get('from') === prev_model.get('from') &&
!this.isRetracted() && !prev_model.isRetracted() &&
!this.isMeCommand() && !prev_model.isMeCommand() &&
!!this.get('is_encrypted') === !!prev_model.get('is_encrypted') &&
this.get('type') === prev_model.get('type') && this.get('type') !== 'info' &&
Expand Down
4 changes: 4 additions & 0 deletions src/headless/types/plugins/chat/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ declare class Message extends Message_base {
* @returns {boolean}
*/
isMeCommand(): boolean;
/**
* @returns {boolean}
*/
isRetracted(): boolean;
/**
* Returns a boolean indicating whether this message is considered a followup
* message from the previous one. Followup messages are shown grouped together
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/muc-views/templates/mep-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default (el) => {
<div class="chat-msg__content">
<div class="chat-msg__body chat-msg__body--${el.model.get('type')} ${el.model.get('is_delayed') ? 'chat-msg__body--delayed' : '' }">
<div class="chat-info__message">
${ el.isRetracted() ? el.renderRetraction() : html`
${ el.model.isRetracted() ? el.renderRetraction() : html`
<converse-texture
.mentions=${el.model.get('references')}
render_styling
Expand All @@ -25,7 +25,7 @@ export default (el) => {
`}
</div>
<converse-message-actions
?is_retracted=${el.isRetracted()}
?is_retracted=${el.model.isRetracted()}
.model=${el.model}></converse-message-actions>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/muc-views/tests/mep.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ describe("A XEP-0316 MEP notification", function () {
expect(view.model.messages.at(0).get('moderation_reason')).toBeUndefined;
expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false);
expect(view.model.messages.at(0).get('editable')).toBe(false);
const msg_el = view.querySelector('.chat-msg--retracted .chat-info__message div');
expect(msg_el.textContent).toBe(`${nick} has removed this message`);
const msg_el = view.querySelector('.chat-msg--retracted .chat-info__message .retraction');
expect(msg_el.firstElementChild.textContent).toBe(`${nick} has removed a message`);
}));
});
38 changes: 19 additions & 19 deletions src/plugins/muc-views/tests/retractions.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ describe("Message Retractions", function () {
expect(view.model.messages.at(0).get('retracted')).toBeTruthy();
expect(view.model.messages.at(0).get('editable')).toBe(false);
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
expect(msg_el.textContent.trim()).toBe('eve has removed this message');
const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(msg_el?.textContent.trim()).toBe('eve has removed a message');
expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null);
}));

Expand Down Expand Up @@ -314,10 +314,10 @@ describe("Message Retractions", function () {
expect(view.model.messages.at(0).get('editable')).toBe(false);
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);

const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
const ret_el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(ret_el.firstElementChild.textContent.trim()).toBe('romeo has removed a message');

const qel = msg_el.querySelector('q');
const qel = ret_el.querySelector('q');
expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!');

// The server responds with a retraction message
Expand Down Expand Up @@ -421,8 +421,8 @@ describe("Message Retractions", function () {
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('moderated')).toBe('retracted');
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(msg_el.textContent).toBe('romeo has removed this message');
const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed a message');
const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q');
expect(qel.textContent).toBe('This content is inappropriate for this forum!');

Expand Down Expand Up @@ -495,8 +495,8 @@ describe("Message Retractions", function () {
expect(view.model.messages.last().get('editable')).toBe(false);
expect(message.get(`stanza_id ${muc_jid}`)).toBe(stanza_id);
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent).toBe('You have removed this message');
const el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(el?.textContent.trim()).toBe('You have removed a message');
}));

it("can be retracted by its author, causing an error message in response",
Expand All @@ -516,8 +516,8 @@ describe("Message Retractions", function () {

expect(view.model.messages.length).toBe(1);
await u.waitUntil(() => view.model.messages.last().get('retracted'), 1000);
const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('You have removed this message');
const el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(el?.textContent.trim()).toBe('You have removed a message');

const message = view.model.messages.last();
const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`);
Expand Down Expand Up @@ -562,8 +562,8 @@ describe("Message Retractions", function () {
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.last().get('retracted')).toBeTruthy();
await u.waitUntil(() => view.querySelectorAll('.chat-msg--retracted').length === 1);
const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('You have removed this message');
const el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(el?.textContent.trim()).toBe('You have removed a message');

await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);

Expand Down Expand Up @@ -641,7 +641,7 @@ describe("Message Retractions", function () {
const occupant = view.model.getOwnOccupant();
expect(occupant.get('role')).toBe('moderator');

view.model.sendMessage({'body': 'Visit this site to get free bitcoin'});
view.model.sendMessage({body: 'Visit this site to get free bitcoin'});
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 1);

// Check that you can only edit a message before it's been
Expand Down Expand Up @@ -702,7 +702,7 @@ describe("Message Retractions", function () {
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);

const msg_el = view.querySelector('.chat-msg--retracted .chat-msg__message');
expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message');
expect(msg_el.querySelector('.retraction')?.textContent.trim()).toBe('romeo has removed a message');
expect(msg_el.querySelector('q')).toBe(null);

// The server responds with a retraction message
Expand Down Expand Up @@ -801,8 +801,8 @@ describe("Message Retractions", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('eve has removed this message');
const el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(el?.textContent.trim()).toBe('eve has removed a message');
}));

it("may be returned as a tombstone moderated groupchat message",
Expand Down Expand Up @@ -888,8 +888,8 @@ describe("Message Retractions", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(1);

expect(view.querySelectorAll('.chat-msg--retracted').length).toBe(1);
const el = view.querySelector('.chat-msg--retracted .chat-msg__message div');
expect(el.textContent.trim()).toBe('A moderator has removed this message');
const el = view.querySelector('.chat-msg--retracted .chat-msg__message .retraction');
expect(el.firstElementChild.textContent.trim()).toBe('A moderator has removed a message');
const qel = view.querySelector('.chat-msg--retracted .chat-msg__message q');
expect(qel.textContent.trim()).toBe('This message contains inappropriate content');
}));
Expand Down
15 changes: 6 additions & 9 deletions src/shared/chat/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ export default class Message extends CustomElement {
this.parentElement.removeChild(this);
}

isRetracted () {
return this.model.get('retracted') || this.model.get('moderated') === 'retracted';
}

hasMentions () {
const is_groupchat = this.model.get('type') === 'groupchat';
return is_groupchat && this.model.get('sender') === 'them' && this.model_with_messages.isUserMentioned(this.model);
Expand All @@ -141,11 +137,12 @@ export default class Message extends CustomElement {
}

getExtraMessageClasses () {
const is_action = this.model.isMeCommand() || this.model.isRetracted();
const extra_classes = [
this.model.isFollowup() ? 'chat-msg--followup' : null,
this.model.get('is_delayed') ? 'delayed' : null,
this.model.isMeCommand() ? 'chat-msg--action' : null,
this.isRetracted() ? 'chat-msg--retracted' : null,
is_action ? 'chat-msg--action' : null,
this.model.isRetracted() ? 'chat-msg--retracted' : null,
this.model.get('type'),
this.shouldShowAvatar() ? 'chat-msg--with-avatar' : null,
].map(c => c);
Expand All @@ -171,11 +168,11 @@ export default class Message extends CustomElement {
occupants.findOccupant({'nick': Strophe.getResourceFromJid(retracted_by_mod)});
}
const modname = this.model.mod ? this.model.mod.getDisplayName() : __('A moderator');
return __('%1$s has removed this message', modname);
return __('%1$s has removed a message', modname);
} else {
return this.model.get('sender') === 'me' ?
__('You have removed this message') :
__('%1$s has removed this message', this.model.getDisplayName());
__('You have removed a message') :
__('%1$s has removed a message', this.model.getDisplayName());
}
}

Expand Down
31 changes: 16 additions & 15 deletions src/shared/chat/templates/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default (el) => {
const is_first_unread = el.model_with_messages.get('first_unread_id') === el.model.get('id');
const is_followup = el.model.isFollowup();
const is_me_message = el.model.isMeCommand();
const is_retracted = el.isRetracted();
const is_retracted = el.model.isRetracted();
const msgid = el.model.get('msgid');
const sender = el.model.get('sender');
const time = el.model.get('time');
Expand All @@ -30,7 +30,11 @@ export default (el) => {
const pretty_time = dayjs(edited || time).format(format);
const hats = getHats(el.model);
const username = el.model.getDisplayName();
const should_show_avatar = el.shouldShowAvatar();

const is_action = is_me_message || is_retracted;
debugger;
const should_show_header = !is_action && !is_followup;
const should_show_avatar = el.shouldShowAvatar() && should_show_header;

// The model to use for the avatar.
// Note: it can happen that the contact has not the vcard attribute but the message has.
Expand All @@ -52,7 +56,7 @@ export default (el) => {
<!-- Anchor to allow us to scroll the message into view -->
<a id="${msgid}"></a>
${should_show_avatar && !is_followup
${should_show_avatar
? html`<a class="show-msg-author-modal" @click=${el.showUserModal}>
<converse-avatar
.model=${avatar_model}
Expand All @@ -65,12 +69,8 @@ export default (el) => {
</a>`
: ''}
<div
class="chat-msg__content chat-msg__content--${sender} ${is_me_message
? 'chat-msg__content--action'
: ''}"
>
${!is_me_message && !is_followup
<div class="chat-msg__content chat-msg__content--${sender} ${is_action ? 'chat-msg__content--action' : ''}">
${should_show_header
? html` <span class="chat-msg__heading">
<span class="chat-msg__author">
<a class="show-msg-author-modal" @click=${el.showUserModal} style="${author_style}"
Expand All @@ -91,12 +91,13 @@ export default (el) => {
: ''} ${el.model.get('is_delayed') ? 'chat-msg__body--delayed' : ''}"
>
<div class="chat-msg__message">
${is_me_message
? html` <time timestamp="${edited || time}" class="chat-msg__time">${pretty_time}</time
>&nbsp;
<span class="chat-msg__author" style="${author_style}"
>${is_me_message ? '**' : ''}${username}</span
>&nbsp;`
${is_action
? html`<time timestamp="${edited || time}" class="chat-msg__time">${pretty_time}</time>
${is_me_message
? html`<span class="chat-msg__author" style="${author_style}"
>${is_me_message ? '**' : ''}${username}</span
>&nbsp;`
: ''}`
: ''}
${is_retracted ? el.renderRetraction() : el.renderMessageText()}
</div>
Expand Down
19 changes: 13 additions & 6 deletions src/shared/chat/templates/retraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { html } from 'lit';

import '../styles/retraction.scss';

/**
* @param {import('shared/chat/message').default} el
*/
export default (el) => {
const retraction_text = el.isRetracted() ? el.getRetractionText() : null;
return html`
<div class="retraction">${retraction_text}</div>
${ el.model.get('moderation_reason') ?
html`<q class="chat-msg--retracted__reason">${el.model.get('moderation_reason')}</q>` : '' }`;
}
const retraction_text = el.model.isRetracted() ? el.getRetractionText() : null;
return html`<span class="retraction">
<span>${retraction_text}</span>
${el.model.get('moderation_reason')
? html`<q class="chat-msg--retracted__reason"
>${el.model.get('moderation_reason')}</q
>`
: ''}
</span>`;
};
6 changes: 6 additions & 0 deletions src/shared/styles/messages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,14 @@
font-size: var(--message-font-size);
}
.chat-msg__time {
margin-inline-end: 0.5em;
margin-inline-start: 0;
}

.retraction {
display: flex;
flex-direction: column;
}
}

.chat-msg__content {
Expand Down
1 change: 0 additions & 1 deletion src/types/shared/chat/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default class Message extends CustomElement {
onUnfurlAnimationEnd(): void;
onRetryClicked(): Promise<void>;
show_spinner: boolean;
isRetracted(): any;
hasMentions(): any;
getOccupantAffiliation(): any;
getOccupantRole(): any;
Expand Down
2 changes: 1 addition & 1 deletion src/types/shared/chat/templates/retraction.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
declare function _default(el: any): import("lit").TemplateResult<1>;
declare function _default(el: import("shared/chat/message").default): import("lit").TemplateResult<1>;
export default _default;
//# sourceMappingURL=retraction.d.ts.map

0 comments on commit 147be37

Please sign in to comment.