stream_list: Show mention indicator in section headings.

Fixes #35104.
This commit is contained in:
Evy Kassirer
2025-08-06 14:01:56 -07:00
committed by Tim Abbott
parent 304fe9ff14
commit 4df4f072f4
6 changed files with 88 additions and 3 deletions

View File

@@ -357,11 +357,35 @@ export function build_stream_list(force_rerender: boolean): void {
const counts = unread.get_counts();
left_sidebar_navigation_area.update_dom_with_unread_counts(counts, false);
update_dom_with_unread_counts(counts);
update_stream_section_mention_indicators();
sidebar_ui.update_unread_counts_visibility();
set_sections_states();
$("#streams_list").toggleClass("is_searching", ui_util.get_left_sidebar_search_term() !== "");
}
export let update_stream_section_mention_indicators = function (): void {
const mentions = unread.mention_counts_by_section();
for (const section of stream_list_sort.section_ids()) {
const $header = $(`#stream-list-${section}-container .stream-list-subsection-header`);
const mentions_for_section = mentions.get(section) ?? {
has_mentions: false,
has_unmuted_mentions: false,
};
ui_util.update_unread_mention_info_in_dom($header, mentions_for_section.has_mentions);
$header.toggleClass(
"has-only-muted-mentions",
mentions_for_section.has_mentions && !mentions_for_section.has_unmuted_mentions,
);
}
};
export function rewire_update_stream_section_mention_indicators(
value: typeof update_stream_section_mention_indicators,
): void {
update_stream_section_mention_indicators = value;
}
/* When viewing a channel in a collapsed folder, we show that active
highlighted channel in the left sidebar even though the folder is
collapsed. If there's an active highlighted topic within the
@@ -689,6 +713,7 @@ export let update_dom_with_unread_counts = function (counts: FullUnreadCountsDat
stream_has_only_muted_unread_mentions,
);
}
update_stream_section_mention_indicators();
// (2) Unread counts in stream headers and collapse/uncollapse
// toggles for muted and inactive channels.

View File

@@ -35,6 +35,10 @@ export function get_stream_ids(): number[] {
return all_rows.flatMap((row) => (row.type === "stream" ? row.stream_id : []));
}
export function section_ids(): string[] {
return current_sections.map((section) => section.id);
}
function current_section_ids_for_streams(): Map<number, StreamListSection> {
const map = new Map<number, StreamListSection>();
for (const section of current_sections) {

View File

@@ -14,6 +14,7 @@ import type {
unread_direct_message_info_schema,
} from "./state_data.ts";
import * as stream_data from "./stream_data.ts";
import * as stream_list_sort from "./stream_list_sort.ts";
import type {TopicHistoryEntry} from "./stream_topic_history.ts";
import * as sub_store from "./sub_store.ts";
import {user_settings} from "./user_settings.ts";
@@ -1026,6 +1027,46 @@ export function stream_has_any_unmuted_mentions(stream_id: number): boolean {
return streams_with_mentions.has(stream_id);
}
export function mention_counts_by_section(): Map<
string,
{
has_mentions: boolean;
has_unmuted_mentions: boolean;
}
> {
const mentions_map = new Map<
string,
{
has_mentions: boolean;
has_unmuted_mentions: boolean;
}
>();
const streams_with_mentions = unread_topic_counter.get_streams_with_unread_mentions();
const streams_with_unmuted_mentions = unread_topic_counter.get_streams_with_unmuted_mentions();
for (const stream_id of streams_with_mentions) {
const section_id = stream_list_sort.current_section_id_for_stream(stream_id);
if (section_id === undefined) {
continue;
}
if (!mentions_map.has(section_id)) {
mentions_map.set(section_id, {
has_mentions: false,
has_unmuted_mentions: false,
});
}
mentions_map.get(section_id)!.has_mentions = true;
}
for (const stream_id of streams_with_unmuted_mentions) {
const section_id = stream_list_sort.current_section_id_for_stream(stream_id);
if (section_id === undefined) {
continue;
}
mentions_map.get(section_id)!.has_unmuted_mentions = true;
}
return mentions_map;
}
export function topic_has_any_unread_mentions(stream_id: number, topic: string): boolean {
// Because this function is called in a loop for every displayed
// Recent Conversations row, it's important for it to run in O(1) time.

View File

@@ -367,6 +367,11 @@
below. This extra margin prevents that overlap. */
margin-bottom: 1px;
.markers-and-unreads {
display: flex;
align-items: center;
}
.unread_count,
.masked_unread_count {
margin-right: var(--left-sidebar-unread-offset);
@@ -536,6 +541,10 @@
visibility: hidden;
}
}
.stream-list-subsection-header .unread_mention_info {
display: none;
}
}
.direct-messages-container {

View File

@@ -10,6 +10,7 @@
</a>
{{/if}}
<div class="markers-and-unreads">
<span class="unread_mention_info"></span>
<span class="unread_count normal-count"></span>
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>

View File

@@ -176,6 +176,7 @@ test_ui("create_sidebar_row", ({override, override_rewire, mock_template}) => {
return `<stub-section-${section.id}>`;
});
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
const pinned_streams = [];
$("#stream-list-pinned-streams").append = (stream) => {
@@ -251,6 +252,7 @@ test_ui("create_sidebar_row", ({override, override_rewire, mock_template}) => {
});
test_ui("pinned_streams_never_inactive", ({mock_template, override_rewire}) => {
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
stream_data.add_sub(devel);
@@ -442,15 +444,15 @@ test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
});
test_ui("narrowing", ({override_rewire}) => {
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
initialize_stream_data();
topic_list.close = noop;
topic_list.rebuild_left_sidebar = noop;
topic_list.active_stream_id = noop;
topic_list.get_stream_li = noop;
override_rewire(stream_list, "scroll_stream_into_view", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
initialize_stream_data();
assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter"));
let filter;
@@ -570,6 +572,8 @@ test_ui("sort_streams", ({override_rewire}) => {
test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
// Get coverage on early-exit.
stream_list.build_stream_list();
@@ -623,6 +627,7 @@ test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
test_ui("rename_stream", ({mock_template, override, override_rewire}) => {
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
override(user_settings, "web_stream_unreads_count_display_policy", 3);
override(current_user, "user_id", me.user_id);
initialize_stream_data();