left_sidebar: Refactor to support user's navigation view preference.

Co-authored-by: Aditya Chaudhary <aditya.chaudhary1558@gmail.com>
This commit is contained in:
Aman Agrawal
2025-07-31 17:02:10 +05:30
committed by Tim Abbott
parent 87aaf6dd4d
commit 8207eaab55
18 changed files with 572 additions and 205 deletions

View File

@@ -95,12 +95,12 @@ async function navigation_tests(page: Page): Promise<void> {
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);

View File

@@ -61,7 +61,7 @@ async function stars_test(page: Page): Promise<void> {
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},

View File

@@ -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();

View File

@@ -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;

236
web/src/navigation_views.ts Normal file
View File

@@ -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<string, BuiltInViewBasicMetadata> = {
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<string, NavigationView>;
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<NavigationView>): 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<string, NavigationView>(
params.navigation_views.map((view) => [view.fragment, view]),
);
};

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<typeof split_state_data_schema>;

View File

@@ -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();

View File

@@ -1,167 +1,22 @@
<div class="left-sidebar" id="left-sidebar" role="navigation">
<div id="left-sidebar-navigation-area" class="left-sidebar-navigation-area">
<div id="views-label-container" class="showing-expanded-navigation {{#if is_spectator}}remove-pointer-for-spectator{{/if}}">
<div id="views-label-container" class="showing-expanded-navigation{{#if is_spectator}} remove-pointer-for-spectator{{/if}}">
<i id="toggle-top-left-navigation-area-icon" class="zulip-icon zulip-icon-heading-triangle-right sidebar-heading-icon rotate-icon-down views-tooltip-target hidden-for-spectators" aria-hidden="true" tabindex="0" role="button"></i>
{{~!-- squash whitespace --~}}
<h4 class="left-sidebar-title"><span class="views-tooltip-target">{{t 'VIEWS' }}</span></h4>
<ul id="left-sidebar-navigation-list-condensed" class="filters">
<li class="top_left_inbox left-sidebar-navigation-condensed-item {{#if is_inbox_home_view}}selected-home-view{{/if}}">
<a href="#inbox" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="inbox-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-inbox" aria-hidden="true"></i>
</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_recent_view left-sidebar-navigation-condensed-item {{#if is_recent_view_home_view}}selected-home-view{{/if}}">
<a href="#recent" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="recent-conversations-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-recent" aria-hidden="true"></i>
</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_all_messages left-sidebar-navigation-condensed-item {{#if is_all_messages_home_view}}selected-home-view{{/if}}">
<a href="#feed" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="all-message-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-all-messages" aria-hidden="true"></i>
</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_mentions left-sidebar-navigation-condensed-item">
<a href="#narrow/is/mentioned" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="mentions-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-at-sign" aria-hidden="true"></i>
</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_starred_messages left-sidebar-navigation-condensed-item">
<a href="#narrow/is/starred" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="starred-message-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-star" aria-hidden="true"></i>
</span>
<span class="unread_count quiet-count"></span>
</a>
</li>
{{#each primary_condensed_views}}
{{> left_sidebar_primary_condensed_view_item . }}
{{/each}}
</ul>
<div class="left-sidebar-navigation-menu-icon">
<i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Other views'}}"></i>
</div>
</div>
<ul id="left-sidebar-navigation-list" class="left-sidebar-navigation-list filters">
<li class="top_left_inbox top_left_row hidden-for-spectators {{#if is_inbox_home_view}}selected-home-view{{/if}}">
<a href="#inbox" class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" data-tooltip-template-id="inbox-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-inbox" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Inbox' }}</span>
<span class="unread_count normal-count"></span>
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
</a>
<span class="arrow sidebar-menu-icon inbox-sidebar-menu-icon hidden-for-spectators"><i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Inbox options'}}"></i></span>
</li>
<li class="top_left_recent_view top_left_row {{#if is_recent_view_home_view}}selected-home-view{{/if}}">
<a href="#recent" class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" data-tooltip-template-id="recent-conversations-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-recent" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Recent conversations' }}</span>
<span class="unread_count normal-count"></span>
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
</a>
<span class="arrow sidebar-menu-icon recent-view-sidebar-menu-icon hidden-for-spectators">
<i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Recent conversations options'}}"></i>
</span>
</li>
<li class="top_left_all_messages top_left_row {{#if is_all_messages_home_view}}selected-home-view{{/if}}">
<a href="#feed" class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" data-tooltip-template-id="all-message-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-all-messages" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Combined feed' }}</span>
<span class="unread_count normal-count"></span>
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
</a>
<span class="arrow sidebar-menu-icon all-messages-sidebar-menu-icon hidden-for-spectators">
<i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Combined feed options'}}"></i>
</span>
</li>
<li class="top_left_mentions top_left_row hidden-for-spectators">
<a class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" href="#narrow/is/mentioned" data-tooltip-template-id="mentions-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-at-sign" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Mentions' }}</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_my_reactions top_left_row hidden-for-spectators">
<a class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" href="#narrow/has/reaction/sender/me" data-tooltip-template-id="my-reactions-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-smile" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Reactions' }}</span>
<span class="unread_count"></span>
</a>
</li>
<li class="top_left_starred_messages top_left_row hidden-for-spectators">
<a class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" href="#narrow/is/starred" data-tooltip-template-id="starred-message-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-star" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Starred messages' }}</span>
<span class="unread_count quiet-count"></span>
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
</a>
<span class="arrow sidebar-menu-icon starred-messages-sidebar-menu-icon"><i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Starred messages options'}}"></i></span>
</li>
<li class="top_left_drafts top_left_row hidden-for-spectators">
<a href="#drafts" class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" data-tooltip-template-id="drafts-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-drafts" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Drafts' }}</span>
<span class="unread_count quiet-count"></span>
</a>
<span class="arrow sidebar-menu-icon drafts-sidebar-menu-icon"><i class="zulip-icon zulip-icon-more-vertical" aria-label="{{t 'Drafts options'}}"></i></span>
</li>
<li class="top_left_scheduled_messages top_left_row hidden-for-spectators">
<a class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" href="#scheduled" data-tooltip-template-id="scheduled-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-calendar-days" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Scheduled messages' }}</span>
<span class="unread_count quiet-count"></span>
</a>
</li>
<li class="top_left_reminders top_left_row hidden-for-spectators">
<a class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" href="#reminders" data-tooltip-template-id="reminders-tooltip-template">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-alarm-clock" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{t 'Reminders' }}</span>
<span class="unread_count quiet-count"></span>
</a>
</li>
{{#each expanded_views}}
{{> left_sidebar_expanded_view_item . }}
{{/each}}
</ul>
</div>

View File

@@ -0,0 +1,18 @@
<li class="top_left_{{css_class_suffix}} top_left_row {{#if hidden_for_spectators}}hidden-for-spectators{{/if}} {{#if is_home_view}}selected-home-view{{/if}}">
<a href="#{{fragment}}" class="left-sidebar-navigation-label-container tippy-left-sidebar-tooltip" data-tooltip-template-id="{{tooltip_template_id}}">
<span class="filter-icon">
<i class="zulip-icon {{icon}}" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span class="left-sidebar-navigation-label">{{name}}</span>
<span class="unread_count {{unread_count_type}}"></span>
{{#if supports_masked_unread}}
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
{{/if}}
</a>
{{#if menu_icon_class}}
<span class="arrow sidebar-menu-icon {{menu_icon_class}} hidden-for-spectators"><i class="zulip-icon zulip-icon-more-vertical" aria-label="{{menu_aria_label}}"></i></span>
{{/if}}
</li>

View File

@@ -0,0 +1,8 @@
<li class="top_left_{{css_class_suffix}} left-sidebar-navigation-condensed-item{{#if is_home_view}} selected-home-view{{/if}}">
<a href="#{{fragment}}" class="tippy-left-sidebar-tooltip left-sidebar-navigation-icon-container" data-tooltip-template-id="{{tooltip_template_id}}">
<span class="filter-icon">
<i class="zulip-icon {{icon}}" aria-hidden="true"></i>
</span>
<span class="unread_count {{unread_count_type}}"></span>
</a>
</li>

View File

@@ -1,30 +0,0 @@
<div class="popover-menu" data-simplebar data-simplebar-tab-index="-1">
<ul role="menu" class="popover-menu-list condensed-views-popover-menu">
<li role="none" class="link-item popover-menu-list-item condensed-views-popover-menu-reactions">
<a href="#narrow/has/reaction/sender/me" role="menuitem" class="popover-menu-link tippy-left-sidebar-tooltip" data-tooltip-template-id="my-reactions-tooltip-template" tabindex="0">
<i class="popover-menu-icon zulip-icon zulip-icon-smile" aria-hidden="true"></i>
<span class="popover-menu-label">{{t 'Reactions' }}</span>
</a>
</li>
<li role="none" class="link-item popover-menu-list-item condensed-views-popover-menu-drafts">
<a href="#drafts" role="menuitem" class="popover-menu-link tippy-left-sidebar-tooltip" data-tooltip-template-id="drafts-tooltip-template" tabindex="0">
<i class="popover-menu-icon zulip-icon zulip-icon-drafts" aria-hidden="true"></i>
<span class="label-and-unread-wrapper">
<span class="popover-menu-label">{{t 'Drafts' }}</span>
<span class="unread_count quiet-count"></span>
</span>
</a>
</li>
{{#if has_scheduled_messages }}
<li role="none" class="link-item popover-menu-list-item condensed-views-popover-menu-scheduled-messages">
<a href="#scheduled" role="menuitem" class="popover-menu-link tippy-left-sidebar-tooltip" tabindex="0" data-tooltip-template-id="scheduled-tooltip-template">
<i class="popover-menu-icon zulip-icon zulip-icon-calendar-days" aria-hidden="true"></i>
<span class="label-and-unread-wrapper">
<span class="popover-menu-label">{{t 'Scheduled messages' }}</span>
<span class="unread_count quiet-count"></span>
</span>
</a>
</li>
{{/if}}
</ul>
</div>

View File

@@ -0,0 +1,18 @@
<li role="none" class="link-item popover-menu-list-item views-popover-menu-{{css_class_suffix}}">
<a href="#{{fragment}}" role="menuitem" class="popover-menu-link tippy-left-sidebar-tooltip" data-tooltip-template-id="{{tooltip_template_id}}" tabindex="0">
<i class="popover-menu-icon zulip-icon {{icon}}" aria-hidden="true"></i>
{{#if has_unread_count}}
<span class="label-and-unread-wrapper">
<span class="popover-menu-label">{{name}}</span>
<span class="unread_count {{unread_count_type}}">{{#if unread_count}}{{unread_count}}{{/if}}</span>
{{#if supports_masked_unread}}
<span class="masked_unread_count">
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
{{/if}}
</span>
{{else}}
<span class="popover-menu-label">{{name}}</span>
{{/if}}
</a>
</li>

View File

@@ -0,0 +1,7 @@
<div class="popover-menu" data-simplebar data-simplebar-tab-index="-1">
<ul role="menu" class="popover-menu-list condensed-views-popover-menu">
{{#each views}}
{{> left_sidebar_view_popover_item .}}
{{/each}}
</ul>
</div>

View File

@@ -51,6 +51,7 @@ const realm_playground = mock_esm("../src/realm_playground");
const reload = mock_esm("../src/reload");
const message_reminder = mock_esm("../src/message_reminder");
const reminders_overlay_ui = mock_esm("../src/reminders_overlay_ui");
const navigation_views = mock_esm("../src/navigation_views");
const saved_snippets = mock_esm("../src/saved_snippets");
const saved_snippets_ui = mock_esm("../src/saved_snippets_ui");
const scheduled_messages = mock_esm("../src/scheduled_messages");
@@ -195,6 +196,37 @@ run_test("alert_words", ({override}) => {
assert.ok(alert_words.has_alert_word("lunch"));
});
run_test("navigation_views", ({override}) => {
const add_event = event_fixtures.navigation_view__add;
{
const stub = make_stub();
override(navigation_views, "add_navigation_view", stub.f);
dispatch(add_event);
assert.equal(stub.num_calls, 1);
assert_same(stub.get_args("event").event, add_event.navigation_view);
}
const update_event = event_fixtures.navigation_view__update;
{
const stub = make_stub();
override(navigation_views, "update_navigation_view", stub.f);
dispatch(update_event);
assert.equal(stub.num_calls, 1);
assert_same(stub.get_args("event").event, update_event.fragment);
}
const remove_event = event_fixtures.navigation_view__remove;
{
const stub = make_stub();
override(navigation_views, "remove_navigation_view", stub.f);
dispatch(remove_event);
assert.equal(stub.num_calls, 1);
assert_same(stub.get_args("event").event, remove_event.fragment);
}
});
run_test("saved_snippets", ({override}) => {
const add_event = event_fixtures.saved_snippets__add;
override(saved_snippets_ui, "rerender_dropdown_widget", noop);

View File

@@ -283,6 +283,29 @@ exports.fixtures = {
],
},
navigation_view__add: {
type: "navigation_view",
op: "add",
navigation_view: {
fragment: "narrow/is/alerted",
is_pinned: true,
name: "Watched phrases",
},
},
navigation_view__remove: {
type: "navigation_view",
op: "remove",
fragment: "narrow/is/alerted",
},
navigation_view__update: {
type: "navigation_view",
op: "update",
fragment: "narrow/is/alerted",
data: {is_pinned: false},
},
onboarding_steps: {
type: "onboarding_steps",
onboarding_steps: [

View File

@@ -0,0 +1,131 @@
"use strict";
const assert = require("node:assert/strict");
const {set_global, zrequire} = require("./lib/namespace.cjs");
const {run_test} = require("./lib/test.cjs");
set_global("page_params", {
is_spectator: false,
});
const params = {
navigation_views: [
{
fragment: "narrow/is/starred",
is_pinned: true,
name: null,
},
{
fragment: "narrow/is/mentioned",
is_pinned: false,
name: null,
},
{
fragment: "custom/view/1",
is_pinned: true,
name: "Custom View 1",
},
],
};
const blueslip = zrequire("blueslip");
const people = zrequire("people");
const navigation_views = zrequire("navigation_views");
const {built_in_views_meta_data} = zrequire("navigation_views");
const {initialize_user_settings} = zrequire("user_settings");
people.add_active_user({
email: "tester@zulip.com",
full_name: "Tester von Tester",
user_id: 42,
});
people.initialize_current_user(42);
const user_settings = {
web_home_view: "inbox",
};
initialize_user_settings({user_settings});
navigation_views.initialize(params);
run_test("initialize", () => {
assert.ok(navigation_views.get_navigation_view_by_fragment("narrow/is/starred"));
assert.ok(navigation_views.get_navigation_view_by_fragment("narrow/is/mentioned"));
assert.ok(navigation_views.get_navigation_view_by_fragment("custom/view/1"));
});
run_test("add_navigation_view", () => {
const view = {
fragment: "inbox",
is_pinned: true,
name: null,
};
navigation_views.add_navigation_view(view);
assert.equal(navigation_views.get_navigation_view_by_fragment(view.fragment), view);
});
run_test("update_navigation_view", () => {
const view = {
fragment: "inbox",
is_pinned: true,
name: null,
};
navigation_views.add_navigation_view(view);
assert.equal(navigation_views.get_navigation_view_by_fragment(view.fragment), view);
navigation_views.update_navigation_view(view.fragment, {is_pinned: false});
assert.equal(navigation_views.get_navigation_view_by_fragment(view.fragment).is_pinned, false);
blueslip.expect("error", "Cannot find navigation view to update");
navigation_views.update_navigation_view("nonexistent", {name: "Nonexistent"});
});
run_test("remove_navigation_view", () => {
const view = {
fragment: "inbox",
is_pinned: true,
name: null,
};
navigation_views.add_navigation_view(view);
assert.equal(navigation_views.get_navigation_view_by_fragment(view.fragment), view);
navigation_views.remove_navigation_view(view.fragment);
assert.equal(navigation_views.get_navigation_view_by_fragment(view.fragment), undefined);
});
run_test("get_built_in_views", () => {
const built_in_views = navigation_views.get_built_in_views();
assert.ok(built_in_views.length > 0);
const starred_view = built_in_views.find((view) => view.fragment === "narrow/is/starred");
assert.ok(starred_view);
assert.equal(starred_view.is_pinned, true);
const mentions_view = built_in_views.find((view) => view.fragment === "narrow/is/mentioned");
assert.ok(mentions_view);
assert.equal(mentions_view.is_pinned, false);
const inbox_view = built_in_views.find((view) => view.fragment === "inbox");
assert.ok(inbox_view);
assert.equal(inbox_view.is_pinned, built_in_views_meta_data.inbox.is_pinned);
});
run_test("get_all_navigation_views", () => {
const all_views = navigation_views.get_all_navigation_views();
assert.ok(all_views.length > 0);
const starred_view = all_views.find((view) => view.fragment === "narrow/is/starred");
assert.ok(starred_view);
assert.equal(starred_view.is_pinned, true);
assert.equal(starred_view.name, built_in_views_meta_data.starred_messages.name);
const custom_view = all_views.find((view) => view.fragment === "custom/view/1");
assert.ok(custom_view);
assert.equal(custom_view.is_pinned, true);
assert.equal(custom_view.name, "Custom View 1");
const fragments = all_views.map((view) => view.fragment);
const unique_fragments = [...new Set(fragments)];
assert.equal(fragments.length, unique_fragments.length);
});