diff --git a/web/e2e-tests/navigation.test.ts b/web/e2e-tests/navigation.test.ts index 6252de2583..75a4d9eb7d 100644 --- a/web/e2e-tests/navigation.test.ts +++ b/web/e2e-tests/navigation.test.ts @@ -95,12 +95,12 @@ async function navigation_tests(page: Page): Promise { await navigate_using_left_sidebar(page, "Verona"); - await page.click("#left-sidebar-navigation-list .home-link"); + await page.click("#left-sidebar-navigation-list .top_left_all_messages"); await page.waitForSelector("#message_view_header .zulip-icon-all-messages", {visible: true}); await navigate_to_subscriptions(page); - await page.click("#left-sidebar-navigation-list .home-link"); + await page.click("#left-sidebar-navigation-list .top_left_all_messages"); await page.waitForSelector("#message_view_header .zulip-icon-all-messages", {visible: true}); await navigate_to_settings(page); diff --git a/web/e2e-tests/stars.test.ts b/web/e2e-tests/stars.test.ts index 1e496426ff..b7fc9c594d 100644 --- a/web/e2e-tests/stars.test.ts +++ b/web/e2e-tests/stars.test.ts @@ -61,7 +61,7 @@ async function stars_test(page: Page): Promise { await toggle_test_star_message(page); await page.click("#left-sidebar-navigation-list .top_left_all_messages"); - message_list_id = await common.get_current_msg_list_id(page, false); + message_list_id = await common.get_current_msg_list_id(page, true); await page.waitForSelector( `.message-list[data-message-list-id='${message_list_id}'] .zulip-icon-star-filled`, {visible: true}, diff --git a/web/src/left_sidebar_navigation_area.ts b/web/src/left_sidebar_navigation_area.ts index 15a1618e3b..85e209284d 100644 --- a/web/src/left_sidebar_navigation_area.ts +++ b/web/src/left_sidebar_navigation_area.ts @@ -3,9 +3,11 @@ import _ from "lodash"; import assert from "minimalistic-assert"; import * as channel_folders from "./channel_folders.ts"; +import * as drafts from "./drafts.ts"; import type {Filter} from "./filter.ts"; import {localstorage} from "./localstorage.ts"; import * as message_reminder from "./message_reminder.ts"; +import * as navigation_views from "./navigation_views.ts"; import {page_params} from "./page_params.ts"; import * as people from "./people.ts"; import * as resize from "./resize.ts"; @@ -371,6 +373,55 @@ export function handle_home_view_changed(new_home_view: string): void { update_dom_with_unread_counts(res, true); } +export function get_built_in_primary_condensed_views(): navigation_views.BuiltInViewMetadata[] { + function score(view: navigation_views.BuiltInViewMetadata): number { + if (view.prioritize_in_condensed_view) { + return 1; + } + return 0; + } + // Get the top 5 prioritized views. + return navigation_views + .get_built_in_views() + .sort((view1, view2) => score(view2) - score(view1)) + .slice(0, 5); + // TODO: Think about filtering out scheduled message and reminders views with UI to support less than 5 views. +} + +export function get_built_in_popover_condensed_views(): navigation_views.BuiltInViewMetadata[] { + const visible_condensed_views = get_built_in_primary_condensed_views(); + const all_views = navigation_views.get_built_in_views(); + return all_views.filter((view) => { + if (view.fragment === "scheduled") { + const scheduled_message_count = scheduled_messages.get_count(); + if (scheduled_message_count === 0) { + return false; + } + view.unread_count = scheduled_message_count; + return true; + } + if (view.fragment === "reminders") { + const reminders_count = message_reminder.get_count(); + if (reminders_count === 0) { + return false; + } + view.unread_count = reminders_count; + return true; + } + if (view.fragment === "drafts") { + view.unread_count = drafts.draft_model.getDraftCount(); + } + // Remove views that are already visible. + return !visible_condensed_views.some( + (visible_view) => visible_view.fragment === view.fragment, + ); + }); +} + +export function get_built_in_views(): navigation_views.BuiltInViewMetadata[] { + return navigation_views.get_built_in_views(); +} + export function initialize(): void { update_reminders_row(); update_scheduled_messages_row(); diff --git a/web/src/left_sidebar_navigation_area_popovers.ts b/web/src/left_sidebar_navigation_area_popovers.ts index 6915c391b1..fcf18cb489 100644 --- a/web/src/left_sidebar_navigation_area_popovers.ts +++ b/web/src/left_sidebar_navigation_area_popovers.ts @@ -3,17 +3,17 @@ import assert from "minimalistic-assert"; import type * as tippy from "tippy.js"; import render_left_sidebar_all_messages_popover from "../templates/popovers/left_sidebar/left_sidebar_all_messages_popover.hbs"; -import render_left_sidebar_condensed_views_popover from "../templates/popovers/left_sidebar/left_sidebar_condensed_views_popover.hbs"; import render_left_sidebar_drafts_popover from "../templates/popovers/left_sidebar/left_sidebar_drafts_popover.hbs"; import render_left_sidebar_inbox_popover from "../templates/popovers/left_sidebar/left_sidebar_inbox_popover.hbs"; import render_left_sidebar_recent_view_popover from "../templates/popovers/left_sidebar/left_sidebar_recent_view_popover.hbs"; import render_left_sidebar_starred_messages_popover from "../templates/popovers/left_sidebar/left_sidebar_starred_messages_popover.hbs"; +import render_left_sidebar_views_popover from "../templates/popovers/left_sidebar/left_sidebar_views_popover.hbs"; import * as channel from "./channel.ts"; import * as drafts from "./drafts.ts"; +import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area.ts"; import * as popover_menus from "./popover_menus.ts"; import * as popovers from "./popovers.ts"; -import * as scheduled_messages from "./scheduled_messages.ts"; import * as settings_config from "./settings_config.ts"; import * as starred_messages from "./starred_messages.ts"; import * as starred_messages_ui from "./starred_messages_ui.ts"; @@ -286,30 +286,18 @@ export function initialize(): void { popover_menus.register_popover_menu(".left-sidebar-navigation-menu-icon", { ...popover_menus.left_sidebar_tippy_options, onShow(instance) { - // Determine at show time whether there are scheduled messages, - // so that Tippy properly calculates the height of the popover - const scheduled_message_count = scheduled_messages.get_count(); - let has_scheduled_messages = false; - if (scheduled_message_count > 0) { - has_scheduled_messages = true; - } + const built_in_popover_condensed_views = + left_sidebar_navigation_area.get_built_in_popover_condensed_views(); + popovers.hide_all(); instance.setContent( ui_util.parse_html( - render_left_sidebar_condensed_views_popover({has_scheduled_messages}), + render_left_sidebar_views_popover({ + views: built_in_popover_condensed_views, + }), ), ); }, - onMount() { - ui_util.update_unread_count_in_dom( - $(".condensed-views-popover-menu-drafts"), - drafts.draft_model.getDraftCount(), - ); - ui_util.update_unread_count_in_dom( - $(".condensed-views-popover-menu-scheduled-messages"), - scheduled_messages.get_count(), - ); - }, onHidden(instance) { instance.destroy(); popover_menus.popover_instances.top_left_sidebar = null; diff --git a/web/src/navigation_views.ts b/web/src/navigation_views.ts new file mode 100644 index 0000000000..e9259f78df --- /dev/null +++ b/web/src/navigation_views.ts @@ -0,0 +1,236 @@ +import * as blueslip from "./blueslip.ts"; +import {$t} from "./i18n.ts"; +import type {StateData} from "./state_data.ts"; +import {user_settings} from "./user_settings.ts"; + +export type BuiltInViewBasicMetadata = { + fragment: string; + name: string; + is_pinned: boolean; + icon: string; + css_class_suffix: string; + tooltip_template_id: string; + has_unread_count: boolean; + unread_count_type: "normal-count" | "quiet-count" | ""; + supports_masked_unread: boolean; + hidden_for_spectators: boolean; + menu_icon_class: string; + menu_aria_label: string; + home_view_code: string; + prioritize_in_condensed_view: boolean; +}; + +export const built_in_views_meta_data: Record = { + inbox: { + fragment: "inbox", + name: $t({defaultMessage: "Inbox"}), + is_pinned: true, + icon: "zulip-icon-inbox", + css_class_suffix: "inbox", + tooltip_template_id: "inbox-tooltip-template", + has_unread_count: true, + unread_count_type: "normal-count", + supports_masked_unread: true, + hidden_for_spectators: true, + menu_icon_class: "inbox-sidebar-menu-icon", + menu_aria_label: $t({defaultMessage: "Inbox options"}), + home_view_code: "inbox", + prioritize_in_condensed_view: true, + }, + recent_view: { + fragment: "recent", + name: $t({defaultMessage: "Recent conversations"}), + is_pinned: true, + icon: "zulip-icon-recent", + css_class_suffix: "recent_view", + tooltip_template_id: "recent-conversations-tooltip-template", + has_unread_count: true, + unread_count_type: "normal-count", + supports_masked_unread: true, + hidden_for_spectators: false, + menu_icon_class: "recent-view-sidebar-menu-icon", + menu_aria_label: $t({defaultMessage: "Recent conversations options"}), + home_view_code: "recent_topics", + prioritize_in_condensed_view: true, + }, + all_messages: { + fragment: "feed", + name: $t({defaultMessage: "Combined feed"}), + is_pinned: true, + icon: "zulip-icon-all-messages", + css_class_suffix: "all_messages", + tooltip_template_id: "all-message-tooltip-template", + has_unread_count: true, + unread_count_type: "normal-count", + supports_masked_unread: true, + hidden_for_spectators: false, + menu_icon_class: "all-messages-sidebar-menu-icon", + menu_aria_label: $t({defaultMessage: "Combined feed options"}), + home_view_code: "all_messages", + prioritize_in_condensed_view: true, + }, + mentions: { + fragment: "narrow/is/mentioned", + name: $t({defaultMessage: "Mentions"}), + is_pinned: true, + icon: "zulip-icon-at-sign", + css_class_suffix: "mentions", + tooltip_template_id: "mentions-tooltip-template", + has_unread_count: true, + unread_count_type: "normal-count", + supports_masked_unread: false, + hidden_for_spectators: true, + menu_icon_class: "", + menu_aria_label: "", + home_view_code: "", + prioritize_in_condensed_view: true, + }, + my_reactions: { + fragment: "narrow/has/reaction/sender/me", + name: $t({defaultMessage: "Reactions"}), + is_pinned: true, + icon: "zulip-icon-smile", + css_class_suffix: "my_reactions", + tooltip_template_id: "my-reactions-tooltip-template", + has_unread_count: false, + unread_count_type: "", + supports_masked_unread: false, + hidden_for_spectators: true, + menu_icon_class: "", + menu_aria_label: "", + home_view_code: "", + prioritize_in_condensed_view: false, + }, + starred_messages: { + fragment: "narrow/is/starred", + name: $t({defaultMessage: "Starred messages"}), + is_pinned: true, + icon: "zulip-icon-star", + css_class_suffix: "starred_messages", + tooltip_template_id: "starred-message-tooltip-template", + has_unread_count: true, + unread_count_type: "quiet-count", + supports_masked_unread: true, + hidden_for_spectators: true, + menu_icon_class: "starred-messages-sidebar-menu-icon", + menu_aria_label: $t({defaultMessage: "Starred messages options"}), + home_view_code: "", + prioritize_in_condensed_view: true, + }, + drafts: { + fragment: "drafts", + name: $t({defaultMessage: "Drafts"}), + is_pinned: true, + icon: "zulip-icon-drafts", + css_class_suffix: "drafts", + tooltip_template_id: "drafts-tooltip-template", + has_unread_count: true, + unread_count_type: "quiet-count", + supports_masked_unread: false, + hidden_for_spectators: true, + menu_icon_class: "drafts-sidebar-menu-icon", + menu_aria_label: $t({defaultMessage: "Drafts options"}), + home_view_code: "", + prioritize_in_condensed_view: false, + }, + scheduled_messages: { + fragment: "scheduled", + name: $t({defaultMessage: "Scheduled messages"}), + is_pinned: true, + icon: "zulip-icon-calendar-days", + css_class_suffix: "scheduled_messages", + tooltip_template_id: "scheduled-tooltip-template", + has_unread_count: true, + unread_count_type: "quiet-count", + supports_masked_unread: false, + hidden_for_spectators: true, + menu_icon_class: "", + menu_aria_label: "", + home_view_code: "", + prioritize_in_condensed_view: false, + }, + reminders: { + fragment: "reminders", + name: $t({defaultMessage: "Reminders"}), + is_pinned: true, + icon: "zulip-icon-alarm-clock", + css_class_suffix: "reminders", + tooltip_template_id: "reminders-tooltip-template", + has_unread_count: true, + unread_count_type: "quiet-count", + supports_masked_unread: false, + hidden_for_spectators: true, + menu_icon_class: "", + menu_aria_label: "", + home_view_code: "", + prioritize_in_condensed_view: false, + }, +}; + +export type NavigationView = { + fragment: string; + is_pinned: boolean; + name: string | null; +}; + +let navigation_views_dict: Map; + +export function add_navigation_view(navigation_view: NavigationView): void { + navigation_views_dict.set(navigation_view.fragment, navigation_view); +} + +export function update_navigation_view(fragment: string, data: Partial): void { + const view = get_navigation_view_by_fragment(fragment); + if (view) { + navigation_views_dict.set(fragment, { + ...view, + ...data, + }); + } else { + blueslip.error("Cannot find navigation view to update"); + } +} + +export function remove_navigation_view(fragment: string): void { + navigation_views_dict.delete(fragment); +} + +export function get_navigation_view_by_fragment(fragment: string): NavigationView | undefined { + return navigation_views_dict.get(fragment); +} + +export type BuiltInViewMetadata = BuiltInViewBasicMetadata & { + is_home_view: boolean; + unread_count?: number; +}; + +export function get_built_in_views(): BuiltInViewMetadata[] { + return Object.values(built_in_views_meta_data).map((view) => { + const view_current_data = get_navigation_view_by_fragment(view.fragment); + return { + ...view, + is_pinned: view_current_data?.is_pinned ?? view.is_pinned, + is_home_view: view.home_view_code === user_settings.web_home_view, + }; + }); +} + +export function get_all_navigation_views(): NavigationView[] { + const built_in_views = get_built_in_views().map((view) => ({ + fragment: view.fragment, + is_pinned: view.is_pinned, + name: view.name, + })); + const built_in_fragments = new Set(built_in_views.map((view) => view.fragment)); + const custom_views = [...navigation_views_dict.values()].filter( + (view) => !built_in_fragments.has(view.fragment), + ); + + return [...built_in_views, ...custom_views]; +} + +export const initialize = (params: StateData["navigation_views"]): void => { + navigation_views_dict = new Map( + params.navigation_views.map((view) => [view.fragment, view]), + ); +}; diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 38ed4fd77f..737e4cfd9d 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -36,6 +36,7 @@ import * as message_view from "./message_view.ts"; import * as muted_users_ui from "./muted_users_ui.ts"; import * as narrow_title from "./narrow_title.ts"; import * as navbar_alerts from "./navbar_alerts.ts"; +import * as navigation_views from "./navigation_views.ts"; import * as onboarding_steps from "./onboarding_steps.ts"; import * as overlays from "./overlays.ts"; import * as peer_data from "./peer_data.ts"; @@ -203,6 +204,20 @@ export function dispatch_normal_event(event) { muted_users_ui.handle_user_updates(event.muted_users); break; + case "navigation_view": + switch (event.op) { + case "add": + navigation_views.add_navigation_view(event.navigation_view); + break; + case "update": + navigation_views.update_navigation_view(event.fragment, event.data); + break; + case "remove": + navigation_views.remove_navigation_view(event.fragment); + break; + } + break; + case "presence": activity_ui.update_presence_info(event.user_id, event.presence, event.server_timestamp); break; diff --git a/web/src/sidebar_ui.ts b/web/src/sidebar_ui.ts index d83d8cdfe2..fdf20c042a 100644 --- a/web/src/sidebar_ui.ts +++ b/web/src/sidebar_ui.ts @@ -288,6 +288,10 @@ export function initialize(): void { } export function initialize_left_sidebar(): void { + const primary_condensed_views = + left_sidebar_navigation_area.get_built_in_primary_condensed_views(); + const expanded_views = left_sidebar_navigation_area.get_built_in_views(); + const rendered_sidebar = render_left_sidebar({ is_guest: current_user.is_guest, development_environment: page_params.development_environment, @@ -298,6 +302,8 @@ export function initialize_left_sidebar(): void { is_recent_view_home_view: user_settings.web_home_view === settings_config.web_home_view_values.recent_topics.code, is_spectator: page_params.is_spectator, + primary_condensed_views, + expanded_views, }); $("#left-sidebar-container").html(rendered_sidebar); diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 783c6d1c34..f6dae0aff1 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -160,6 +160,12 @@ export const channel_folder_schema = z.object({ is_archived: z.boolean(), }); +export const navigation_view_schema = z.object({ + fragment: z.string(), + name: z.string(), + is_pinned: z.boolean(), +}); + export const user_topic_schema = z.object({ stream_id: z.number(), topic_name: z.string(), @@ -537,6 +543,7 @@ export const split_state_data_schema = z.object({ }), current_user: current_user_schema, realm: realm_schema, + navigation_views: z.object({navigation_views: z.array(navigation_view_schema)}), }); type SplitStateDataInput = z.input; diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 031f388947..a278cca726 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -86,6 +86,7 @@ import * as narrow_title from "./narrow_title.ts"; import * as navbar_alerts from "./navbar_alerts.ts"; import * as navbar_help_menu from "./navbar_help_menu.ts"; import * as navigate from "./navigate.ts"; +import * as navigation_views from "./navigation_views.ts"; import * as onboarding_steps from "./onboarding_steps.ts"; import * as overlays from "./overlays.ts"; import {page_params} from "./page_params.ts"; @@ -455,6 +456,7 @@ export async function initialize_everything(state_data) { // This populates data for scheduled messages. scheduled_messages.initialize(state_data.scheduled_messages); message_reminder.initialize(state_data.reminders); + navigation_views.initialize(state_data.navigation_views); scheduled_messages_ui.initialize(); reminders_overlay_ui.initialize(); popover_menus.initialize(); diff --git a/web/templates/left_sidebar.hbs b/web/templates/left_sidebar.hbs index f9d26bc49d..e1cd9757c9 100644 --- a/web/templates/left_sidebar.hbs +++ b/web/templates/left_sidebar.hbs @@ -1,167 +1,22 @@