mirror of
https://github.com/discourse/discourse.git
synced 2025-06-04 23:36:11 +08:00
FIX: improves reliability of last visit line in chat (#30948)
This commit does several changes: - it moves the ownership of the last message info to the channel instead of storing it on the message, it avoids the need to iterate over every messages - makes an optimistic update of the last read message id - adds a spec to confirm this behavior
This commit is contained in:
@ -322,7 +322,7 @@ export default class ChatChannel extends Component {
|
|||||||
processMessages(channel, result) {
|
processMessages(channel, result) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
let foundFirstNew = false;
|
let foundFirstNew = false;
|
||||||
const hasNewest = this.messagesManager.messages.some((m) => m.newest);
|
channel.newestMessage = null;
|
||||||
|
|
||||||
result?.messages?.forEach((messageData, index) => {
|
result?.messages?.forEach((messageData, index) => {
|
||||||
messageData.firstOfResults = index === 0;
|
messageData.firstOfResults = index === 0;
|
||||||
@ -342,19 +342,22 @@ export default class ChatChannel extends Component {
|
|||||||
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const message = ChatMessage.create(channel, messageData);
|
||||||
|
message.manager = channel.messagesManager;
|
||||||
|
|
||||||
// newest has to be in after fetch callback as we don't want to make it
|
// newest has to be in after fetch callback as we don't want to make it
|
||||||
// dynamic or it will make the pane jump around, it will disappear on reload
|
// dynamic or it will make the pane jump around, it will disappear on reload
|
||||||
if (
|
if (
|
||||||
!hasNewest &&
|
|
||||||
!foundFirstNew &&
|
!foundFirstNew &&
|
||||||
messageData.id > this.currentUserMembership?.lastReadMessageId
|
messageData.id > this.currentUserMembership?.lastReadMessageId
|
||||||
) {
|
) {
|
||||||
foundFirstNew = true;
|
foundFirstNew = true;
|
||||||
messageData.newest = true;
|
if (message !== channel.lastMessage) {
|
||||||
|
channel.newestMessage = message;
|
||||||
|
} else {
|
||||||
|
channel.newestMessage = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = ChatMessage.create(channel, messageData);
|
|
||||||
message.manager = channel.messagesManager;
|
|
||||||
|
|
||||||
if (message.thread) {
|
if (message.thread) {
|
||||||
this.#preloadThreadTrackingState(
|
this.#preloadThreadTrackingState(
|
||||||
@ -430,6 +433,10 @@ export default class ChatChannel extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optimistic update
|
||||||
|
this.args.channel.currentUserMembership.lastReadMessageId = firstMessage.id;
|
||||||
|
this.args.channel.updateLastViewedAt();
|
||||||
|
|
||||||
return this.chatApi.markChannelAsRead(
|
return this.chatApi.markChannelAsRead(
|
||||||
this.args.channel.id,
|
this.args.channel.id,
|
||||||
firstMessage.id
|
firstMessage.id
|
||||||
|
@ -62,6 +62,10 @@ export default class ChatMessageSeparator extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isNewestMessage() {
|
||||||
|
return this.args.message.id === this.args.message.channel.newestMessage?.id;
|
||||||
|
}
|
||||||
|
|
||||||
#areDatesOnSameDay(a, b) {
|
#areDatesOnSameDay(a, b) {
|
||||||
return (
|
return (
|
||||||
a.getFullYear() === b.getFullYear() &&
|
a.getFullYear() === b.getFullYear() &&
|
||||||
@ -87,17 +91,19 @@ export default class ChatMessageSeparator extends Component {
|
|||||||
{{#if this.formattedFirstMessageDate}}
|
{{#if this.formattedFirstMessageDate}}
|
||||||
<div
|
<div
|
||||||
class={{concatClass
|
class={{concatClass
|
||||||
|
"chat-message-separator"
|
||||||
"chat-message-separator-date"
|
"chat-message-separator-date"
|
||||||
(if @message.newest "with-last-visit")
|
(if this.isNewestMessage "with-last-visit")
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
{{on "click" this.onDateClick passive=true}}
|
{{on "click" this.onDateClick passive=true}}
|
||||||
|
data-id={{@message.id}}
|
||||||
>
|
>
|
||||||
<div class="chat-message-separator__text-container" {{this.track}}>
|
<div class="chat-message-separator__text-container" {{this.track}}>
|
||||||
<span class="chat-message-separator__text">
|
<span class="chat-message-separator__text">
|
||||||
{{this.formattedFirstMessageDate}}
|
{{this.formattedFirstMessageDate}}
|
||||||
|
|
||||||
{{#if @message.newest}}
|
{{#if this.isNewestMessage}}
|
||||||
<span class="chat-message-separator__last-visit">
|
<span class="chat-message-separator__last-visit">
|
||||||
<span
|
<span
|
||||||
class="chat-message-separator__last-visit-separator"
|
class="chat-message-separator__last-visit-separator"
|
||||||
@ -112,8 +118,11 @@ export default class ChatMessageSeparator extends Component {
|
|||||||
<div class="chat-message-separator__line-container">
|
<div class="chat-message-separator__line-container">
|
||||||
<div class="chat-message-separator__line"></div>
|
<div class="chat-message-separator__line"></div>
|
||||||
</div>
|
</div>
|
||||||
{{else if @message.newest}}
|
{{else if this.isNewestMessage}}
|
||||||
<div class="chat-message-separator-new">
|
<div
|
||||||
|
class="chat-message-separator chat-message-separator-new"
|
||||||
|
data-id={{@message.id}}
|
||||||
|
>
|
||||||
<div class="chat-message-separator__text-container">
|
<div class="chat-message-separator__text-container">
|
||||||
<span class="chat-message-separator__text">
|
<span class="chat-message-separator__text">
|
||||||
{{i18n "chat.last_visit"}}
|
{{i18n "chat.last_visit"}}
|
||||||
|
@ -4,12 +4,6 @@
|
|||||||
</Styleguide::Component>
|
</Styleguide::Component>
|
||||||
|
|
||||||
<Styleguide::Controls>
|
<Styleguide::Controls>
|
||||||
<Styleguide::Controls::Row @name="Last Visit">
|
|
||||||
<DToggleSwitch
|
|
||||||
@state={{this.message.newest}}
|
|
||||||
{{on "click" this.toggleLastVisit}}
|
|
||||||
/>
|
|
||||||
</Styleguide::Controls::Row>
|
|
||||||
<Styleguide::Controls::Row @name="Deleted">
|
<Styleguide::Controls::Row @name="Deleted">
|
||||||
<DToggleSwitch
|
<DToggleSwitch
|
||||||
@state={{not (not this.message.deletedAt)}}
|
@state={{not (not this.message.deletedAt)}}
|
||||||
|
@ -46,11 +46,6 @@ export default class ChatStyleguideChatMessage extends Component {
|
|||||||
this.message.edited = !this.message.edited;
|
this.message.edited = !this.message.edited;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
toggleLastVisit() {
|
|
||||||
this.message.newest = !this.message.newest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleThread() {
|
toggleThread() {
|
||||||
if (this.message.thread) {
|
if (this.message.thread) {
|
||||||
|
@ -72,6 +72,7 @@ export default class ChatChannel {
|
|||||||
@tracked tracking;
|
@tracked tracking;
|
||||||
@tracked threadingEnabled;
|
@tracked threadingEnabled;
|
||||||
@tracked draft;
|
@tracked draft;
|
||||||
|
@tracked newestMessage;
|
||||||
|
|
||||||
threadsManager = new ChatThreadsManager(getOwnerWithFallback(this));
|
threadsManager = new ChatThreadsManager(getOwnerWithFallback(this));
|
||||||
messagesManager = new ChatMessagesManager(getOwnerWithFallback(this));
|
messagesManager = new ChatMessagesManager(getOwnerWithFallback(this));
|
||||||
|
@ -45,7 +45,6 @@ export default class ChatMessage {
|
|||||||
@tracked chatWebhookEvent;
|
@tracked chatWebhookEvent;
|
||||||
@tracked mentionWarning;
|
@tracked mentionWarning;
|
||||||
@tracked availableFlags;
|
@tracked availableFlags;
|
||||||
@tracked newest;
|
|
||||||
@tracked highlighted;
|
@tracked highlighted;
|
||||||
@tracked firstOfResults;
|
@tracked firstOfResults;
|
||||||
@tracked message;
|
@tracked message;
|
||||||
@ -62,7 +61,6 @@ export default class ChatMessage {
|
|||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this.streaming = args.streaming;
|
this.streaming = args.streaming;
|
||||||
this.manager = args.manager;
|
this.manager = args.manager;
|
||||||
this.newest = args.newest ?? false;
|
|
||||||
this.draftSaved = args.draftSaved ?? args.draft_saved ?? false;
|
this.draftSaved = args.draftSaved ?? args.draft_saved ?? false;
|
||||||
this.firstOfResults = args.firstOfResults ?? args.first_of_results ?? false;
|
this.firstOfResults = args.firstOfResults ?? args.first_of_results ?? false;
|
||||||
this.staged = args.staged ?? false;
|
this.staged = args.staged ?? false;
|
||||||
|
48
plugins/chat/spec/system/last_visit_spec.rb
Normal file
48
plugins/chat/spec/system/last_visit_spec.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Last visit", type: :system do
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: false) }
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel, threading_enabled: false) }
|
||||||
|
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
chat_system_bootstrap
|
||||||
|
channel_1.add(user_1)
|
||||||
|
channel_1.add(current_user)
|
||||||
|
channel_2.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly updates the last visit line" do
|
||||||
|
# a slightly complicated setup to ensure we test against a non trivial case
|
||||||
|
message_1 = Fabricate(:chat_message, user: user_1, chat_channel: channel_1, use_service: true)
|
||||||
|
Fabricate(:chat_message, user: user_1, chat_channel: channel_1, use_service: true)
|
||||||
|
Fabricate(
|
||||||
|
:chat_message,
|
||||||
|
user: user_1,
|
||||||
|
chat_channel: channel_1,
|
||||||
|
in_reply_to: message_1,
|
||||||
|
use_service: true,
|
||||||
|
)
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_last_visit_line_at_id(message_1.id)
|
||||||
|
|
||||||
|
sidebar_page.open_channel(channel_2)
|
||||||
|
sidebar_page.open_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_no_last_visit_line
|
||||||
|
|
||||||
|
sidebar_page.open_channel(channel_2)
|
||||||
|
message_4 = Fabricate(:chat_message, user: user_1, chat_channel: channel_1, use_service: true)
|
||||||
|
sidebar_page.open_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_last_visit_line_at_id(message_4.id)
|
||||||
|
end
|
||||||
|
end
|
@ -52,6 +52,16 @@ module PageObjects
|
|||||||
find(message_by_id_selector(id))
|
find(message_by_id_selector(id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_last_visit_line_at_id?(id)
|
||||||
|
find(".chat-message-separator[data-id=\"#{id}\"]").has_content?(
|
||||||
|
I18n.t("js.chat.last_visit"),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_last_visit_line?
|
||||||
|
has_no_content?(I18n.t("js.chat.last_visit"))
|
||||||
|
end
|
||||||
|
|
||||||
def has_no_loading_skeleton?
|
def has_no_loading_skeleton?
|
||||||
has_no_css?(".chat-skeleton")
|
has_no_css?(".chat-skeleton")
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user