UX: groups deleted messages (#21411)

Any continuous series of deleted messages will now be grouped into one single expand button.
This commit is contained in:
Joffrey JAFFEUX 2023-05-05 17:08:33 +02:00 committed by GitHub
parent ae3231e140
commit cb5e5f3e5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 325 additions and 215 deletions

View File

@ -108,7 +108,7 @@ export default class ChatLivePane extends Component {
// Technically we could keep messages to avoid re-fetching them, but // Technically we could keep messages to avoid re-fetching them, but
// it's not worth the complexity for now // it's not worth the complexity for now
this.args.channel?.messagesManager?.clearMessages(); this.args.channel?.clearMessages();
if (this._loadedChannelId !== this.args.channel?.id) { if (this._loadedChannelId !== this.args.channel?.id) {
this._unsubscribeToUpdates(this._loadedChannelId); this._unsubscribeToUpdates(this._loadedChannelId);
@ -183,7 +183,7 @@ export default class ChatLivePane extends Component {
results results
); );
this.args.channel.messages = messages; this.args.channel.addMessages(messages);
this.args.channel.details = meta; this.args.channel.details = meta;
if (this.requestedTargetMessageId) { if (this.requestedTargetMessageId) {
@ -224,8 +224,8 @@ export default class ChatLivePane extends Component {
const loadingMoreKey = `loadingMore${capitalize(direction)}`; const loadingMoreKey = `loadingMore${capitalize(direction)}`;
const canLoadMore = loadingPast const canLoadMore = loadingPast
? this.#messagesManager.canLoadMorePast ? this.args.channel?.canLoadMorePast
: this.#messagesManager.canLoadMoreFuture; : this.args.channel?.canLoadMoreFuture;
if ( if (
!canLoadMore || !canLoadMore ||
@ -276,7 +276,7 @@ export default class ChatLivePane extends Component {
} }
this.args.channel.details = meta; this.args.channel.details = meta;
this.#messagesManager.addMessages(messages); this.args.channel.addMessages(messages);
// Edge case for IOS to avoid blank screens // Edge case for IOS to avoid blank screens
// and/or scrolling to bottom losing track of scroll position // and/or scrolling to bottom losing track of scroll position
@ -367,7 +367,7 @@ export default class ChatLivePane extends Component {
@debounce(100) @debounce(100)
highlightOrFetchMessage(messageId) { highlightOrFetchMessage(messageId) {
const message = this.#messagesManager?.findMessage(messageId); const message = this.args.channel?.findMessage(messageId);
if (message) { if (message) {
this.scrollToMessage(message.id, { this.scrollToMessage(message.id, {
highlight: true, highlight: true,
@ -388,7 +388,7 @@ export default class ChatLivePane extends Component {
return; return;
} }
const message = this.#messagesManager?.findMessage(messageId); const message = this.args.channel?.findMessage(messageId);
if (message?.deletedAt && opts.autoExpand) { if (message?.deletedAt && opts.autoExpand) {
message.expanded = true; message.expanded = true;
} }
@ -485,7 +485,7 @@ export default class ChatLivePane extends Component {
return; return;
} }
if (this.#messagesManager?.canLoadMoreFuture) { if (this.args.channel?.canLoadMoreFuture) {
this._fetchAndScrollToLatest(); this._fetchAndScrollToLatest();
} else if (this.args.channel.messages?.length > 0) { } else if (this.args.channel.messages?.length > 0) {
this.scrollToMessage( this.scrollToMessage(
@ -534,9 +534,9 @@ export default class ChatLivePane extends Component {
} }
removeMessage(msgData) { removeMessage(msgData) {
const message = this.#messagesManager.findMessage(msgData.id); const message = this.args.channel?.findMessage(msgData.id);
if (message) { if (message) {
this.#messagesManager.removeMessage(message); this.args.channel?.removeMessage(message);
} }
} }
@ -557,7 +557,7 @@ export default class ChatLivePane extends Component {
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) { if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
const stagedMessage = handleStagedMessage( const stagedMessage = handleStagedMessage(
this.args.channel, this.args.channel,
this.#messagesManager, this.args.channel.messagesManager,
data data
); );
if (stagedMessage) { if (stagedMessage) {
@ -565,20 +565,19 @@ export default class ChatLivePane extends Component {
} }
} }
if (this.#messagesManager.canLoadMoreFuture) { if (this.args.channel?.canLoadMoreFuture) {
// If we can load more messages, we just notice the user of new messages // If we can load more messages, we just notice the user of new messages
this.hasNewMessages = true; this.hasNewMessages = true;
} else if (this.#isTowardsBottom()) { } else if (this.#isTowardsBottom()) {
// If we are at the bottom, we append the message and scroll to it // If we are at the bottom, we append the message and scroll to it
const message = ChatMessage.create(this.args.channel, data.chat_message); const message = ChatMessage.create(this.args.channel, data.chat_message);
this.args.channel.addMessages([message]);
this.#messagesManager.addMessages([message]);
this.scrollToLatestMessage(); this.scrollToLatestMessage();
this.updateLastReadMessage(); this.updateLastReadMessage();
} else { } else {
// If we are almost at the bottom, we append the message and notice the user // If we are almost at the bottom, we append the message and notice the user
const message = ChatMessage.create(this.args.channel, data.chat_message); const message = ChatMessage.create(this.args.channel, data.chat_message);
this.#messagesManager.addMessages([message]); this.args.channel.addMessages([message]);
this.hasNewMessages = true; this.hasNewMessages = true;
} }
} }
@ -589,10 +588,6 @@ export default class ChatLivePane extends Component {
return this.isDestroying || this.isDestroyed; return this.isDestroying || this.isDestroyed;
} }
get #messagesManager() {
return this.args.channel?.messagesManager;
}
@action @action
onSendMessage(message) { onSendMessage(message) {
if (message.editing) { if (message.editing) {
@ -711,7 +706,7 @@ export default class ChatLivePane extends Component {
} }
_onSendError(id, error) { _onSendError(id, error) {
const stagedMessage = this.#messagesManager.findStagedMessage(id); const stagedMessage = this.args.channel.findStagedMessage(id);
if (stagedMessage) { if (stagedMessage) {
if (error.jqXHR?.responseJSON?.errors?.length) { if (error.jqXHR?.responseJSON?.errors?.length) {
// only network errors are retryable // only network errors are retryable

View File

@ -1,202 +1,206 @@
{{! template-lint-disable no-invalid-interactive }} {{! template-lint-disable no-invalid-interactive }}
{{#if (eq @context "channel")}} {{#if this.shouldRender}}
<ChatMessageSeparatorDate @message={{@message}} /> {{#if (eq @context "channel")}}
<ChatMessageSeparatorNew @message={{@message}} /> <ChatMessageSeparatorDate @message={{@message}} />
{{/if}} <ChatMessageSeparatorNew @message={{@message}} />
{{/if}}
<div <div
{{will-destroy this.teardownChatMessage}} {{will-destroy this.teardownChatMessage}}
{{did-insert this.decorateCookedMessage}} {{did-insert this.decorateCookedMessage}}
{{did-update this.decorateCookedMessage @message.id}} {{did-update this.decorateCookedMessage @message.id}}
{{did-update this.decorateCookedMessage @message.version}} {{did-update this.decorateCookedMessage @message.version}}
{{on "touchmove" this.handleTouchMove passive=true}} {{on "touchmove" this.handleTouchMove passive=true}}
{{on "touchstart" this.handleTouchStart passive=true}} {{on "touchstart" this.handleTouchStart passive=true}}
{{on "touchend" this.handleTouchEnd passive=true}} {{on "touchend" this.handleTouchEnd passive=true}}
{{on "mouseenter" this.onMouseEnter}} {{on "mouseenter" this.onMouseEnter}}
{{on "mouseleave" this.onMouseLeave}} {{on "mouseleave" this.onMouseLeave}}
{{on "mousemove" this.onMouseMove}} {{on "mousemove" this.onMouseMove}}
class={{concat-class class={{concat-class
"chat-message-container" "chat-message-container"
(if this.pane.selectingMessages "selecting-messages") (if this.pane.selectingMessages "selecting-messages")
(if @message.highlighted "highlighted") (if @message.highlighted "highlighted")
}} }}
data-id={{@message.id}} data-id={{@message.id}}
data-thread-id={{@message.thread.id}} data-thread-id={{@message.thread.id}}
{{chat/track-message {{chat/track-message
(hash (hash
didEnterViewport=(fn @messageDidEnterViewport @message) didEnterViewport=(fn @messageDidEnterViewport @message)
didLeaveViewport=(fn @messageDidLeaveViewport @message) didLeaveViewport=(fn @messageDidLeaveViewport @message)
) )
}} }}
> >
{{#if this.show}} {{#if this.show}}
{{#if this.pane.selectingMessages}} {{#if this.pane.selectingMessages}}
<Input <Input
@type="checkbox" @type="checkbox"
class="chat-message-selector" class="chat-message-selector"
@checked={{@message.selected}} @checked={{@message.selected}}
{{on "click" this.toggleChecked}} {{on "click" this.toggleChecked}}
/>
{{/if}}
{{#if this.deletedAndCollapsed}}
<div class="chat-message-deleted">
<DButton
@class="btn-flat chat-message-expand"
@action={{this.expand}}
@label="chat.deleted"
/> />
</div> {{/if}}
{{else if this.hiddenAndCollapsed}}
<div class="chat-message-hidden">
<DButton
@class="btn-flat chat-message-expand"
@action={{this.expand}}
@label="chat.hidden"
/>
</div>
{{else}}
<div
class={{concat-class
"chat-message"
(if @message.staged "chat-message-staged")
(if @message.deletedAt "deleted")
(if (and @message.inReplyTo (not this.hideReplyToInfo)) "is-reply")
(if this.showThreadIndicator "is-threaded")
(if this.hideUserInfo "user-info-hidden")
(if @message.error "errored")
(if @message.bookmark "chat-message-bookmarked")
(if (eq @message.id this.chat.activeMessage.model.id) "is-active")
}}
>
{{#unless this.hideReplyToInfo}}
<ChatMessageInReplyToIndicator @message={{@message}} />
{{/unless}}
{{#if this.hideUserInfo}} {{#if this.deletedAndCollapsed}}
<ChatMessageLeftGutter @message={{@message}} /> <div class="chat-message-deleted">
{{else}} <DButton
<ChatMessageAvatar @message={{@message}} /> @class="btn-flat chat-message-expand"
{{/if}} @action={{this.expand}}
@translatedLabel={{this.deletedMessageLabel}}
<div class="chat-message-content"> />
{{#unless this.hideUserInfo}} </div>
<ChatMessageInfo @message={{@message}} /> {{else if this.hiddenAndCollapsed}}
<div class="chat-message-hidden">
<DButton
@class="btn-flat chat-message-expand"
@action={{this.expand}}
@label="chat.hidden"
/>
</div>
{{else}}
<div
class={{concat-class
"chat-message"
(if @message.staged "chat-message-staged")
(if @message.deletedAt "deleted")
(if (and @message.inReplyTo (not this.hideReplyToInfo)) "is-reply")
(if this.showThreadIndicator "is-threaded")
(if this.hideUserInfo "user-info-hidden")
(if @message.error "errored")
(if @message.bookmark "chat-message-bookmarked")
(if (eq @message.id this.chat.activeMessage.model.id) "is-active")
}}
>
{{#unless this.hideReplyToInfo}}
<ChatMessageInReplyToIndicator @message={{@message}} />
{{/unless}} {{/unless}}
<ChatMessageText {{#if this.hideUserInfo}}
@cooked={{@message.cooked}} <ChatMessageLeftGutter @message={{@message}} />
@uploads={{@message.uploads}} {{else}}
@edited={{@message.edited}} <ChatMessageAvatar @message={{@message}} />
> {{/if}}
{{#if @message.reactions.length}}
<div class="chat-message-reaction-list">
{{#if this.reactionLabel}}
<div class="reaction-users-list">
{{replace-emoji this.reactionLabel}}
</div>
{{/if}}
{{#each @message.reactions as |reaction|}} <div class="chat-message-content">
<ChatMessageReaction {{#unless this.hideUserInfo}}
@reaction={{reaction}} <ChatMessageInfo @message={{@message}} />
@onReaction={{this.messageInteractor.react}} {{/unless}}
@message={{@message}}
@showTooltip={{true}}
/>
{{/each}}
{{#if this.chat.userCanInteractWithChat}} <ChatMessageText
{{#unless this.site.mobileView}} @cooked={{@message.cooked}}
<DButton @uploads={{@message.uploads}}
@class="chat-message-react-btn" @edited={{@message.edited}}
@action={{this.messageInteractor.openEmojiPicker}} >
@icon="discourse-emojis" {{#if @message.reactions.length}}
@title="chat.react" <div class="chat-message-reaction-list">
@forwardEvent={{true}} {{#if this.reactionLabel}}
<div class="reaction-users-list">
{{replace-emoji this.reactionLabel}}
</div>
{{/if}}
{{#each @message.reactions as |reaction|}}
<ChatMessageReaction
@reaction={{reaction}}
@onReaction={{this.messageInteractor.react}}
@message={{@message}}
@showTooltip={{true}}
/> />
{{/unless}} {{/each}}
{{#if this.chat.userCanInteractWithChat}}
{{#unless this.site.mobileView}}
<DButton
@class="chat-message-react-btn"
@action={{this.messageInteractor.openEmojiPicker}}
@icon="discourse-emojis"
@title="chat.react"
@forwardEvent={{true}}
/>
{{/unless}}
{{/if}}
</div>
{{/if}}
</ChatMessageText>
{{#if @message.error}}
<div class="chat-send-error">
{{#if (eq @message.error "network_error")}}
<DButton
class="retry-staged-message-btn"
@action={{fn @resendStagedMessage @message}}
@icon="exclamation-circle"
>
<span class="retry-staged-message-btn__title">
{{i18n "chat.retry_staged_message.title"}}
</span>
<span class="retry-staged-message-btn__action">
{{i18n "chat.retry_staged_message.action"}}
</span>
</DButton>
{{else}}
{{@message.error}}
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
</ChatMessageText>
{{#if @message.error}} {{#if this.mentionWarning}}
<div class="chat-send-error"> <div class="alert alert-info chat-message-mention-warning">
{{#if (eq @message.error "network_error")}} {{#if this.mentionWarning.invitation_sent}}
<DButton {{d-icon "check"}}
class="retry-staged-message-btn" <span>
@action={{fn @resendStagedMessage @message}} {{i18n
@icon="exclamation-circle" "chat.mention_warning.invitations_sent"
> count=this.mentionWarning.without_membership.length
<span class="retry-staged-message-btn__title"> }}
{{i18n "chat.retry_staged_message.title"}}
</span> </span>
<span class="retry-staged-message-btn__action"> {{else}}
{{i18n "chat.retry_staged_message.action"}} <FlatButton
</span> @class="dismiss-mention-warning"
</DButton> @title="chat.mention_warning.dismiss"
{{else}} @action={{this.dismissMentionWarning}}
{{@message.error}} @icon="times"
{{/if}} />
</div>
{{/if}}
{{#if this.mentionWarning}} {{#if this.mentionWarning.cannot_see}}
<div class="alert alert-info chat-message-mention-warning"> <p class="warning-item cannot-see">
{{#if this.mentionWarning.invitation_sent}} {{this.mentionedCannotSeeText}}
{{d-icon "check"}} </p>
<span> {{/if}}
{{i18n
"chat.mention_warning.invitations_sent"
count=this.mentionWarning.without_membership.length
}}
</span>
{{else}}
<FlatButton
@class="dismiss-mention-warning"
@title="chat.mention_warning.dismiss"
@action={{this.dismissMentionWarning}}
@icon="times"
/>
{{#if this.mentionWarning.cannot_see}} {{#if this.mentionWarning.without_membership}}
<p class="warning-item cannot-see"> <p class="warning-item without-membership">
{{this.mentionedCannotSeeText}} <span>{{this.mentionedWithoutMembershipText}}</span>
</p> <a
{{/if}} class="invite-link"
href
onclick={{this.inviteMentioned}}
>
{{i18n "chat.mention_warning.invite"}}
</a>
</p>
{{/if}}
{{#if this.mentionWarning.group_mentions_disabled}}
<p class="warning-item">
{{this.groupsWithDisabledMentions}}
</p>
{{/if}}
{{#if this.mentionWarning.without_membership}} {{#if this.mentionWarning.groups_with_too_many_members}}
<p class="warning-item without-membership"> <p
<span>{{this.mentionedWithoutMembershipText}}</span> class="warning-item"
<a >{{this.groupsWithTooManyMembers}}</p>
class="invite-link" {{/if}}
href
onclick={{this.inviteMentioned}}
>
{{i18n "chat.mention_warning.invite"}}
</a>
</p>
{{/if}}
{{#if this.mentionWarning.group_mentions_disabled}}
<p class="warning-item">
{{this.groupsWithDisabledMentions}}
</p>
{{/if}} {{/if}}
</div>
{{/if}}
</div>
{{#if this.mentionWarning.groups_with_too_many_members}} {{#if this.showThreadIndicator}}
<p class="warning-item">{{this.groupsWithTooManyMembers}}</p> <ChatMessageThreadIndicator @message={{@message}} />
{{/if}}
{{/if}}
</div>
{{/if}} {{/if}}
</div> </div>
{{/if}}
{{#if this.showThreadIndicator}}
<ChatMessageThreadIndicator @message={{@message}} />
{{/if}}
</div>
{{/if}} {{/if}}
{{/if}} </div>
</div> {{/if}}

View File

@ -69,9 +69,43 @@ export default class ChatMessage extends Component {
return !this.args.message?.expanded; return !this.args.message?.expanded;
} }
get deletedMessageLabel() {
let count = 1;
const recursiveCount = (message) => {
const previousMessage = message.previousMessage;
if (previousMessage?.deletedAt) {
count++;
recursiveCount(previousMessage);
}
};
recursiveCount(this.args.message);
return I18n.t("chat.deleted", { count });
}
get shouldRender() {
return (
this.args.message.expanded ||
!this.args.message.deletedAt ||
(this.args.message.deletedAt && !this.args.message.nextMessage?.deletedAt)
);
}
@action @action
expand() { expand() {
const recursiveExpand = (message) => {
const previousMessage = message.previousMessage;
if (previousMessage?.deletedAt) {
previousMessage.expanded = true;
recursiveExpand(previousMessage);
}
};
this.args.message.expanded = true; this.args.message.expanded = true;
recursiveExpand(this.args.message);
} }
@action @action

View File

@ -215,9 +215,9 @@ export default class ChatMessageInteractor {
bulkSelect(checked) { bulkSelect(checked) {
const channel = this.message.channel; const channel = this.message.channel;
const lastSelectedIndex = channel.findIndexOfMessage( const lastSelectedIndex = channel.findIndexOfMessage(
this.pane.lastSelectedMessage this.pane.lastSelectedMessage.id
); );
const newlySelectedIndex = channel.findIndexOfMessage(this.message); const newlySelectedIndex = channel.findIndexOfMessage(this.message.id);
const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort( const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort(
(a, b) => a - b (a, b) => a - b
); );

View File

@ -12,6 +12,7 @@ export default class ChatMessagesManager {
} }
clearMessages() { clearMessages() {
this.messages.forEach((message) => (message.manager = null));
this.messages.clear(); this.messages.clear();
this.canLoadMoreFuture = null; this.canLoadMoreFuture = null;
@ -19,6 +20,10 @@ export default class ChatMessagesManager {
} }
addMessages(messages = []) { addMessages(messages = []) {
messages.forEach((message) => {
message.manager = this;
});
this.messages = this.messages this.messages = this.messages
.concat(messages) .concat(messages)
.uniqBy("id") .uniqBy("id")
@ -40,4 +45,8 @@ export default class ChatMessagesManager {
(message) => message.staged && message.id === stagedMessageId (message) => message.staged && message.id === stagedMessageId
); );
} }
findIndexOfMessage(id) {
return this.messages.findIndex((m) => m.id === id);
}
} }

View File

@ -68,14 +68,30 @@ export default class ChatChannel extends RestModel {
threadsManager = new ChatThreadsManager(getOwner(this)); threadsManager = new ChatThreadsManager(getOwner(this));
messagesManager = new ChatMessagesManager(getOwner(this)); messagesManager = new ChatMessagesManager(getOwner(this));
findIndexOfMessage(message) { findIndexOfMessage(id) {
return this.messages.findIndex((m) => m.id === message.id); return this.messagesManager.findIndexOfMessage(id);
}
findStagedMessage(id) {
return this.messagesManager.findStagedMessage(id);
} }
findMessage(id) { findMessage(id) {
return this.messagesManager.findMessage(id); return this.messagesManager.findMessage(id);
} }
addMessages(messages) {
this.messagesManager.addMessages(messages);
}
clearMessages() {
this.messagesManager.clearMessages();
}
removeMessage(message) {
this.messagesManager.removeMessage(message);
}
get messages() { get messages() {
return this.messagesManager.messages; return this.messagesManager.messages;
} }
@ -88,6 +104,10 @@ export default class ChatChannel extends RestModel {
return this.messagesManager.canLoadMoreFuture; return this.messagesManager.canLoadMoreFuture;
} }
get canLoadMorePast() {
return this.messagesManager.canLoadMorePast;
}
get escapedTitle() { get escapedTitle() {
return escapeExpression(this.title); return escapeExpression(this.title);
} }
@ -186,10 +206,10 @@ export default class ChatChannel extends RestModel {
if (message.inReplyTo) { if (message.inReplyTo) {
if (!this.threadingEnabled) { if (!this.threadingEnabled) {
this.messagesManager.addMessages([message]); this.addMessages([message]);
} }
} else { } else {
this.messagesManager.addMessages([message]); this.addMessages([message]);
} }
} }

View File

@ -50,6 +50,8 @@ export default class ChatMessage {
@tracked message; @tracked message;
@tracked thread; @tracked thread;
@tracked threadReplyCount; @tracked threadReplyCount;
@tracked manager = null;
@tracked _cooked; @tracked _cooked;
constructor(channel, args = {}) { constructor(channel, args = {}) {
@ -193,17 +195,17 @@ export default class ChatMessage {
@cached @cached
get index() { get index() {
return this.channel.messages.indexOf(this); return this.manager?.messages?.indexOf(this);
} }
@cached @cached
get previousMessage() { get previousMessage() {
return this.channel?.messages?.objectAt?.(this.index - 1); return this.manager?.messages?.objectAt?.(this.index - 1);
} }
@cached @cached
get nextMessage() { get nextMessage() {
return this.channel?.messages?.objectAt?.(this.index + 1); return this.manager?.messages?.objectAt?.(this.index + 1);
} }
incrementVersion() { incrementVersion() {

View File

@ -7,6 +7,10 @@
color: var(--primary-low-mid); color: var(--primary-low-mid);
padding: 0.25em; padding: 0.25em;
.d-button-label {
text-align: left;
}
&:hover { &:hover {
background: inherit; background: inherit;
color: inherit; color: inherit;

View File

@ -81,7 +81,10 @@ en:
collapse: "Collapse Chat Drawer" collapse: "Collapse Chat Drawer"
expand: "Expand Chat Drawer" expand: "Expand Chat Drawer"
confirm_flag: "Are you sure you want to flag %{username}'s message?" confirm_flag: "Are you sure you want to flag %{username}'s message?"
deleted: "A message was deleted. [view]" deleted:
one: "A message was deleted. [view]"
other: "%{count} messages were deleted. [view all]"
hidden: "A message was hidden. [view]" hidden: "A message was hidden. [view]"
delete: "Delete" delete: "Delete"
edited: "edited" edited: "edited"

View File

@ -4,7 +4,7 @@ RSpec.describe "Deleted message", type: :system, js: true do
let(:chat_page) { PageObjects::Pages::Chat.new } let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new } let(:channel_page) { PageObjects::Pages::ChatChannel.new }
fab!(:current_user) { Fabricate(:user) } fab!(:current_user) { Fabricate(:admin) }
fab!(:channel_1) { Fabricate(:category_channel) } fab!(:channel_1) { Fabricate(:category_channel) }
before do before do
@ -21,7 +21,33 @@ RSpec.describe "Deleted message", type: :system, js: true do
last_message = find(".chat-message-container:last-child") last_message = find(".chat-message-container:last-child")
channel_page.delete_message(OpenStruct.new(id: last_message["data-id"])) channel_page.delete_message(OpenStruct.new(id: last_message["data-id"]))
expect(page).to have_content(I18n.t("js.chat.deleted")) expect(channel_page).to have_deleted_message(
OpenStruct.new(id: last_message["data-id"]),
count: 1,
)
end
end
context "when deleting multiple messages" do
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_3) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_4) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_5) { Fabricate(:chat_message, chat_channel: channel_1) }
fab!(:message_6) { Fabricate(:chat_message, chat_channel: channel_1) }
it "groups them" do
chat_page.visit_channel(channel_1)
channel_page.delete_message(message_1)
channel_page.delete_message(message_3)
channel_page.delete_message(message_4)
channel_page.delete_message(message_6)
expect(channel_page).to have_deleted_message(message_1)
expect(channel_page).to have_deleted_message(message_4, count: 2)
expect(channel_page).to have_deleted_message(message_6)
expect(channel_page).to have_no_message(id: message_3.id)
end end
end end
@ -65,9 +91,9 @@ RSpec.describe "Deleted message", type: :system, js: true do
) )
expect(channel_page).to have_no_message(id: message_1.id) expect(channel_page).to have_no_message(id: message_1.id)
expect(channel_page).to have_no_message(id: message_2.id) expect(channel_page).to have_deleted_message(message_2, count: 2)
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_4.id) expect(open_thread).to have_no_message(thread_id: thread.id, id: message_4.id)
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_5.id) expect(open_thread).to have_deleted_message(message_5, count: 2)
end end
end end
end end

View File

@ -87,8 +87,7 @@ RSpec.describe "Move message to channel", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
expect(page).to have_no_content(message_1.message) expect(channel).to have_deleted_message(message_1)
expect(page).to have_content(I18n.t("js.chat.deleted"))
end end
end end
end end

View File

@ -172,6 +172,13 @@ module PageObjects
check_message_presence(exists: false, text: text, id: id) check_message_presence(exists: false, text: text, id: id)
end end
def has_deleted_message?(message, count: 1)
has_css?(
".chat-channel .chat-message-container[data-id=\"#{message.id}\"] .chat-message-deleted",
text: I18n.t("js.chat.deleted", count: count),
)
end
def check_message_presence(exists: true, text: nil, id: nil) def check_message_presence(exists: true, text: nil, id: nil)
css_method = exists ? :has_css? : :has_no_css? css_method = exists ? :has_css? : :has_no_css?
if text if text

View File

@ -77,6 +77,13 @@ module PageObjects
def message_by_id_selector(id) def message_by_id_selector(id)
".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]" ".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
end end
def has_deleted_message?(message, count: 1)
has_css?(
".chat-thread .chat-message-container[data-id=\"#{message.id}\"] .chat-message-deleted",
text: I18n.t("js.chat.deleted", count: count),
)
end
end end
end end
end end