FEATURE: Show unread in sidebar for unread channel threads (#22342)

This commit makes it so that when the user has unread threads
for a channel we show a blue dot in the sidebar (or channel index
for mobile/drawer).

This blue dot is slightly different from the channel unread messages:

1. It will only show if the new thread messages were created since
   the user last viewed the channel
2. It will be cleared when the user views the channel, but the threads
   are still considered unread because we want the user to click into
   the thread list to view them

This necessitates a change to the current user serializer to also
include the unread thread overview, which is all unread threads
across all channels and their last reply date + time.
This commit is contained in:
Martin Brennan
2023-07-17 13:00:49 +10:00
committed by GitHub
parent edc837eaf5
commit 07c3782e51
31 changed files with 459 additions and 132 deletions
@@ -9,6 +9,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
include_missing_memberships: include_missing_memberships,
include_threads: include_threads,
include_read: include_read,
include_last_reply_details: include_last_reply_details,
)
end
@@ -20,6 +21,7 @@ RSpec.describe Chat::TrackingStateReportQuery do
let(:include_missing_memberships) { false }
let(:include_threads) { false }
let(:include_read) { true }
let(:include_last_reply_details) { false }
context "when channel_ids empty" do
it "returns empty object for channel_tracking" do
expect(query.channel_tracking).to eq({})
@@ -129,6 +131,56 @@ RSpec.describe Chat::TrackingStateReportQuery do
)
end
context "when include_last_reply_details is true" do
let(:include_last_reply_details) { true }
before do
thread_1.add(current_user)
thread_2.add(current_user)
Fabricate(:chat_message, chat_channel: channel_1, thread: thread_1)
Fabricate(:chat_message, chat_channel: channel_2, thread: thread_2)
end
it "gets the last_reply_created_at for each thread based on the last_message" do
expect(query.thread_tracking).to eq(
{
thread_1.id => {
unread_count: 1,
mention_count: 0,
channel_id: channel_1.id,
last_reply_created_at: thread_1.reload.last_message.created_at,
},
thread_2.id => {
unread_count: 1,
mention_count: 0,
channel_id: channel_2.id,
last_reply_created_at: thread_2.reload.last_message.created_at,
},
},
)
end
it "does not get the last_reply_created_at for threads where the last_message is deleted" do
thread_1.reload.last_message.trash!
expect(query.thread_tracking).to eq(
{
thread_1.id => {
unread_count: 0,
mention_count: 0,
channel_id: channel_1.id,
last_reply_created_at: nil,
},
thread_2.id => {
unread_count: 1,
mention_count: 0,
channel_id: channel_2.id,
last_reply_created_at: thread_2.reload.last_message.created_at,
},
},
)
end
end
context "when thread_ids and channel_ids is empty" do
let(:thread_ids) { [] }
let(:channel_ids) { [] }
@@ -173,6 +173,7 @@ RSpec.describe Chat::ChannelViewBuilder do
channel_ids: [channel.id],
include_threads: true,
include_read: false,
include_last_reply_details: true,
)
.returns(Chat::TrackingStateReport.new)
.once
@@ -188,7 +189,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(result.view.unread_thread_ids).to eq([message_1.thread.id])
expect(result.view.unread_thread_overview).to eq({ thread.id => message_1.created_at })
end
it "fetches the tracking state of threads in the channel" do
@@ -137,18 +137,21 @@ RSpec.describe Chat::MarkAllUserChannelsRead do
expect(message.data).to eq(
channel_1.id.to_s => {
"last_read_message_id" => message_2.id,
"last_reply_created_at" => nil,
"membership_id" => membership_1.id,
"mention_count" => 0,
"unread_count" => 0,
},
channel_2.id.to_s => {
"last_read_message_id" => message_4.id,
"last_reply_created_at" => nil,
"membership_id" => membership_2.id,
"mention_count" => 0,
"unread_count" => 0,
},
channel_3.id.to_s => {
"last_read_message_id" => message_6.id,
"last_reply_created_at" => nil,
"membership_id" => membership_3.id,
"mention_count" => 0,
"unread_count" => 0,
@@ -96,8 +96,10 @@ describe Chat::Publisher do
context "when the channel has threading enabled and the message is a thread reply" do
fab!(:thread) { Fabricate(:chat_thread, channel: channel) }
before do
message_1.update!(thread: thread)
thread.update_last_message_id!
channel.update!(threading_enabled: true)
end
@@ -106,16 +108,22 @@ describe Chat::Publisher do
it "publishes the tracking state with correct counts" do
expect(data["thread_id"]).to eq(thread.id)
expect(data["unread_thread_ids"]).to eq([thread.id])
expect(data["thread_tracking"]).to eq({ "unread_count" => 1, "mention_count" => 0 })
expect(data["unread_thread_overview"]).to eq(
{ thread.id.to_s => thread.reload.last_message.created_at.iso8601(3) },
)
expect(data["thread_tracking"]).to eq(
{ "unread_count" => 1, "mention_count" => 0, "last_reply_created_at" => nil },
)
end
end
context "when the user has no thread membership" do
it "publishes the tracking state with zeroed out counts" do
expect(data["thread_id"]).to eq(thread.id)
expect(data["unread_thread_ids"]).to eq([])
expect(data["thread_tracking"]).to eq({ "unread_count" => 0, "mention_count" => 0 })
expect(data["unread_thread_overview"]).to eq({})
expect(data["thread_tracking"]).to eq(
{ "unread_count" => 0, "mention_count" => 0, "last_reply_created_at" => nil },
)
end
end
end
@@ -26,6 +26,7 @@
"avatar_template": { "type": "string" },
"username": { "type": "string" }
}
}
},
"last_viewed_at": { "type": "datetime" }
}
}
@@ -109,7 +109,6 @@ RSpec.describe "Message notifications - mobile", type: :system, mobile: true do
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
text: 1,
)
end
end
@@ -130,7 +129,6 @@ RSpec.describe "Message notifications - mobile", type: :system, mobile: true do
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
text: 1,
)
end
end
@@ -20,12 +20,22 @@ module PageObjects
end
def open_channel(channel)
find(
"#{VISIBLE_DRAWER} .channels-list .chat-channel-row[data-chat-channel-id='#{channel.id}']",
).click
find("#{VISIBLE_DRAWER} .channels-list #{channel_row_selector(channel)}").click
has_no_css?(".chat-skeleton")
end
def channel_row_selector(channel)
".chat-channel-row[data-chat-channel-id='#{channel.id}']"
end
def has_unread_channel?(channel)
has_css?("#{channel_row_selector(channel)} .chat-channel-unread-indicator")
end
def has_no_unread_channel?(channel)
has_no_css?("#{channel_row_selector(channel)} .chat-channel-unread-indicator")
end
def maximize
mouseout
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
@@ -40,6 +40,16 @@ module PageObjects
find(".sidebar-section-link.channel-#{channel.id}")
self
end
def has_unread_channel?(channel)
has_css?(".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread")
end
def has_no_unread_channel?(channel)
has_no_css?(
".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread",
)
end
end
end
end
@@ -64,5 +64,53 @@ describe "Thread tracking state | drawer", type: :system do
expect(drawer_page).to have_unread_thread_indicator(count: 1)
expect(thread_list_page).to have_unread_item(thread.id)
end
describe "channel index unread indicators" do
fab!(:other_channel) { Fabricate(:chat_channel) }
before { other_channel.add(current_user) }
it "shows an unread indicator for the channel with unread threads in the index" do
visit("/")
chat_page.open_from_header
expect(drawer_page).to have_unread_channel(channel)
end
it "does not show an unread indicator for the channel if the user has visited the channel since the unread thread message arrived" do
channel.membership_for(current_user).update!(last_viewed_at: Time.zone.now)
visit("/")
chat_page.open_from_header
expect(drawer_page).to have_no_unread_channel(channel)
end
it "clears the index unread indicator for the channel when opening it but keeps the thread list unread indicator" do
visit("/")
chat_page.open_from_header
drawer_page.open_channel(channel)
expect(channel_page).to have_unread_thread_indicator(count: 1)
drawer_page.back
expect(drawer_page).to have_no_unread_channel(channel)
end
it "does not show an unread indicator for the channel index if a new thread message arrives while the user is looking at the channel" do
visit("/")
chat_page.open_from_header
expect(drawer_page).to have_unread_channel(channel)
drawer_page.open_channel(channel)
Fabricate(:chat_message, thread: thread)
drawer_page.back
expect(drawer_page).to have_no_unread_channel(channel)
end
it "shows an unread indicator for the channel index if a new thread message arrives while the user is not looking at the channel" do
visit("/")
chat_page.open_from_header
drawer_page.open_channel(channel)
drawer_page.back
expect(drawer_page).to have_no_unread_channel(channel)
Fabricate(:chat_message, thread: thread)
expect(drawer_page).to have_unread_channel(channel)
end
end
end
end
@@ -10,6 +10,7 @@ describe "Thread tracking state | full page", type: :system do
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:thread_page) { PageObjects::Pages::ChatThread.new }
let(:thread_list_page) { PageObjects::Components::Chat::ThreadList.new }
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
@@ -86,6 +87,47 @@ describe "Thread tracking state | full page", type: :system do
expect(thread_list_page).to have_thread(new_thread)
end
describe "sidebar unread indicators" do
fab!(:other_channel) { Fabricate(:chat_channel) }
before do
other_channel.add(current_user)
SiteSetting.navigation_menu = "sidebar"
end
it "shows an unread indicator for the channel with unread threads in the sidebar" do
chat_page.visit_channel(other_channel)
expect(sidebar_page).to have_unread_channel(channel)
end
it "does not show an unread indicator for the channel if the user has visited the channel since the unread thread message arrived" do
channel.membership_for(current_user).update!(last_viewed_at: Time.zone.now)
chat_page.visit_channel(other_channel)
expect(sidebar_page).to have_no_unread_channel(channel)
end
it "clears the sidebar unread indicator for the channel when opening it but keeps the thread list unread indicator" do
chat_page.visit_channel(channel)
expect(sidebar_page).to have_no_unread_channel(channel)
expect(channel_page).to have_unread_thread_indicator(count: 1)
end
it "does not show an unread indicator for the channel sidebar if a new thread message arrives while the user is looking at the channel" do
chat_page.visit_channel(channel)
expect(sidebar_page).to have_no_unread_channel(channel)
Fabricate(:chat_message, thread: thread)
expect(sidebar_page).to have_no_unread_channel(channel)
end
it "shows an unread indicator for the channel sidebar if a new thread message arrives while the user is not looking at the channel" do
chat_page.visit_channel(channel)
expect(sidebar_page).to have_no_unread_channel(channel)
chat_page.visit_channel(other_channel)
Fabricate(:chat_message, thread: thread)
expect(sidebar_page).to have_unread_channel(channel)
end
end
context "when the user's notification level for the thread is set to normal" do
before { thread.membership_for(current_user).update!(notification_level: :normal) }