mirror of
https://github.com/discourse/discourse.git
synced 2025-06-04 08:44:48 +08:00
FEATURE: Initial chat thread unread indicators (#21694)
This commit adds the thread index and individual thread in the index list unread indicators, and wires up the message bus events to mark the threads as read/unread when: 1. People send a new message in the thread 2. The user marks a thread as read There are several hacky parts and TODOs to cover before this is more functional: 1. We need to flesh out the thread scrolling and message visibility behaviour. Currently if you scroll to the end of the thread it will just mark the whole thread read unconditionally. 2. We need to send down the thread current user membership along with the last read message ID to the client and update that with read state. 3. We need to handle the sidebar unread dot for when threads are unread in the channel and clear it based on when the channel was last viewed. 4. We need to show some indicator of thread unreads on the thread indicators on original messages. 5. UI improvements to make the experience nicer and more like the actual design rather than just placeholders. But, the basic premise around incrementing/decrementing the thread overview count and showing which thread is unread in the list is working as intended.
This commit is contained in:
@ -148,7 +148,7 @@ RSpec.describe Chat::ChannelViewBuilder do
|
||||
thread = Fabricate(:chat_thread, channel: channel)
|
||||
thread.add(current_user)
|
||||
message_1 = Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(subject.view.thread_tracking_overview).to eq([message_1.thread.id])
|
||||
expect(subject.view.unread_thread_ids).to eq([message_1.thread.id])
|
||||
end
|
||||
|
||||
it "fetches the tracking state of threads in the channel" do
|
||||
|
@ -219,6 +219,7 @@ describe Chat::Publisher do
|
||||
end
|
||||
expect(messages.first.data).to eq(
|
||||
{
|
||||
type: "channel",
|
||||
channel_id: channel.id,
|
||||
message_id: message_1.id,
|
||||
user_id: message_1.user_id,
|
||||
@ -270,12 +271,21 @@ describe Chat::Publisher do
|
||||
context "if threading_enabled is true for the channel" do
|
||||
before { channel.update!(threading_enabled: true) }
|
||||
|
||||
it "does not publish to the new_messages_message_bus_channel" do
|
||||
it "does publish to the new_messages_message_bus_channel" do
|
||||
messages =
|
||||
MessageBus.track_publish(
|
||||
described_class.new_messages_message_bus_channel(channel.id),
|
||||
) { described_class.publish_new!(channel, message_1, staged_id) }
|
||||
expect(messages).to be_empty
|
||||
expect(messages.first.data).to eq(
|
||||
{
|
||||
type: "thread",
|
||||
channel_id: channel.id,
|
||||
message_id: message_1.id,
|
||||
user_id: message_1.user_id,
|
||||
username: message_1.user.username,
|
||||
thread_id: thread.id,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -65,7 +65,7 @@ RSpec.describe Chat::UpdateUserLastRead do
|
||||
membership.update!(last_read_message_id: 1)
|
||||
end
|
||||
|
||||
it { is_expected.to fail_a_policy(:ensure_message_exists) }
|
||||
it { is_expected.to fail_to_find_a_model(:message) }
|
||||
end
|
||||
|
||||
context "when everything is fine" do
|
||||
|
@ -52,10 +52,6 @@ module PageObjects
|
||||
find(".open-drawer-btn").click
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(".open-thread-list-btn").click
|
||||
end
|
||||
|
||||
def has_message?(message)
|
||||
container = find(".chat-message-container[data-id=\"#{message.id}\"")
|
||||
container.has_content?(message.message)
|
||||
|
@ -214,6 +214,26 @@ module PageObjects
|
||||
find(message_thread_indicator_selector(message))
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(thread_list_button_selector).click
|
||||
end
|
||||
|
||||
def has_unread_thread_indicator?(count:)
|
||||
has_css?("#{thread_list_button_selector}.-has-unreads") &&
|
||||
has_css?(
|
||||
".chat-thread-header-unread-indicator .chat-thread-header-unread-indicator__number-wrap",
|
||||
text: count.to_s,
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_unread_thread_indicator?
|
||||
has_no_css?("#{thread_list_button_selector}.-has-unreads")
|
||||
end
|
||||
|
||||
def thread_list_button_selector
|
||||
".chat-threads-list-button"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_thread_indicator_selector(message)
|
||||
|
@ -7,6 +7,14 @@ module PageObjects
|
||||
find(item_by_id_selector(id))
|
||||
end
|
||||
|
||||
def has_unread_item?(id)
|
||||
has_css?(item_by_id_selector(id) + ".-unread")
|
||||
end
|
||||
|
||||
def has_no_unread_item?(id)
|
||||
has_no_css?(item_by_id_selector(id) + ".-unread")
|
||||
end
|
||||
|
||||
def item_by_id_selector(id)
|
||||
".chat-thread-list__items .chat-thread-list-item[data-thread-id=\"#{id}\"]"
|
||||
end
|
||||
|
@ -46,6 +46,26 @@ module PageObjects
|
||||
def has_open_thread_list?
|
||||
has_css?("#{VISIBLE_DRAWER} .chat-thread-list")
|
||||
end
|
||||
|
||||
def open_thread_list
|
||||
find(thread_list_button_selector).click
|
||||
end
|
||||
|
||||
def thread_list_button_selector
|
||||
".chat-threads-list-button"
|
||||
end
|
||||
|
||||
def has_unread_thread_indicator?(count:)
|
||||
has_css?("#{thread_list_button_selector}.-has-unreads") &&
|
||||
has_css?(
|
||||
".chat-thread-header-unread-indicator .chat-thread-header-unread-indicator__number-wrap",
|
||||
text: count.to_s,
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_unread_thread_indicator?
|
||||
has_no_css?("#{thread_list_button_selector}.-has-unreads")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,7 +25,9 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
expect(find(".chat-drawer-header__right-actions")).not_to have_css(".open-thread-list-btn")
|
||||
expect(find(".chat-drawer-header__right-actions")).not_to have_css(
|
||||
drawer_page.thread_list_button_selector,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -55,7 +57,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
end
|
||||
|
||||
@ -63,7 +65,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
expect(thread_list_page).to have_content(thread_1.title)
|
||||
expect(thread_list_page).to have_content(thread_2.title)
|
||||
@ -73,7 +75,7 @@ describe "Thread list in side panel | drawer", type: :system, js: true do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
find(".open-thread-list-btn").click
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
thread_list_page.item_by_id(thread_1.id).click
|
||||
expect(drawer_page).to have_open_thread(thread_1)
|
||||
|
@ -20,7 +20,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
context "when there are no threads that the user is participating in" do
|
||||
it "shows a message" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(page).to have_content(I18n.t("js.chat.threads.none"))
|
||||
end
|
||||
end
|
||||
@ -37,14 +37,14 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "shows a default title for threads without a title" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(page).to have_content(I18n.t("js.chat.thread.default_title", thread_id: thread_1.id))
|
||||
end
|
||||
|
||||
it "shows the thread title with emoji" do
|
||||
thread_1.update!(title: "What is for dinner? :hamburger:")
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_content("What is for dinner?")
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_css("img.emoji[alt='hamburger']")
|
||||
end
|
||||
@ -53,7 +53,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
thread_1.original_message.update!(message: "This is a long message for the excerpt")
|
||||
thread_1.original_message.rebake!
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_content(
|
||||
"This is a long message for the excerpt",
|
||||
)
|
||||
@ -61,7 +61,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "shows the thread original message user username and avatar" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page.item_by_id(thread_1.id)).to have_css(
|
||||
".chat-thread-original-message__avatar .chat-user-avatar .chat-user-avatar-container img",
|
||||
)
|
||||
@ -72,7 +72,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
it "opens a thread" do
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread_1.id).click
|
||||
expect(side_panel).to have_open_thread(thread_1)
|
||||
end
|
||||
@ -82,7 +82,7 @@ describe "Thread list in side panel | full page", type: :system, js: true do
|
||||
|
||||
def open_thread_list
|
||||
chat_page.visit_channel(channel)
|
||||
chat_page.open_thread_list
|
||||
channel_page.open_thread_list
|
||||
expect(side_panel).to have_open_thread_list
|
||||
end
|
||||
|
||||
|
68
plugins/chat/spec/system/thread_tracking/drawer_spec.rb
Normal file
68
plugins/chat/spec/system/thread_tracking/drawer_spec.rb
Normal file
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Thread tracking state | drawer", type: :system, js: true do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:other_user) { Fabricate(:user) }
|
||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:thread_list_page) { PageObjects::Pages::ChatThreadList.new }
|
||||
let(:drawer_page) { PageObjects::Pages::ChatDrawer.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_bootstrap(current_user, [channel])
|
||||
sign_in(current_user)
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
context "when the user has unread messages for a thread" do
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
|
||||
fab!(:message_2) do
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread, user: current_user)
|
||||
end
|
||||
|
||||
it "shows the count of threads with unread messages on the thread list button" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
expect(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||
end
|
||||
|
||||
it "shows an indicator on the unread thread in the list" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_open_thread_list
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "marks the thread as read and removes both indicators when the user opens it" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread.id).click
|
||||
expect(drawer_page).to have_no_unread_thread_indicator
|
||||
drawer_page.open_thread_list
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "shows unread indicators for the header icon and the list when a new unread arrives" do
|
||||
message_1.trash!
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.open_channel(channel)
|
||||
drawer_page.open_thread_list
|
||||
expect(drawer_page).to have_no_unread_thread_indicator
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(drawer_page).to have_unread_thread_indicator(count: 1)
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
end
|
||||
end
|
58
plugins/chat/spec/system/thread_tracking/full_page_spec.rb
Normal file
58
plugins/chat/spec/system/thread_tracking/full_page_spec.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe "Thread tracking state | full page", type: :system, js: true do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:other_user) { Fabricate(:user) }
|
||||
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:thread_list_page) { PageObjects::Pages::ChatThreadList.new }
|
||||
|
||||
before do
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_bootstrap(current_user, [channel])
|
||||
sign_in(current_user)
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
context "when the user has unread messages for a thread" do
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel, thread: thread) }
|
||||
fab!(:message_2) do
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread, user: current_user)
|
||||
end
|
||||
|
||||
it "shows the count of threads with unread messages on the thread list button" do
|
||||
chat_page.visit_channel(channel)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
end
|
||||
|
||||
it "shows an indicator on the unread thread in the list" do
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "marks the thread as read and removes both indicators when the user opens it" do
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
thread_list_page.item_by_id(thread.id).click
|
||||
expect(channel_page).to have_no_unread_thread_indicator
|
||||
channel_page.open_thread_list
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
end
|
||||
|
||||
it "shows unread indicators for the header icon and the list when a new unread arrives" do
|
||||
message_1.trash!
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
expect(channel_page).to have_no_unread_thread_indicator
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
Fabricate(:chat_message, chat_channel: channel, thread: thread)
|
||||
expect(channel_page).to have_unread_thread_indicator(count: 1)
|
||||
expect(thread_list_page).to have_unread_item(thread.id)
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user