From 4fbc8daf6614308459dd37f3f438dddf1baaffec Mon Sep 17 00:00:00 2001 From: Evy Kassirer Date: Tue, 6 Jan 2026 11:42:31 -0800 Subject: [PATCH] left_sidebar: Modal styling for more topics. Fixes #34471. --- web/src/inbox_ui.ts | 1 + web/src/left_sidebar_navigation_area.ts | 2 - web/src/pm_list.ts | 14 +- web/src/stream_list.ts | 230 ++++++---- web/src/stream_popover.ts | 17 +- web/src/topic_list.ts | 178 +++++--- web/src/topic_popover.ts | 2 +- web/styles/dark_theme.css | 6 - web/styles/left_sidebar.css | 414 ++++++++++-------- web/styles/zulip.css | 4 +- web/templates/left_sidebar.hbs | 11 +- .../show_inactive_or_muted_channels.hbs | 2 +- .../stream_list_section_container.hbs | 2 +- web/templates/stream_sidebar_row.hbs | 6 +- .../left_sidebar_navigation_area.test.cjs | 3 - web/tests/stream_list.test.cjs | 45 +- 16 files changed, 507 insertions(+), 430 deletions(-) diff --git a/web/src/inbox_ui.ts b/web/src/inbox_ui.ts index 346d950920..923fe51d30 100644 --- a/web/src/inbox_ui.ts +++ b/web/src/inbox_ui.ts @@ -1249,6 +1249,7 @@ function render_channel_view(channel_id: number): void { channel_view_topic_widget = new InboxTopicListWidget( $("#inbox-list"), channel_id, + false, (topic_names: string[]) => filter_topics_in_channel(channel_id, topic_names), ); channel_view_topic_widget.build(); diff --git a/web/src/left_sidebar_navigation_area.ts b/web/src/left_sidebar_navigation_area.ts index 81827d3208..341f27e9aa 100644 --- a/web/src/left_sidebar_navigation_area.ts +++ b/web/src/left_sidebar_navigation_area.ts @@ -76,12 +76,10 @@ export let update_dom_with_unread_counts = function ( const $mentioned_li = $(".top_left_mentions"); const $home_view_li = $(".selected-home-view"); const $condensed_view_li = $(".top_left_condensed_unread_marker"); - const $back_to_streams = $("#topics_header"); ui_util.update_unread_count_in_dom($mentioned_li, counts.mentioned_message_count); ui_util.update_unread_count_in_dom($home_view_li, counts.home_unread_messages); ui_util.update_unread_count_in_dom($condensed_view_li, counts.home_unread_messages); - ui_util.update_unread_count_in_dom($back_to_streams, counts.stream_unread_messages); if (!skip_animations) { animate_unread_changes($mentioned_li, counts.mentioned_message_count, last_mention_count); diff --git a/web/src/pm_list.ts b/web/src/pm_list.ts index af2870490a..8aba9fead2 100644 --- a/web/src/pm_list.ts +++ b/web/src/pm_list.ts @@ -341,8 +341,8 @@ function zoom_in(): void { pre_search_scroll_position = 0; ui_util.disable_left_sidebar_search(); update_private_messages(); - $("#left-sidebar").removeClass("zoom-out").addClass("zoom-in"); - $("#streams_list").hide(); + $("#left-sidebar").addClass("zoom-in"); + $("#left-sidebar").addClass("zoom-in-conversations"); $("#direct-messages-modal").toggleClass("no-display", false); const $filter = $(".direct-messages-list-filter").expectOne(); @@ -356,8 +356,8 @@ function zoom_out(): void { zoomed = false; ui_util.enable_left_sidebar_search(); clear_search(); - $("#left-sidebar").removeClass("zoom-in").addClass("zoom-out"); - $("#streams_list").show(); + $("#left-sidebar").removeClass("zoom-in"); + $("#left-sidebar").removeClass("zoom-in-conversations"); $("#direct-messages-modal").toggleClass("no-display", true); } @@ -391,11 +391,7 @@ export function initialize(): void { } }); - $("#left-sidebar").on("click", ".left-sidebar-modal-close-area", (e) => { - if ($("#direct-messages-modal.no-display").length > 0) { - // This can happen when zooming out of a topics modal - return; - } + $("body").on("click", ".zoom-in-conversations .left-sidebar-modal-close-area", (e) => { e.stopPropagation(); e.preventDefault(); diff --git a/web/src/stream_list.ts b/web/src/stream_list.ts index e486f43fd7..ac3458e6bd 100644 --- a/web/src/stream_list.ts +++ b/web/src/stream_list.ts @@ -19,6 +19,7 @@ import * as compose_actions from "./compose_actions.ts"; import type {Filter} from "./filter.ts"; import * as hash_util from "./hash_util.ts"; import {$t} from "./i18n.ts"; +import * as keydown_util from "./keydown_util.ts"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area.ts"; import {localstorage} from "./localstorage.ts"; import * as mouse_drag from "./mouse_drag.ts"; @@ -73,10 +74,14 @@ function zoom_in(): void { const stream_id = topic_list.active_stream_id(); assert(stream_id !== undefined); + $("#direct-messages-modal").toggleClass("no-display", true); popovers.hide_all(); pm_list.close(); - topic_list.zoom_in(); zoom_in_topics(stream_id); + topic_list.zoom_in(get_stream_li(stream_id)!); + $("#left-sidebar").addClass("zoom-in"); + $("#left-sidebar").addClass("zoom-in-topics"); + $("#left-sidebar-modal").addClass("zoom-in-topics"); } export function set_pending_stream_list_rerender(value: boolean): void { @@ -600,7 +605,16 @@ export function rewire_set_sections_states(value: typeof set_sections_states): v set_sections_states = value; } -export function get_stream_li(stream_id: number): JQuery | undefined { +export function get_stream_li(stream_id: number, override_zoomed_in?: boolean): JQuery | undefined { + const for_zoomed = override_zoomed_in ?? zoomed_in; + if (for_zoomed) { + assert(zoomed_in_row !== undefined); + if (zoomed_in_row.sub.stream_id !== stream_id) { + return undefined; + } + return zoomed_in_row.$list_item; + } + const row = stream_sidebar.get_row(stream_id); if (!row) { // Not all streams are in the sidebar, so we don't report @@ -655,9 +669,9 @@ class StreamSidebarRow { sub: StreamSubscription; $list_item: JQuery; - constructor(sub: StreamSubscription) { + constructor(sub: StreamSubscription, for_modal = false) { this.sub = sub; - this.$list_item = build_stream_sidebar_li(sub); + this.$list_item = build_stream_sidebar_li(sub, for_modal); this.update_unread_count(); } @@ -702,32 +716,27 @@ class StreamSidebarRow { } } +let zoomed_in_row: StreamSidebarRow | undefined; export function zoom_in_topics(stream_id: number): void { - // This only does stream-related tasks related to zooming - // in to more topics, which is basically hiding all the - // other streams. + // This only does channel-related tasks related to zooming + // in to more topics, which is creating the sidebar row + // and setting up the channel's more topics modal. + const sub = sub_store.get(stream_id); + assert(sub !== undefined); - $("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in"); - - $("#stream_filters li.narrow-filter").each(function () { - const $elt = $(this); - - if (stream_id_for_elt($elt) === stream_id) { - $elt.toggleClass("hide", false); - // Add search box for topics list. - $elt.children("div.bottom_left_row").append($(render_filter_topics())); - topic_list.setup_topic_search_typeahead(); - } else { - $elt.toggleClass("hide", true); - } - }); + zoomed_in_row = new StreamSidebarRow(sub, true); + $("#more-topics-modal").replaceWith(zoomed_in_row.$list_item); + $("#more-topics-modal").find("div.bottom_left_row").append($(render_filter_topics())); + topic_list.setup_topic_search_typeahead(); } export function zoom_out_topics(): void { - $("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out"); + $("#left-sidebar").removeClass("zoom-in"); + $("#left-sidebar").removeClass("zoom-in-topics"); + $("#left-sidebar-modal").removeClass("zoom-in-topics"); $("#stream_filters li.narrow-filter").toggleClass("hide", false); - // Remove search box for topics list from DOM. - $(".filter-topics").remove(); + $("#more-topics-modal").empty(); + zoomed_in_row = undefined; } export function set_in_home_view(stream_id: number, in_home: boolean): void { @@ -744,7 +753,7 @@ export function set_in_home_view(stream_id: number, in_home: boolean): void { } } -function build_stream_sidebar_li(sub: StreamSubscription): JQuery { +function build_stream_sidebar_li(sub: StreamSubscription, for_modal = false): JQuery { const name = sub.name; const is_muted = stream_data.is_muted(sub.stream_id); const can_post_messages = stream_data.can_post_messages_in_stream(sub); @@ -763,6 +772,7 @@ function build_stream_sidebar_li(sub: StreamSubscription): JQuery { sub.stream_id, ), is_empty_topic_only_channel: stream_data.is_empty_topic_only_channel(sub.stream_id), + for_modal, }; const $list_item = $(render_stream_sidebar_row(args)); return $list_item; @@ -808,7 +818,10 @@ function set_stream_unread_count( stream_has_any_unmuted_unread_mention: boolean, stream_has_only_muted_unread_mentions: boolean, ): void { - const $stream_li = get_stream_li(stream_id); + // Update the unread count for the regular stream list, even if we're + // currently zoomed in, so that it has the correct number when we zoom + // out. + const $stream_li = get_stream_li(stream_id, false); if (!$stream_li) { // This can happen for legitimate reasons, but we warn // just in case. @@ -822,6 +835,23 @@ function set_stream_unread_count( stream_has_any_unmuted_unread_mention, stream_has_only_muted_unread_mentions, ); + + if (zoomed_in) { + const $stream_li = get_stream_li(stream_id); + if (!$stream_li) { + // This can happen for legitimate reasons, but we warn + // just in case. + blueslip.warn("stream id no longer in sidebar: " + stream_id); + return; + } + update_count_in_dom( + $stream_li, + count, + stream_has_any_unread_mention_messages, + stream_has_any_unmuted_unread_mention, + stream_has_only_muted_unread_mentions, + ); + } } export let update_streams_sidebar = (force_rerender = false): void => { @@ -1134,7 +1164,7 @@ export function update_stream_sidebar_for_narrow(filter: Filter): JQuery | undef return undefined; } - if (!info.topic_selected) { + if (!info.topic_selected && !zoomed_in) { $stream_li.addClass("active-filter"); } @@ -1176,13 +1206,15 @@ export function handle_narrow_activated( change_hash: boolean, show_more_topics: boolean, ): void { + // Zoom out, if needed, so that get_stream_li returns the correct + // value when calling update_stream_sidebar_for_narrow. + if (!change_hash && is_zoomed_in() && !show_more_topics) { + zoom_out(); + } + const $stream_li = update_stream_sidebar_for_narrow(filter); - if ($stream_li && !change_hash) { - if (!is_zoomed_in() && show_more_topics) { - zoom_in(); - } else if (is_zoomed_in() && !show_more_topics) { - zoom_out(); - } + if ($stream_li && !change_hash && !is_zoomed_in() && show_more_topics) { + zoom_in(); } scroll_stream_into_view(); @@ -1227,7 +1259,19 @@ export function initialize({ e.stopPropagation(); }); - $(".show-all-streams").on("click", (e) => { + $("body").on("click", ".zoom-in-topics .left-sidebar-modal-close-area", (e) => { + zoom_out(); + browser_history.update_current_history_state_data({show_more_topics: false}); + + e.preventDefault(); + e.stopPropagation(); + }); + + $("body").on("keydown", ".zoom-in-topics .left-sidebar-modal-close-area", (e) => { + if (!keydown_util.is_enter_event(e)) { + return; + } + zoom_out(); browser_history.update_current_history_state_data({show_more_topics: false}); @@ -1237,41 +1281,47 @@ export function initialize({ } export function initialize_tippy_tooltips(): void { - tippy.delegate("body", { - target: "#stream_filters li .subscription_block .stream-name", - delay: LONG_HOVER_DELAY, - onShow(instance) { - // check for "Go to channel feed" tooltip conditions first. - const stream_id = stream_id_for_elt($(instance.reference).parents("li.narrow-filter")); - const current_narrow_stream_id = narrow_state.stream_id(); - const current_topic = narrow_state.topic(); - if (current_narrow_stream_id === stream_id && current_topic !== undefined) { - if ( - user_settings.web_channel_default_view === - web_channel_default_view_values.list_of_topics.code - ) { - instance.setContent( - ui_util.parse_html(render_go_to_channel_list_of_topics_tooltip()), - ); - } else { - instance.setContent(ui_util.parse_html(render_go_to_channel_feed_tooltip())); + for (const parent_class of ["#stream_filters li", "#left-sidebar-modal"]) { + tippy.delegate("body", { + target: `${parent_class} .subscription_block .stream-name`, + delay: LONG_HOVER_DELAY, + onShow(instance) { + // check for "Go to channel feed" tooltip conditions first. + const stream_id = stream_id_for_elt( + $(instance.reference).parents("li.narrow-filter"), + ); + const current_narrow_stream_id = narrow_state.stream_id(); + const current_topic = narrow_state.topic(); + if (current_narrow_stream_id === stream_id && current_topic !== undefined) { + if ( + user_settings.web_channel_default_view === + web_channel_default_view_values.list_of_topics.code + ) { + instance.setContent( + ui_util.parse_html(render_go_to_channel_list_of_topics_tooltip()), + ); + } else { + instance.setContent( + ui_util.parse_html(render_go_to_channel_feed_tooltip()), + ); + } + return undefined; } - return undefined; - } - // Then check for truncation - const stream_name_element = instance.reference; - assert(stream_name_element instanceof HTMLElement); + // Then check for truncation + const stream_name_element = instance.reference; + assert(stream_name_element instanceof HTMLElement); - if (stream_name_element.offsetWidth < stream_name_element.scrollWidth) { - const stream_name = stream_name_element.textContent ?? ""; - instance.setContent(stream_name); - return undefined; - } + if (stream_name_element.offsetWidth < stream_name_element.scrollWidth) { + const stream_name = stream_name_element.textContent ?? ""; + instance.setContent(stream_name); + return undefined; + } - return false; - }, - appendTo: () => document.body, - }); + return false; + }, + appendTo: () => document.body, + }); + } } export function on_sidebar_channel_click( @@ -1405,28 +1455,36 @@ export function set_event_handlers({ on_sidebar_channel_click(stream_id, e, show_channel_feed); }); - $("#stream_filters").on( + function on_click_new_topic(element: HTMLElement, e: JQuery.ClickEvent): void { + e.stopPropagation(); + e.preventDefault(); + const stream_id = Number.parseInt(element.getAttribute("data-stream-id")!, 10); + let trigger = "clear topic button"; + let topic = ""; + + if ($(e.target).closest(".zoomed-new-topic").length > 0) { + trigger = "zoomed new topic"; + topic = $("#topic_filter_query").text().trim().slice(0, realm.max_topic_length); + } + + compose_actions.start({ + message_type: "stream", + stream_id, + topic, + trigger, + keep_composebox_empty: true, + }); + } + + $("#stream_filters").on("click", ".channel-new-topic-button", function (this: HTMLElement, e) { + on_click_new_topic(this, e); + }); + + $("#left-sidebar-modal").on( "click", - ".channel-new-topic-button, .zoomed-new-topic", + "#more-topics-modal .channel-new-topic-button, #more-topics-modal .zoomed-new-topic", function (this: HTMLElement, e) { - e.stopPropagation(); - e.preventDefault(); - const stream_id = Number.parseInt(this.getAttribute("data-stream-id")!, 10); - let trigger = "clear topic button"; - let topic = ""; - - if ($(e.target).closest(".zoomed-new-topic").length > 0) { - trigger = "zoomed new topic"; - topic = $("#topic_filter_query").text().trim().slice(0, realm.max_topic_length); - } - - compose_actions.start({ - message_type: "stream", - stream_id, - topic, - trigger, - keep_composebox_empty: true, - }); + on_click_new_topic(this, e); }, ); diff --git a/web/src/stream_popover.ts b/web/src/stream_popover.ts index 84ecf160f1..4499ea00d2 100644 --- a/web/src/stream_popover.ts +++ b/web/src/stream_popover.ts @@ -1220,19 +1220,30 @@ export async function build_move_topic_to_stream_popover( } export function initialize(): void { - $("#stream_filters").on("click", ".stream-sidebar-menu-icon", function (this: HTMLElement, e) { + function on_sidebar_menu_icon_click(element: HTMLElement, e: JQuery.ClickEvent): void { e.preventDefault(); - const $stream_li = $(this).parents("li"); + const $stream_li = $(element).parents("li"); const stream_id = elem_to_stream_id($stream_li); build_stream_popover({ - elt: this, + elt: element, stream_id, }); e.stopPropagation(); + } + $("#stream_filters").on("click", ".stream-sidebar-menu-icon", function (this: HTMLElement, e) { + on_sidebar_menu_icon_click(this, e); }); + $("#left-sidebar-modal").on( + "click", + "#more-topics-modal .stream-sidebar-menu-icon", + function (this: HTMLElement, e) { + on_sidebar_menu_icon_click(this, e); + }, + ); + $("body").on("click", ".inbox-stream-menu", function (this: HTMLElement, e) { const stream_id = Number.parseInt($(this).attr("data-stream-id")!, 10); diff --git a/web/src/topic_list.ts b/web/src/topic_list.ts index d42a28cca8..65ac4d4d4f 100644 --- a/web/src/topic_list.ts +++ b/web/src/topic_list.ts @@ -31,6 +31,7 @@ import * as vdom from "./vdom.ts"; one for now, but we may eventually allow multiple streams to be expanded. */ const active_widgets = new Map(); +let zoomed_in_widget: LeftSidebarTopicListWidget | undefined; export let topic_filter_pill_widget: TopicFilterPillWidget | null = null; export let topic_state_typeahead: Typeahead | undefined; @@ -45,6 +46,9 @@ export function update(): void { for (const widget of active_widgets.values()) { widget.build(); } + if (zoomed_in_widget) { + zoomed_in_widget.build(); + } } export function update_widget_for_stream(stream_id: number): void { @@ -54,6 +58,10 @@ export function update_widget_for_stream(stream_id: number): void { return; } widget.build(); + + if (zoomed_in_widget?.my_stream_id === stream_id) { + zoomed_in_widget.build(); + } } export function clear(): void { @@ -67,6 +75,12 @@ export function clear(): void { active_widgets.clear(); } +export function clear_zoomed(): void { + popover_menus.get_topic_menu_popover()?.hide(); + topic_filter_pill_widget?.clear(true); + zoomed_in_widget?.remove(); +} + export function close(): void { clear(); if (zoomed) { @@ -83,14 +97,13 @@ export function zoom_out(): void { zoomed = false; ui_util.enable_left_sidebar_search(); - const stream_ids = [...active_widgets.keys()]; - - if (stream_ids.length !== 1 || stream_ids[0] === undefined) { - blueslip.error("Unexpected number of topic lists to zoom out."); + const stream_id = zoomed_in_widget?.my_stream_id; + if (stream_id === undefined) { + blueslip.error("Expected a topic list to zoom out."); return; } + zoomed_in_widget = undefined; - const stream_id = stream_ids[0]; const widget = active_widgets.get(stream_id); assert(widget !== undefined); const $stream_li = widget.get_stream_li(); @@ -274,26 +287,28 @@ export class TopicListWidget { prior_dom: vdom.Tag | undefined = undefined; $stream_li: JQuery; my_stream_id: number; + for_modal: boolean; filter_topics: (topic_names: string[]) => string[]; constructor( $stream_li: JQuery, my_stream_id: number, + for_modal: boolean, filter_topics: (topic_names: string[]) => string[], ) { this.$stream_li = $stream_li; this.my_stream_id = my_stream_id; + this.for_modal = for_modal; this.filter_topics = filter_topics; } build_list( spinner: boolean, formatter: (conversation: TopicInfo) => ListInfoNode, - is_zoomed: boolean, ): vdom.Tag { const list_info = topic_list_data.get_list_info( this.my_stream_id, - is_zoomed, + this.for_modal, this.filter_topics, ); @@ -328,7 +343,7 @@ export class TopicListWidget { list_info.more_topics_unread_count_muted, ), ); - } else if (is_zoomed && stream_data.can_post_messages_in_stream(stream)) { + } else if (this.for_modal && stream_data.can_post_messages_in_stream(stream)) { nodes.push(new_topic(this.my_stream_id)); } @@ -353,16 +368,17 @@ export class TopicListWidget { this.prior_dom = undefined; } - build( - spinner = false, - formatter: (conversation: TopicInfo) => ListInfoNode, - is_zoomed: boolean, - ): void { - const new_dom = this.build_list(spinner, formatter, is_zoomed); - + build(spinner = false, formatter: (conversation: TopicInfo) => ListInfoNode): void { + const new_dom = this.build_list(spinner, formatter); const replace_content = (html: string): void => { this.remove(); - this.$stream_li.append($(html)); + if (this.for_modal && this.$stream_li.find(".simplebar-content").length > 0) { + this.$stream_li.find(".simplebar-content").append($(html)); + } else if (this.for_modal) { + this.$stream_li.find(".topic-list-scroll-container").append($(html)); + } else { + this.$stream_li.append($(html)); + } }; const find = (): JQuery => this.$stream_li.find(`.${this.topic_list_class_name}`); @@ -393,15 +409,14 @@ function filter_topics_left_sidebar(topic_names: string[]): string[] { } export class LeftSidebarTopicListWidget extends TopicListWidget { - constructor($stream_li: JQuery, my_stream_id: number) { - super($stream_li, my_stream_id, filter_topics_left_sidebar); + constructor($stream_li: JQuery, my_stream_id: number, for_modal: boolean) { + super($stream_li, my_stream_id, for_modal, filter_topics_left_sidebar); } override build(spinner = false): void { - const is_zoomed = zoomed; const formatter = keyed_topic_li; - super.build(spinner, formatter, is_zoomed); + super.build(spinner, formatter); } } @@ -421,6 +436,10 @@ export function clear_topic_search(e: JQuery.Event): void { } export function active_stream_id(): number | undefined { + if (zoomed) { + return zoomed_in_widget?.my_stream_id; + } + const stream_ids = [...active_widgets.keys()]; if (stream_ids.length !== 1) { @@ -431,6 +450,10 @@ export function active_stream_id(): number | undefined { } export function get_stream_li(): JQuery | undefined { + if (zoomed) { + return zoomed_in_widget?.get_stream_li(); + } + const widgets = [...active_widgets.values()]; if (widgets.length !== 1 || widgets[0] === undefined) { @@ -442,6 +465,16 @@ export function get_stream_li(): JQuery | undefined { } export function rebuild_left_sidebar($stream_li: JQuery, stream_id: number): void { + if (zoomed) { + if (zoomed_in_widget?.my_stream_id !== stream_id) { + clear_zoomed(); + zoomed_in_widget = new LeftSidebarTopicListWidget($stream_li, stream_id, true); + } + zoomed_in_widget.build(); + return; + } + + zoomed_in_widget?.remove(); const active_widget = active_widgets.get(stream_id); if (active_widget) { @@ -450,51 +483,58 @@ export function rebuild_left_sidebar($stream_li: JQuery, stream_id: number): voi } clear(); - const widget = new LeftSidebarTopicListWidget($stream_li, stream_id); + const widget = new LeftSidebarTopicListWidget($stream_li, stream_id, false); widget.build(); active_widgets.set(stream_id, widget); } export function left_sidebar_scroll_zoomed_in_topic_into_view(): void { - const $selected_topic = $(".topic-list .topic-list-item.active-sub-filter"); + const $scroll_container = zoomed + ? $("#more-topics-modal .topic-list-scroll-container") + : $("#left_sidebar_scroll_container"); + const $selected_topic = $scroll_container.find(".topic-list-item.active-sub-filter"); if ($selected_topic.length === 0) { // If we don't have a selected topic, scroll to top. - scroll_util.get_scroll_element($("#left_sidebar_scroll_container")).scrollTop(0); + scroll_util.get_scroll_element($scroll_container).scrollTop(0); return; } - const $container = $("#left_sidebar_scroll_container"); - let sticky_header_height = 0; if (zoomed) { - const stream_header_height = - $( - "#streams_list.zoom-in .narrow-filter.stream-expanded > .bottom_left_row", - ).outerHeight(true) ?? 0; - const topic_header_height = - $("#streams_list.zoom-in #topics_header").outerHeight(true) ?? 0; - sticky_header_height += stream_header_height + topic_header_height; - } - const channel_folder_header_height = - $selected_topic - .closest(".stream-list-section-container") - .find(".stream-list-subsection-header") - .outerHeight(true) ?? 0; - sticky_header_height += channel_folder_header_height; - const $topic_list = $selected_topic.closest(".topic-list"); - const topic_list_height = $topic_list.outerHeight(true) ?? 0; - const available_topic_height = ($container.height() ?? 0) - sticky_header_height; + scroll_util.scroll_element_into_container($selected_topic, $scroll_container, 0); + } else { + const direct_message_header_height = + $("#direct-messages-section-header").outerHeight(true) ?? 0; + const direct_message_divider_height = + $(".direct-message-section-bottom-divider-container").outerHeight(true) ?? 0; + const channel_folder_header_height = + $selected_topic + .closest(".stream-list-section-container") + .find(".stream-list-subsection-header") + .outerHeight(true) ?? 0; + const sticky_header_height = + direct_message_header_height + + direct_message_divider_height + + channel_folder_header_height; - let $scroll_target = $selected_topic; - if (topic_list_height <= available_topic_height) { - $scroll_target = $topic_list; + const $channel_section = $selected_topic.closest(".stream-expanded"); + const channel_section_height = $channel_section.outerHeight(true) ?? 0; + const available_topic_height = ($scroll_container.height() ?? 0) - sticky_header_height; + + let $scroll_target = $selected_topic; + if (channel_section_height <= available_topic_height) { + $scroll_target = $channel_section; + } + scroll_util.scroll_element_into_container( + $scroll_target, + $scroll_container, + sticky_header_height, + ); } - scroll_util.scroll_element_into_container($scroll_target, $container, sticky_header_height); } // For zooming, we only do topic-list stuff here...let stream_list // handle hiding/showing the non-narrowed streams -export function zoom_in(): void { - zoomed = true; +export function zoom_in($stream_li: JQuery): void { previous_search_term = ""; pre_search_scroll_position = 0; ui_util.disable_left_sidebar_search(); @@ -505,8 +545,10 @@ export function zoom_in(): void { return; } - const active_widget = active_widgets.get(stream_id); - assert(active_widget !== undefined); + zoomed = true; + zoomed_in_widget = new LeftSidebarTopicListWidget($stream_li, stream_id, true); + const spinner = true; + zoomed_in_widget.build(spinner); function on_success(): void { if (!active_widgets.has(stream_id!)) { @@ -523,19 +565,14 @@ export function zoom_in(): void { return; } - active_widget!.build(); - if (zoomed) { - // It is fine to force scroll here even if user has scrolled to a different - // position since we just added some topics to the list which moved user - // to a different position anyway. - left_sidebar_scroll_zoomed_in_topic_into_view(); - topic_state_typeahead?.lookup(true); - } + rebuild_left_sidebar($stream_li, stream_id!); + // It is fine to force scroll here even if user has scrolled to a different + // position since we just added some topics to the list which moved user + // to a different position anyway. + left_sidebar_scroll_zoomed_in_topic_into_view(); + topic_state_typeahead?.lookup(true); } - const spinner = true; - active_widget.build(spinner); - stream_topic_history_util.get_server_history(stream_id, on_success); left_sidebar_scroll_zoomed_in_topic_into_view(); } @@ -667,12 +704,8 @@ export function setup_topic_search_typeahead(): void { }); topic_filter_pill_widget.onPillRemove(() => { - const stream_id = active_stream_id(); - if (stream_id !== undefined) { - const widget = active_widgets.get(stream_id); - if (widget) { - widget.build(); - } + if (zoomed_in_widget) { + zoomed_in_widget.build(); } }); } @@ -682,7 +715,7 @@ export function initialize({ }: { on_topic_click: (stream_id: number, topic: string) => void; }): void { - $("#stream_filters").on("click", ".topic-box", (e) => { + function on_topic_box_click(e: JQuery.ClickEvent): void { const $target = $(e.target); if (e.metaKey || e.ctrlKey || e.shiftKey) { return; @@ -720,7 +753,9 @@ export function initialize({ e.preventDefault(); e.stopPropagation(); - }); + } + $("#more-topics-modal").on("click", ".topic-box", on_topic_box_click); + $("#stream_filters").on("click", ".topic-box", on_topic_box_click); $("body").on("input", "#left-sidebar-filter-topic-input", (): void => { const stream_id = active_stream_id(); @@ -730,11 +765,10 @@ export function initialize({ const is_previous_search_term_empty = previous_search_term === ""; previous_search_term = search_term; - const widget = active_widgets.get(stream_id)!; const left_sidebar_scroll_container = scroll_util.get_left_sidebar_scroll_container(); if (search_term === "") { requestAnimationFrame(() => { - widget.build(); + zoomed_in_widget!.build(); // Restore previous scroll position. left_sidebar_scroll_container.scrollTop(pre_search_scroll_position); }); @@ -757,7 +791,7 @@ export function initialize({ pre_search_scroll_position = left_sidebar_scroll_container.scrollTop()!; } requestAnimationFrame(() => { - widget.build(); + zoomed_in_widget!.build(); // Always scroll to top when there is a search term present. left_sidebar_scroll_container.scrollTop(0); }); diff --git a/web/src/topic_popover.ts b/web/src/topic_popover.ts index 68a9ae0354..5845d90020 100644 --- a/web/src/topic_popover.ts +++ b/web/src/topic_popover.ts @@ -86,7 +86,7 @@ export function initialize(): void { }); popover_menus.register_popover_menu( - "#stream_filters .topic-sidebar-menu-icon, .inbox-row .inbox-topic-menu, .recipient-row-topic-menu, .recent_view_focusable .visibility-status-icon", + "#stream_filters .topic-sidebar-menu-icon, #more-topics-modal .topic-sidebar-menu-icon, .inbox-row .inbox-topic-menu, .recipient-row-topic-menu, .recent_view_focusable .visibility-status-icon", { ...popover_menus.left_sidebar_tippy_options, onShow(instance) { diff --git a/web/styles/dark_theme.css b/web/styles/dark_theme.css index 8ea233b40c..d205883ace 100644 --- a/web/styles/dark_theme.css +++ b/web/styles/dark_theme.css @@ -107,12 +107,6 @@ opacity: 0.2; } - .zoom-in { - #topics_header { - background-color: var(--color-background); - } - } - #recent_view_table { .zulip-icon-user { opacity: 0.7; diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css index c0d87bd788..d2b51645aa 100644 --- a/web/styles/left_sidebar.css +++ b/web/styles/left_sidebar.css @@ -245,13 +245,16 @@ margin: 0; } -#left-sidebar-modal-content #direct-messages-modal { +#left-sidebar-modal-content #direct-messages-modal, +#left-sidebar-modal-content #more-topics-modal { display: grid; grid-template: "section-header" minmax(0, max-content) "conversation-list" minmax(0, 1fr) / minmax(0, 1fr); max-height: 100%; +} +#left-sidebar-modal-content #direct-messages-modal { #direct-messages-modal-section-header, .dm-list { margin-right: var(--left-sidebar-right-margin); @@ -327,12 +330,17 @@ } #direct-messages-section-header { - grid-template-columns: - 0 var(--left-sidebar-header-icon-toggle-width) 0 minmax(0, 1fr) - minmax(0, max-content) minmax(0, max-content) var( - --left-sidebar-vdots-width - ) - 0; + display: grid; + align-items: center; + /* This extends the general pattern of left sidebar rows, but includes a + second grid row for placing filter boxes. */ + grid-template-areas: + "starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset" + "filter-container filter-container filter-container filter-container filter-container filter-container filter-container filter-container"; + grid-template-rows: var(--line-height-sidebar-row-prominent) minmax( + 0, + max-content + ); &:hover { background-color: var(--color-background-hover-narrow-filter); @@ -849,10 +857,10 @@ .narrow-filter .topic-list .bottom_left_row:has(a.topic-box:focus-visible), +#more-topics-modal .bottom_left_row:has(a.topic-box:focus-visible), #direct-messages-list .dm-list .bottom_left_row:has(a.dm-box:focus-visible), #modal-direct-messages-list .bottom_left_row:has(a.dm-box:focus-visible), #show-more-direct-messages:has(a.dm-name:focus-visible), -#topics_header .show-all-streams:focus-visible, #subscribe-to-more-streams:has(.subscribe-more-link:focus-visible), #login-to-more-streams:has(.subscribe-more-link:focus-visible) { outline: 2px solid var(--color-outline-focus); @@ -965,7 +973,10 @@ ul.filters { text-decoration: none; } } +} +#more-topics-modal .topic-list, +ul.filters { .sidebar-topic-action-heading { &:focus { color: var(--color-text-sidebar-action-heading); @@ -1503,32 +1514,10 @@ li.active-sub-filter { } } -#direct-messages-section-header, -#topics_header { - display: grid; - align-items: center; - /* This extends the general pattern of left sidebar rows, but includes a - second grid row for placing filter boxes. */ - grid-template-areas: - "starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset" - "filter-container filter-container filter-container filter-container filter-container filter-container filter-container filter-container"; - grid-template-rows: var(--line-height-sidebar-row-prominent) minmax( - 0, - max-content - ); -} - .left-sidebar-filter-input-container { + display: block; + margin-bottom: 5px; grid-area: filter-container; - display: grid; - align-items: center; - grid-template: - "starting-offset filter-input ending-offset" minmax(0, max-content) - / calc( - var(--left-sidebar-toggle-width-offset) - - var(--input-icon-starting-offset) - ) - minmax(0, 1fr) 0; .filter-input, .filter-topics { @@ -1558,6 +1547,35 @@ li.active-sub-filter { display: none; } +#views-label-container.showing-condensed-navigation, +.left-sidebar.zoom-in #views-label-container { + /* Use a next-sibling combinator (+) to use CSS to show and hide + filter rows as needed, based on the narrow. */ + + #left-sidebar-navigation-list { + /* In the condensed state, we don't want to generate + auto rows, or there will be a footprint where the + expanded nav sits. */ + grid-auto-rows: unset; + /* When the navigation area is condensed, hide all + the rows in the full navigation list... */ + & .top_left_row { + display: none; + } + /* ...except when there is an active filter in place: + that row should still be shown. */ + & .top_left_row.top-left-active-filter { + display: grid; + /* In the absence of auto rows in the condensed state, + we set an explicit height on the active filter. */ + height: var(--line-height-sidebar-row); + } + } + + .zulip-icon-heading-triangle-right { + rotate: 0deg; + } +} + #views-label-container { margin-right: var(--left-sidebar-right-margin); grid-template-columns: @@ -1580,30 +1598,6 @@ li.active-sub-filter { } } - /* Use a next-sibling combinator (+) to use CSS to show and hide - filter rows as needed, based on the narrow. */ - &.showing-condensed-navigation { - + #left-sidebar-navigation-list { - /* In the condensed state, we don't want to generate - auto rows, or there will be a footprint where the - expanded nav sits. */ - grid-auto-rows: unset; - /* When the navigation area is condensed, hide all - the rows in the full navigation list... */ - & .top_left_row { - display: none; - } - /* ...except when there is an active filter in place: - that row should still be shown. */ - & .top_left_row.top-left-active-filter { - display: grid; - /* In the absence of auto rows in the condensed state, - we set an explicit height on the active filter. */ - height: var(--line-height-sidebar-row); - } - } - } - /* Remove the cursor: pointer property of Views label for the spectators. */ &.remove-pointer-for-spectator { cursor: default; @@ -1955,6 +1949,22 @@ li.active-sub-filter { align-self: baseline; } +#more-topics-modal .topic-box { + grid-template: + "starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset" + var(--line-height-sidebar-row-prominent) + ". . . row-content . . . . " + auto + / var(--input-icon-starting-offset) var( + --left-sidebar-icon-column-width + ) + var(--left-sidebar-icon-content-gap) minmax(0, 1fr) minmax( + 0, + max-content + ) + minmax(0, max-content) var(--left-sidebar-vdots-width) 0; +} + .zoomed-new-topic { display: grid; grid-template: @@ -2197,9 +2207,9 @@ ul.topic-list:has(.show-more-topics)::after { } /* The grouping border should not be shown - on zoomed-in views. */ -.zoom-in .topic-list.topic-list-has-topics::before, -.zoom-in .topic-list.topic-list-has-topics::after { + in the more topics modal. */ +#more-topics-modal .topic-list.topic-list-has-topics::before, +#more-topics-modal .topic-list.topic-list-has-topics::after { border: 0; } @@ -2262,62 +2272,6 @@ li.topic-list-item { } } -#streams_list.zoom-out #topics_header { - display: none; -} - -#topics_header { - display: grid; - position: sticky; - top: 0; - z-index: 2; - grid-template-columns: - [topics-content-area-start] var(--left-sidebar-toggle-width-offset) - 0 0 minmax(0, 1fr) 0 - max-content 0 var(--left-sidebar-vdots-width) - [topics-content-area-end] var(--left-sidebar-right-margin); - grid-template-rows: - [topics-content-area-start] var(--line-height-sidebar-row-prominent) - [topics-content-area-end] 0; - padding-top: var(--left-sidebar-sections-vertical-gutter); - color: hsl(0deg 0% 43%); - background-color: var(--color-background); - /* With quiet unreads, we want the BACK TO CHANNELS - and unread count to share a common baseline. */ - line-height: var(--line-height-sidebar-row); - align-items: baseline; - - .show-all-streams { - grid-area: topics-content-area; - padding-left: var(--left-sidebar-toggle-width-offset); - border-radius: 4px; - font-size: var(--font-size-sidebar-action-heading); - font-weight: var(--font-weight-sidebar-action-heading); - font-variant: var(--font-variant-sidebar-action-heading); - text-transform: var(--text-transform-sidebar-action-heading); - color: var(--color-text-sidebar-action-heading); - text-decoration: none; - outline: none; - - &:hover { - background-color: var( - --color-background-sidebar-action-heading-hover - ); - box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover); - } - } - - .unread_count { - grid-area: markers-and-unreads; - /* Extra margin for unreads. */ - margin-right: var(--left-sidebar-unread-offset); - - &:empty { - margin-right: 0; - } - } -} - .zero_count { visibility: hidden; } @@ -2326,63 +2280,38 @@ li.topic-list-item { margin-right: 30px; } -.zoom-in { - .narrow-filter.hide { - display: none; - } +#more-topics-modal > .channel-header { + position: sticky; + top: 0; + z-index: 2; + padding-bottom: 1px; + background-color: var(--color-background); - .narrow-filter > .bottom_left_row { - position: sticky; - /* We need to hold the space where the BACK TO CHANNELS - line sits, so the channel info doesn't run over the - top of it when scrolling down. These are the same - variables for setting the space on the BACK TO CHANNELS - grid row plus its top padding: */ - top: calc( - var(--line-height-sidebar-row-prominent) + - var(--left-sidebar-sections-vertical-gutter) - ); - z-index: 2; - padding-bottom: 1px; + &:hover { + /* Prevent hover styles set on other rows. */ + box-shadow: none; background-color: var(--color-background); - - &:hover { - /* Prevent hover styles set on other rows. */ - box-shadow: none; - background-color: var(--color-background); - } - - /* We avoid putting the box-shadow around both - the channel row and the filter input it contains, - as there is no hover effect on channel rows when - zoomed in, making a preserve-the-hover-outline - effect here moot. */ - &:has(.left_sidebar_menu_icon_visible) { - box-shadow: none; - } } - /* When zooming in on a channel that's serving as an - active filter, keep the background colors in line - with the active-narrow-filter colors. */ - .narrow-filter.active-filter > .bottom_left_row { - background-color: var(--color-background-active-narrow-filter); - - &:hover { - background-color: var(--color-background-active-narrow-filter); - } + /* We avoid putting the box-shadow around both + the channel row and the filter input it contains, + as there is no hover effect on channel rows when + zoomed in, making a preserve-the-hover-outline + effect here moot. */ + &:has(.left_sidebar_menu_icon_visible) { + box-shadow: none; } - #subscribe-to-more-streams, - #login-to-more-streams, - .show-more-topics { - display: none; + .subscription_block:focus-visible { + outline: 2px solid var(--color-outline-focus); + border-radius: 4px; + background-color: var(--color-background-hover-narrow-filter); } +} - .zoom-in-hide, - #left-sidebar-search.zoom-in-hide { - display: none; - } +#more-topics-modal .bottom_left_row:last-of-type { + /* Needed for the scroll container to not cut off the hover border */ + margin-bottom: 5px; } .top_left_row.hidden-by-filters { @@ -2408,12 +2337,6 @@ li.topic-list-item { } } -#left-sidebar.zoom-out #left-sidebar-modal { - /* Hidden instead of display: none so that we scroll to the - correct height more easily as it becomes visible again */ - visibility: hidden; -} - #left-sidebar.zoom-in { #left_sidebar_scroll_container { /* Hidden instead of display: none so that we scroll to the @@ -2426,18 +2349,95 @@ li.topic-list-item { } } -#left-sidebar.zoom-in #left-sidebar-modal { +/* Show the DM header when zoomed into topics, visible behind + the back button area */ +#left-sidebar.zoom-in-topics #direct-messages-section-header { + visibility: visible; +} + +#left-sidebar-modal { + /* Hidden instead of display: none so that we scroll to the + correct height more easily as it becomes visible again */ + visibility: hidden; +} + +/* The two modals have different top paddings */ +#left-sidebar.zoom-in-conversations #left-sidebar-modal { + .left-sidebar-modal-close-area { + padding-top: var(--left-sidebar-modal-close-area-padding-top); + } + + #left-sidebar-modal-content { + height: calc( + 100dvh - var(--navbar-fixed-height) - + var(--left-sidebar-modal-close-area-padding-top) - + var(--left-sidebar-modal-close-area-height) + ); + box-shadow: + 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), + 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow), + 0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow); + } + + .left-sidebar-modal-close-area:focus-visible + #left-sidebar-modal-content, + .left-sidebar-modal-close-area:hover + #left-sidebar-modal-content { + box-shadow: + 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), + 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover), + 0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover); + } +} + +/* The topic menu leaves space to see the DM section by padding the sidebar + row height to the close section's upper padding. */ +#left-sidebar.zoom-in-topics #left-sidebar-modal { + .left-sidebar-modal-close-area { + padding-top: calc( + var(--left-sidebar-modal-close-area-padding-top) + + var(--line-height-sidebar-row-prominent) + ); + } + + #left-sidebar-modal-content { + height: calc( + 100dvh - var(--navbar-fixed-height) - + var(--left-sidebar-modal-close-area-padding-top) - + var(--left-sidebar-modal-close-area-height) - + var(--line-height-sidebar-row-prominent) + ); + box-shadow: + 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), + 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow), + 0 -5.5em 1em 0 var(--color-left-sidebar-outer-box-shadow); + } + + .left-sidebar-modal-close-area:focus-visible + #left-sidebar-modal-content, + .left-sidebar-modal-close-area:hover + #left-sidebar-modal-content { + box-shadow: + 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), + 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover), + 0 -5.5em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover); + } +} + +@container app (width <= $cq_ml_min) { + .left-sidebar-modal-close-area .zulip-icon-close { + margin-right: 5px; + } +} + +#left-sidebar.zoom-in-topics #left-sidebar-modal, +#left-sidebar.zoom-in-conversations #left-sidebar-modal { position: absolute; + visibility: visible; left: 0; top: var(--navbar-fixed-height); width: var(--left-sidebar-width); - color: var(--color-left-sidebar-navigation-icon); .left-sidebar-modal-close-area { position: relative; z-index: 1; height: var(--left-sidebar-modal-close-area-height); - padding-top: var(--left-sidebar-modal-close-area-padding-top); display: flex; margin: 0 10px; justify-content: space-between; @@ -2456,44 +2456,70 @@ li.topic-list-item { } } - #direct-messages-modal-section-header { - grid-area: section-header; + #direct-messages-modal-section-header, + .channel-header { margin-top: 5px; + grid-area: section-header; } - .modal-direct-messages-list { + .modal-direct-messages-list, + .topic-list-scroll-container { grid-area: conversation-list; } + .channel-header { + /* Override the usual hover behavior for the first row (topic name and search bar) + and its popover. */ + box-shadow: none; + background-color: var(--color-background); + /* For the scrollbar */ + margin-right: var(--left-sidebar-right-margin); + } + #left-sidebar-modal-content { padding: 0 0 5px 5px; background: var(--color-background); border-radius: 6px 6px 0 0; - height: calc( - 100dvh - var(--navbar-fixed-height) - - var(--left-sidebar-modal-close-area-padding-top) - - var(--left-sidebar-modal-close-area-height) - ); - box-shadow: - 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), - 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow), - 0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow); } .left-sidebar-modal-close-area:focus-visible { outline: none; } - .left-sidebar-modal-close-area:focus-visible + #left-sidebar-modal-content, - .left-sidebar-modal-close-area:hover + #left-sidebar-modal-content { - box-shadow: - 0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow), - 0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover), - 0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover); + /* Remove extra left spacing */ + .subscription_block { + grid-template-columns: + 0.5em var(--left-sidebar-icon-column-width) var( + --left-sidebar-icon-content-gap + ) + minmax(0, 1fr) minmax(0, max-content) minmax(0, max-content) var( + --left-sidebar-vdots-width + ) + 0; } - .left-sidebar-filter-input-container { - display: block; - margin-bottom: 5px; + .topic-list { + margin-bottom: var(--left-sidebar-bottom-scrolling-buffer); + } + + .topic-list-item { + padding-right: 0; + margin-right: var(--left-sidebar-right-margin); + } + + /* Override default margin when in the modal. */ + .topic-list, + .topic-list-item { + margin-left: 0; + } + + /* Always show the new topic button and header menu icon for the modal view. */ + .channel-new-topic-button { + display: flex; + } + + .channel-header .sidebar-menu-icon { + display: flex; + color: var(--color-vdots-visible); } } diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 866ea091f5..0530268576 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -1843,7 +1843,9 @@ body:not(.spectator-view) { } .app-main .column-right.expanded .right-sidebar, - .app-main .column-left.expanded .left-sidebar { + .app-main .column-left.expanded .left-sidebar, + .app-main #left-sidebar.zoom-in-topics #left-sidebar-modal, + .app-main #left-sidebar.zoom-in-conversations #left-sidebar-modal { width: 100vw; max-width: 100%; } diff --git a/web/templates/left_sidebar.hbs b/web/templates/left_sidebar.hbs index a0c8279603..e87dcb5c61 100644 --- a/web/templates/left_sidebar.hbs +++ b/web/templates/left_sidebar.hbs @@ -1,5 +1,5 @@ -