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(); const counts = unread.get_counts();
left_sidebar_navigation_area.update_dom_with_unread_counts(counts, false); left_sidebar_navigation_area.update_dom_with_unread_counts(counts, false);
update_dom_with_unread_counts(counts); update_dom_with_unread_counts(counts);
update_stream_section_mention_indicators();
sidebar_ui.update_unread_counts_visibility(); sidebar_ui.update_unread_counts_visibility();
set_sections_states(); set_sections_states();
$("#streams_list").toggleClass("is_searching", ui_util.get_left_sidebar_search_term() !== ""); $("#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 /* When viewing a channel in a collapsed folder, we show that active
highlighted channel in the left sidebar even though the folder is highlighted channel in the left sidebar even though the folder is
collapsed. If there's an active highlighted topic within the 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, stream_has_only_muted_unread_mentions,
); );
} }
update_stream_section_mention_indicators();
// (2) Unread counts in stream headers and collapse/uncollapse // (2) Unread counts in stream headers and collapse/uncollapse
// toggles for muted and inactive channels. // 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 : [])); 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> { function current_section_ids_for_streams(): Map<number, StreamListSection> {
const map = new Map<number, StreamListSection>(); const map = new Map<number, StreamListSection>();
for (const section of current_sections) { for (const section of current_sections) {

View File

@@ -14,6 +14,7 @@ import type {
unread_direct_message_info_schema, unread_direct_message_info_schema,
} from "./state_data.ts"; } from "./state_data.ts";
import * as stream_data from "./stream_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 type {TopicHistoryEntry} from "./stream_topic_history.ts";
import * as sub_store from "./sub_store.ts"; import * as sub_store from "./sub_store.ts";
import {user_settings} from "./user_settings.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); 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 { export function topic_has_any_unread_mentions(stream_id: number, topic: string): boolean {
// Because this function is called in a loop for every displayed // 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. // 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. */ below. This extra margin prevents that overlap. */
margin-bottom: 1px; margin-bottom: 1px;
.markers-and-unreads {
display: flex;
align-items: center;
}
.unread_count, .unread_count,
.masked_unread_count { .masked_unread_count {
margin-right: var(--left-sidebar-unread-offset); margin-right: var(--left-sidebar-unread-offset);
@@ -536,6 +541,10 @@
visibility: hidden; visibility: hidden;
} }
} }
.stream-list-subsection-header .unread_mention_info {
display: none;
}
} }
.direct-messages-container { .direct-messages-container {

View File

@@ -10,6 +10,7 @@
</a> </a>
{{/if}} {{/if}}
<div class="markers-and-unreads"> <div class="markers-and-unreads">
<span class="unread_mention_info"></span>
<span class="unread_count normal-count"></span> <span class="unread_count normal-count"></span>
<span class="masked_unread_count"> <span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i> <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}>`; return `<stub-section-${section.id}>`;
}); });
override_rewire(stream_list, "update_dom_with_unread_counts", noop); override_rewire(stream_list, "update_dom_with_unread_counts", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
const pinned_streams = []; const pinned_streams = [];
$("#stream-list-pinned-streams").append = (stream) => { $("#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}) => { 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); override_rewire(stream_list, "update_dom_with_unread_counts", noop);
stream_data.add_sub(devel); stream_data.add_sub(devel);
@@ -442,15 +444,15 @@ test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
}); });
test_ui("narrowing", ({override_rewire}) => { test_ui("narrowing", ({override_rewire}) => {
override_rewire(stream_list, "update_dom_with_unread_counts", noop);
initialize_stream_data();
topic_list.close = noop; topic_list.close = noop;
topic_list.rebuild_left_sidebar = noop; topic_list.rebuild_left_sidebar = noop;
topic_list.active_stream_id = noop; topic_list.active_stream_id = noop;
topic_list.get_stream_li = noop; topic_list.get_stream_li = noop;
override_rewire(stream_list, "scroll_stream_into_view", 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")); assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter"));
let filter; let filter;
@@ -570,6 +572,8 @@ test_ui("sort_streams", ({override_rewire}) => {
test_ui("separators_only_pinned_and_dormant", ({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_dom_with_unread_counts", noop);
override_rewire(stream_list, "update_stream_section_mention_indicators", noop);
// Get coverage on early-exit. // Get coverage on early-exit.
stream_list.build_stream_list(); 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}) => { test_ui("rename_stream", ({mock_template, override, override_rewire}) => {
override_rewire(stream_list, "update_dom_with_unread_counts", noop); 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(user_settings, "web_stream_unreads_count_display_policy", 3);
override(current_user, "user_id", me.user_id); override(current_user, "user_id", me.user_id);
initialize_stream_data(); initialize_stream_data();