inbox: Redesign to show channel folders.

This commit is contained in:
Aman Agrawal
2025-07-16 12:13:17 +05:30
committed by Tim Abbott
parent ded8f93ca0
commit 47f42ed149
13 changed files with 547 additions and 153 deletions

View File

@@ -1,3 +1,4 @@
import assert from "minimalistic-assert";
import type {z} from "zod"; import type {z} from "zod";
import {FoldDict} from "./fold_dict.ts"; import {FoldDict} from "./fold_dict.ts";
@@ -39,3 +40,9 @@ export function get_channel_folders(include_archived = false): ChannelFolder[] {
export function is_valid_folder_id(folder_id: number): boolean { export function is_valid_folder_id(folder_id: number): boolean {
return channel_folder_by_id_dict.has(folder_id); return channel_folder_by_id_dict.has(folder_id);
} }
export function get_channel_folder_by_id(folder_id: number): ChannelFolder {
const channel_folder = channel_folder_by_id_dict.get(folder_id);
assert(channel_folder !== undefined);
return channel_folder;
}

View File

@@ -4,6 +4,8 @@ import assert from "minimalistic-assert";
import type * as tippy from "tippy.js"; import type * as tippy from "tippy.js";
import {z} from "zod"; import {z} from "zod";
import render_inbox_folder_row from "../templates/inbox_view/inbox_folder_row.hbs";
import render_inbox_folder_with_channels from "../templates/inbox_view/inbox_folder_with_channels.hbs";
import render_inbox_row from "../templates/inbox_view/inbox_row.hbs"; import render_inbox_row from "../templates/inbox_view/inbox_row.hbs";
import render_inbox_stream_container from "../templates/inbox_view/inbox_stream_container.hbs"; import render_inbox_stream_container from "../templates/inbox_view/inbox_stream_container.hbs";
import render_inbox_view from "../templates/inbox_view/inbox_view.hbs"; import render_inbox_view from "../templates/inbox_view/inbox_view.hbs";
@@ -11,13 +13,14 @@ import render_introduce_zulip_view_modal from "../templates/introduce_zulip_view
import render_user_with_status_icon from "../templates/user_with_status_icon.hbs"; import render_user_with_status_icon from "../templates/user_with_status_icon.hbs";
import * as buddy_data from "./buddy_data.ts"; import * as buddy_data from "./buddy_data.ts";
import * as channel_folders from "./channel_folders.ts";
import * as compose_closed_ui from "./compose_closed_ui.ts"; import * as compose_closed_ui from "./compose_closed_ui.ts";
import * as compose_state from "./compose_state.ts"; import * as compose_state from "./compose_state.ts";
import * as dialog_widget from "./dialog_widget.ts"; import * as dialog_widget from "./dialog_widget.ts";
import * as dropdown_widget from "./dropdown_widget.ts"; import * as dropdown_widget from "./dropdown_widget.ts";
import type {Filter} from "./filter"; import type {Filter} from "./filter";
import * as hash_util from "./hash_util.ts"; import * as hash_util from "./hash_util.ts";
import {$t_html} from "./i18n.ts"; import {$t, $t_html} from "./i18n.ts";
import * as inbox_util from "./inbox_util.ts"; import * as inbox_util from "./inbox_util.ts";
import * as keydown_util from "./keydown_util.ts"; import * as keydown_util from "./keydown_util.ts";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area.ts"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area.ts";
@@ -97,6 +100,7 @@ type StreamContext = {
mention_in_unread: boolean; mention_in_unread: boolean;
unread_count?: number; unread_count?: number;
column_indexes: typeof COLUMNS; column_indexes: typeof COLUMNS;
folder_id: number;
}; };
const stream_context_properties: (keyof StreamContext)[] = [ const stream_context_properties: (keyof StreamContext)[] = [
@@ -134,6 +138,7 @@ type TopicContext = {
all_visibility_policies: typeof user_topics.all_visibility_policies; all_visibility_policies: typeof user_topics.all_visibility_policies;
visibility_policy: number | false; visibility_policy: number | false;
column_indexes: typeof COLUMNS; column_indexes: typeof COLUMNS;
channel_folder_id?: number;
}; };
const topic_context_properties: (keyof TopicContext)[] = [ const topic_context_properties: (keyof TopicContext)[] = [
@@ -153,11 +158,38 @@ const topic_context_properties: (keyof TopicContext)[] = [
"all_visibility_policies", "all_visibility_policies",
"visibility_policy", "visibility_policy",
"column_indexes", "column_indexes",
"channel_folder_id",
];
type ChannelFolderContext = {
header_id: string;
is_header_visible: boolean;
name: string;
id: number;
unread_count: number | undefined;
is_collapsed: boolean;
has_unread_mention: boolean;
};
const channel_folder_context_properties: (keyof ChannelFolderContext)[] = [
"header_id",
"is_header_visible",
"name",
"id",
"unread_count",
"is_collapsed",
"has_unread_mention",
]; ];
let dms_dict = new Map<string, DirectMessageContext>(); let dms_dict = new Map<string, DirectMessageContext>();
let topics_dict = new Map<string, Map<string, TopicContext>>(); let topics_dict = new Map<string, Map<string, TopicContext>>();
let streams_dict = new Map<string, StreamContext>(); let streams_dict = new Map<string, StreamContext>();
const OTHER_CHANNELS_FOLDER_ID = -1;
const OTHER_CHANNEL_HEADER_ID = "inbox-channels-no-folder-header";
const CHANNEL_FOLDER_HEADER_ID_PREFIX = "inbox-channel-folder-header-";
const PINNED_CHANNEL_FOLDER_ID = -2;
const PINNED_CHANNEL_HEADER_ID = "inbox-channels-pinned-folder-header";
let channel_folders_dict = new Map<number, ChannelFolderContext>();
let update_triggered_by_user = false; let update_triggered_by_user = false;
let filters_dropdown_widget; let filters_dropdown_widget;
let channel_view_topic_widget: InboxTopicListWidget | undefined; let channel_view_topic_widget: InboxTopicListWidget | undefined;
@@ -388,13 +420,6 @@ function get_stream_container(stream_key: string): JQuery {
return $(`#${CSS.escape(stream_key)}`); return $(`#${CSS.escape(stream_key)}`);
} }
function get_topics_container(stream_id: number): JQuery {
const $topics_container = get_stream_header_row(stream_id)
.next(".inbox-topic-container")
.expectOne();
return $topics_container;
}
function get_stream_header_row(stream_id: number): JQuery { function get_stream_header_row(stream_id: number): JQuery {
const $stream_header_row = $(`#${CSS.escape(STREAM_HEADER_PREFIX + stream_id)}`); const $stream_header_row = $(`#${CSS.escape(STREAM_HEADER_PREFIX + stream_id)}`);
return $stream_header_row; return $stream_header_row;
@@ -527,6 +552,16 @@ function rerender_dm_inbox_row_if_needed(
} }
} }
function get_channel_folder_id(info: {folder_id: number | null; is_pinned: boolean}): number {
if (info.is_pinned) {
return PINNED_CHANNEL_FOLDER_ID;
}
if (info.folder_id === null) {
return OTHER_CHANNELS_FOLDER_ID;
}
return info.folder_id;
}
function format_stream(stream_id: number): StreamContext { function format_stream(stream_id: number): StreamContext {
// NOTE: Unread count is not included in this function as it is more // NOTE: Unread count is not included in this function as it is more
// efficient for the callers to calculate it based on filters. // efficient for the callers to calculate it based on filters.
@@ -541,6 +576,10 @@ function format_stream(stream_id: number): StreamContext {
stream_name: stream_info.name, stream_name: stream_info.name,
pin_to_top: stream_info.pin_to_top, pin_to_top: stream_info.pin_to_top,
is_muted: stream_info.is_muted, is_muted: stream_info.is_muted,
folder_id: get_channel_folder_id({
folder_id: stream_info.folder_id,
is_pinned: stream_info.pin_to_top,
}),
stream_color: stream_color.get_stream_privacy_icon_color(stream_info.color), stream_color: stream_color.get_stream_privacy_icon_color(stream_info.color),
stream_header_color: stream_color.get_recipient_bar_color(stream_info.color), stream_header_color: stream_color.get_recipient_bar_color(stream_info.color),
stream_url: hash_util.channel_url_by_user_setting(stream_id), stream_url: hash_util.channel_url_by_user_setting(stream_id),
@@ -597,6 +636,28 @@ function rerender_stream_inbox_header_if_needed(
} }
} }
function get_channel_folder_header_id(folder_id: number): string {
if (folder_id === OTHER_CHANNELS_FOLDER_ID) {
return OTHER_CHANNEL_HEADER_ID;
} else if (folder_id === PINNED_CHANNEL_FOLDER_ID) {
return PINNED_CHANNEL_HEADER_ID;
}
return CHANNEL_FOLDER_HEADER_ID_PREFIX + folder_id;
}
function rerender_channel_folder_header_if_needed(
old_folder_data: ChannelFolderContext,
new_folder_data: ChannelFolderContext,
): void {
for (const property of channel_folder_context_properties) {
if (new_folder_data[property] !== old_folder_data[property]) {
const $rendered_row = $(`#${get_channel_folder_header_id(new_folder_data.id)}`);
$rendered_row.replaceWith($(render_inbox_folder_row(new_folder_data)));
return;
}
}
}
function format_topic( function format_topic(
stream_id: number, stream_id: number,
stream_archived: boolean, stream_archived: boolean,
@@ -645,26 +706,21 @@ function format_topic(
}; };
} }
function insert_stream( function insert_stream(stream_key: string): void {
stream_id: number, const channel_folder_id = streams_dict.get(stream_key)!.folder_id;
topic_dict: Map<string, {topic_count: number; latest_msg_id: number}>, const sorted_stream_keys = get_sorted_stream_keys(channel_folder_id);
): boolean {
const stream_key = get_stream_key(stream_id);
update_stream_data(stream_id, stream_key, topic_dict);
const sorted_stream_keys = get_sorted_stream_keys();
const stream_index = sorted_stream_keys.indexOf(stream_key); const stream_index = sorted_stream_keys.indexOf(stream_key);
const rendered_stream = render_inbox_stream_container({ const rendered_stream = render_inbox_stream_container({
topics_dict: new Map([[stream_key, topics_dict.get(stream_key)]]), topics_dict: new Map([[stream_key, topics_dict.get(stream_key)]]),
streams_dict, streams_dict,
}); });
const $channel_folder_header = $(`#${get_channel_folder_header_id(channel_folder_id)}`);
if (stream_index === 0) { if (stream_index === 0) {
$("#inbox-streams-container").prepend($(rendered_stream)); $channel_folder_header.next(".inbox-folder-components").prepend($(rendered_stream));
} else { } else {
const previous_stream_key = sorted_stream_keys[stream_index - 1]!; const previous_stream_key = sorted_stream_keys[stream_index - 1]!;
$(rendered_stream).insertAfter(get_stream_container(previous_stream_key)); $(rendered_stream).insertAfter(get_stream_container(previous_stream_key));
} }
return !streams_dict.get(stream_key)!.is_hidden;
} }
function insert_topics(keys: string[], stream_key: string): void { function insert_topics(keys: string[], stream_key: string): void {
@@ -717,23 +773,23 @@ function rerender_topic_inbox_row_if_needed(
} }
} }
function get_sorted_stream_keys(): string[] { function get_sorted_stream_keys(channel_folder_id: number | undefined = undefined): string[] {
function compare_function(a: string, b: string): number { function compare_function(a: string, b: string): number {
const stream_a = streams_dict.get(a); const stream_a = streams_dict.get(a);
const stream_b = streams_dict.get(b); const stream_b = streams_dict.get(b);
assert(stream_a !== undefined && stream_b !== undefined); assert(stream_a !== undefined && stream_b !== undefined);
// If one of the streams is pinned, they are sorted higher. if (channel_folder_id !== undefined) {
if (stream_a.pin_to_top && !stream_b.pin_to_top) { // Sort streams not in the folder to the end.
return -1; if (stream_a.folder_id !== channel_folder_id) {
} return 1;
}
if (stream_b.pin_to_top && !stream_a.pin_to_top) { if (stream_b.folder_id !== channel_folder_id) {
return 1; return -1;
}
} }
// The muted stream is sorted lower. // The muted stream is sorted lower.
// (Both stream are either pinned or not pinned right now)
if (stream_a.is_muted && !stream_b.is_muted) { if (stream_a.is_muted && !stream_b.is_muted) {
return 1; return 1;
} }
@@ -766,6 +822,64 @@ function get_sorted_row_dict<T extends DirectMessageContext | TopicContext>(
return new Map([...row_dict].sort(([, a], [, b]) => b.latest_msg_id - a.latest_msg_id)); return new Map([...row_dict].sort(([, a], [, b]) => b.latest_msg_id - a.latest_msg_id));
} }
function sort_channel_folders(): void {
const sorted_channel_folders = [...channel_folders_dict.values()].sort((a, b) => {
// Sort OTHER_CHANNELS_FOLDER_ID last, then by name with PINNED_CHANNEL_FOLDER_ID first.
if (a.id === OTHER_CHANNELS_FOLDER_ID) {
return 1;
}
if (b.id === OTHER_CHANNELS_FOLDER_ID) {
return -1;
}
if (a.id === PINNED_CHANNEL_FOLDER_ID) {
return -1;
}
if (b.id === PINNED_CHANNEL_FOLDER_ID) {
return 1;
}
return util.strcmp(a.name, b.name);
});
channel_folders_dict = new Map(sorted_channel_folders.map((folder) => [folder.id, folder]));
}
function get_folder_name_from_id(folder_id: number): string {
if (folder_id === PINNED_CHANNEL_FOLDER_ID) {
return $t({defaultMessage: "PINNED CHANNELS"});
}
if (folder_id === OTHER_CHANNELS_FOLDER_ID) {
return $t({defaultMessage: "OTHER CHANNELS"});
}
return channel_folders.get_channel_folder_by_id(folder_id).name;
}
function update_channel_folder_data(channel_context: StreamContext): void {
const folder_id = channel_context.folder_id;
const folder_header_id = get_channel_folder_header_id(folder_id);
let folder_context = channel_folders_dict.get(folder_id);
if (folder_context === undefined) {
folder_context = {
id: folder_id,
header_id: folder_header_id,
name: get_folder_name_from_id(folder_id),
is_header_visible: !channel_context.is_hidden,
unread_count: channel_context.unread_count,
is_collapsed: collapsed_containers.has(folder_header_id),
has_unread_mention: channel_context.mention_in_unread,
};
channel_folders_dict.set(folder_id, folder_context);
} else {
folder_context.unread_count =
(folder_context.unread_count ?? 0) + (channel_context.unread_count ?? 0);
folder_context.is_header_visible =
folder_context.is_header_visible || !channel_context.is_hidden;
folder_context.has_unread_mention =
folder_context.has_unread_mention || channel_context.mention_in_unread;
}
}
function reset_data(): { function reset_data(): {
unread_dms_count: number; unread_dms_count: number;
is_dms_collapsed: boolean; is_dms_collapsed: boolean;
@@ -776,6 +890,7 @@ function reset_data(): {
dms_dict = new Map(); dms_dict = new Map();
topics_dict = new Map(); topics_dict = new Map();
streams_dict = new Map(); streams_dict = new Map();
channel_folders_dict = new Map();
const unread_dms = unread.get_unread_pm(); const unread_dms = unread.get_unread_pm();
const unread_dms_count = unread_dms.total_count; const unread_dms_count = unread_dms.total_count;
@@ -821,6 +936,19 @@ function reset_data(): {
topics_dict = get_sorted_stream_topic_dict(); topics_dict = get_sorted_stream_topic_dict();
const is_dms_collapsed = collapsed_containers.has("inbox-dm-header"); const is_dms_collapsed = collapsed_containers.has("inbox-dm-header");
for (const [, channel_context] of streams_dict) {
update_channel_folder_data(channel_context);
}
if (is_other_channels_only_visible_folder()) {
const other_channels_folder = channel_folders_dict.get(OTHER_CHANNELS_FOLDER_ID);
if (other_channels_folder !== undefined) {
other_channels_folder.name = $t({defaultMessage: "CHANNELS"});
}
}
sort_channel_folders();
return { return {
has_unread_mention, has_unread_mention,
unread_dms_count, unread_dms_count,
@@ -830,6 +958,20 @@ function reset_data(): {
}; };
} }
function is_other_channels_only_visible_folder(): boolean {
const visible_channel_folders = channel_folders_dict
.values()
.filter((folder) => folder.is_header_visible)
.toArray();
if (visible_channel_folders.length !== 1) {
return false;
}
const only_visible_folder = visible_channel_folders[0]!;
return only_visible_folder.id === OTHER_CHANNELS_FOLDER_ID;
}
function show_empty_inbox_text(has_visible_unreads: boolean): void { function show_empty_inbox_text(has_visible_unreads: boolean): void {
if (!has_visible_unreads) { if (!has_visible_unreads) {
$("#inbox-list").css("border-width", 0); $("#inbox-list").css("border-width", 0);
@@ -1050,6 +1192,7 @@ export function complete_rerender(): void {
dms_dict, dms_dict,
topics_dict, topics_dict,
streams_dict, streams_dict,
channel_folders_dict,
...additional_context, ...additional_context,
}), }),
); );
@@ -1152,21 +1295,7 @@ function filter_should_hide_stream_row({
} }
export function collapse_or_expand(container_id: string): void { export function collapse_or_expand(container_id: string): void {
let $toggle_icon; $(`#${container_id}`).toggleClass("inbox-collapsed-state");
let $container;
if (container_id === "inbox-dm-header") {
$container = $(`#inbox-direct-messages-container`);
$container.children().toggleClass("collapsed_container");
$toggle_icon = $("#inbox-dm-header .toggle-inbox-header-icon");
} else {
const stream_id = Number(container_id.slice(STREAM_HEADER_PREFIX.length));
$container = get_topics_container(stream_id);
$container.children().toggleClass("collapsed_container");
$toggle_icon = $(
`#${CSS.escape(STREAM_HEADER_PREFIX + stream_id)} .toggle-inbox-header-icon`,
);
}
$toggle_icon.toggleClass("icon-collapsed-state");
if (collapsed_containers.has(container_id)) { if (collapsed_containers.has(container_id)) {
collapsed_containers.delete(container_id); collapsed_containers.delete(container_id);
@@ -1195,9 +1324,24 @@ function is_list_focused(): boolean {
} }
function get_all_rows(): JQuery { function get_all_rows(): JQuery {
return $("#inbox-main .inbox-header, #inbox-main .inbox-row").not( // Get all rows in the inbox list that are not hidden by filters.
".hidden_by_filters, .collapsed_container", if (inbox_util.is_channel_view()) {
); return $(".inbox-row").not(".hidden_by_filters");
}
// This includes channel folder headers, DM / channel headers and rows.
const visible_inbox_folder_components =
"#inbox-list .inbox-folder:not(.inbox-collapsed-state) + .inbox-folder-components";
return $(
// Inbox folder headers
"#inbox-list .inbox-folder, " +
// Inbox folder components which display row without any header, i.e. DM row
`${visible_inbox_folder_components} > .inbox-row, ` +
// Inbox folder components which display header row, i.e. channel row
`${visible_inbox_folder_components} .inbox-header, ` +
// Inbox rows whose folder and header is not collapsed.
`${visible_inbox_folder_components} .inbox-header:not(.inbox-collapsed-state) + .inbox-topic-container > .inbox-row`,
).not(".hidden_by_filters");
} }
function get_row_index($elt: JQuery): number { function get_row_index($elt: JQuery): number {
@@ -1339,7 +1483,7 @@ function is_row_a_header($row: JQuery): boolean {
function set_list_focus(input_key?: string): void { function set_list_focus(input_key?: string): void {
// This function is used for both revive_current_focus and // This function is used for both revive_current_focus and
// setting focus after modify col_focus and row_focus as per // setting focus after we modify col_focus and row_focus as per
// hotkey pressed by user. // hotkey pressed by user.
// //
// When to focus on entire row? // When to focus on entire row?
@@ -1626,7 +1770,42 @@ export function change_focused_element(input_key: string): boolean {
return false; return false;
} }
function bulk_insert_channel_folders(channel_folders: Set<number>): void {
sort_channel_folders();
// Insert missing channel folders.
let index = 0;
let previous_folder_id;
for (const [folder_id, folder_context] of channel_folders_dict) {
if (channel_folders.has(folder_id)) {
const $folder_row_html = render_inbox_folder_with_channels({
...folder_context,
topics_dict,
streams_dict,
});
if (index === 0) {
const $dm_container = $("#inbox-direct-messages-container");
$dm_container.after($folder_row_html);
} else {
assert(previous_folder_id !== undefined);
const $previous_folder = $(
`#${CSS.escape(get_channel_folder_header_id(previous_folder_id))} + .inbox-folder-components`,
);
$previous_folder.after($folder_row_html);
}
}
previous_folder_id = folder_id;
index += 1;
}
}
export function update(): void { export function update(): void {
// Since inbox shows a vast amount of sorted data,
// doing surgical updates for everything is hard.
// So, we focus on updating commonly changed data
// like unread counts, mentions, collapse state, etc.
// For rare changes like stream rename, channel folder
// rename and channel folder updates, we expect the event
// path to do a complete rerender of the inbox view.
if (!inbox_util.is_visible()) { if (!inbox_util.is_visible()) {
return; return;
} }
@@ -1676,6 +1855,9 @@ export function update(): void {
$inbox_dm_header.find(".unread_mention_info").toggleClass("hidden", !has_unread_mention); $inbox_dm_header.find(".unread_mention_info").toggleClass("hidden", !has_unread_mention);
} }
const folders_info = new Map<number, {unread_count: number; has_unread_mention: boolean}>();
const channel_folders_to_insert = new Set<number>();
let has_topics_post_filter = false; let has_topics_post_filter = false;
for (const [stream_id, topic_dict] of unread_streams_dict) { for (const [stream_id, topic_dict] of unread_streams_dict) {
const stream_unread = unread.unread_count_info_for_stream(stream_id); const stream_unread = unread.unread_count_info_for_stream(stream_id);
@@ -1687,10 +1869,25 @@ export function update(): void {
// Stream isn't rendered. // Stream isn't rendered.
if (stream_topics_data === undefined) { if (stream_topics_data === undefined) {
const is_stream_visible = insert_stream(stream_id, topic_dict); update_stream_data(stream_id, stream_key, topic_dict);
if (is_stream_visible) { const channel_data = streams_dict.get(stream_key);
assert(channel_data !== undefined);
// If the folder is also not rendered, it will be once we render
// the folder, so we skip adding it.
if (channel_folders_dict.get(channel_data.folder_id)) {
insert_stream(stream_key);
}
if (!channel_data.is_hidden) {
has_topics_post_filter = true; has_topics_post_filter = true;
} }
const folder_id = channel_data.folder_id;
const folder_unread_count = folders_info.get(folder_id)?.unread_count ?? 0;
const folder_has_unread_mention =
folders_info.get(folder_id)?.has_unread_mention ?? false;
folders_info.set(folder_id, {
unread_count: folder_unread_count + channel_data.unread_count!,
has_unread_mention: folder_has_unread_mention || channel_data.mention_in_unread,
});
continue; continue;
} }
@@ -1729,6 +1926,14 @@ export function update(): void {
assert(old_stream_data !== undefined); assert(old_stream_data !== undefined);
new_stream_data.is_hidden = stream_post_filter_unread_count === 0; new_stream_data.is_hidden = stream_post_filter_unread_count === 0;
new_stream_data.unread_count = stream_post_filter_unread_count; new_stream_data.unread_count = stream_post_filter_unread_count;
const folder_id = new_stream_data.folder_id;
const folder_unread_count = folders_info.get(folder_id)?.unread_count ?? 0;
const folder_has_unread_mention =
folders_info.get(folder_id)?.has_unread_mention ?? false;
folders_info.set(folder_id, {
unread_count: folder_unread_count + stream_post_filter_unread_count,
has_unread_mention: folder_has_unread_mention || new_stream_data.mention_in_unread,
});
streams_dict.set(stream_key, new_stream_data); streams_dict.set(stream_key, new_stream_data);
rerender_stream_inbox_header_if_needed(new_stream_data, old_stream_data); rerender_stream_inbox_header_if_needed(new_stream_data, old_stream_data);
topics_dict.set(stream_key, get_sorted_row_dict(stream_topics_data)); topics_dict.set(stream_key, get_sorted_row_dict(stream_topics_data));
@@ -1740,6 +1945,55 @@ export function update(): void {
} }
} }
for (const [folder_id, folder_info] of folders_info.entries()) {
const folder_dict = channel_folders_dict.get(folder_id);
const name = get_folder_name_from_id(folder_id);
const is_collapsed = collapsed_containers.has(get_channel_folder_header_id(folder_id));
const header_id = get_channel_folder_header_id(folder_id);
const is_header_visible = folder_info.unread_count > 0;
channel_folders_dict.set(folder_id, {
header_id,
is_header_visible,
id: folder_id,
unread_count: folder_info.unread_count,
has_unread_mention: folder_info.has_unread_mention,
name,
is_collapsed,
});
if (folder_dict === undefined) {
channel_folders_to_insert.add(folder_id);
} else {
rerender_channel_folder_header_if_needed(
folder_dict,
channel_folders_dict.get(folder_id)!,
);
}
}
// Remove channel folders that are not in the updated folders_info.
const folder_ids_to_keep = new Set(folders_info.keys());
for (const [folder_id] of channel_folders_dict) {
if (!folder_ids_to_keep.has(folder_id)) {
channel_folders_dict.delete(folder_id);
const $rendered_folder_row = $(
`#${CSS.escape(get_channel_folder_header_id(folder_id))}`,
);
$rendered_folder_row.next(".inbox-folder-components").remove();
$rendered_folder_row.remove();
}
}
bulk_insert_channel_folders(channel_folders_to_insert);
// Set name of other channels folder to CHANNELS if it is the only folder.
if (is_other_channels_only_visible_folder()) {
const channel_folder = channel_folders_dict.get(OTHER_CHANNELS_FOLDER_ID)!;
channel_folder.name = $t({defaultMessage: "CHANNELS"});
const $channel_folder_header = $(`#${CSS.escape(OTHER_CHANNEL_HEADER_ID)}`);
$channel_folder_header.find(".inbox-header-name a").text(channel_folder.name);
}
const has_visible_unreads = has_dms_post_filter || has_topics_post_filter; const has_visible_unreads = has_dms_post_filter || has_topics_post_filter;
show_empty_inbox_text(has_visible_unreads); show_empty_inbox_text(has_visible_unreads);
@@ -1949,12 +2203,17 @@ export function initialize({hide_other_views}: {hide_other_views: () => void}):
return; return;
} }
const $elt = $(this); let $elt = $(this);
col_focus = COLUMNS.RECIPIENT;
focus_clicked_list_element($elt);
const href = $elt.find("a").attr("href"); const href = $elt.find("a").attr("href");
assert(href !== undefined); if (href !== undefined) {
window.location.href = href; col_focus = COLUMNS.RECIPIENT;
window.location.href = href;
} else {
$elt = $elt.closest(".inbox-header");
col_focus = COLUMNS.COLLAPSE_BUTTON;
collapse_or_expand($elt.attr("id")!);
}
focus_clicked_list_element($elt);
}); });
$("body").on("click", "#inbox-list .on_hover_dm_read", function (this: HTMLElement, e) { $("body").on("click", "#inbox-list .on_hover_dm_read", function (this: HTMLElement, e) {
@@ -1970,20 +2229,6 @@ export function initialize({hide_other_views}: {hide_other_views: () => void}):
} }
}); });
$("body").on("click", "#inbox-list .on_hover_all_dms_read", (e) => {
e.stopPropagation();
e.preventDefault();
const unread_dms_msg_ids = unread.get_msg_ids_for_private();
const unread_dms_messages = unread_dms_msg_ids.map((msg_id) => {
const message = message_store.get(msg_id);
assert(message !== undefined);
return message;
});
unread_ops.notify_server_messages_read(unread_dms_messages);
focus_inbox_search();
update_triggered_by_user = true;
});
$("body").on("click", "#inbox-list .on_hover_topic_read", function (this: HTMLElement, e) { $("body").on("click", "#inbox-list .on_hover_topic_read", function (this: HTMLElement, e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();

View File

@@ -45,7 +45,7 @@ export function update_stream_colors(): void {
return; return;
} }
const $stream_headers = $("#inbox-streams-container .inbox-header"); const $stream_headers = $(".inbox-streams-container .inbox-header");
$stream_headers.each((_index, stream_header) => { $stream_headers.each((_index, stream_header) => {
const $stream_header = $(stream_header); const $stream_header = $(stream_header);
const stream_id = Number.parseInt($stream_header.attr("data-stream-id")!, 10); const stream_id = Number.parseInt($stream_header.attr("data-stream-id")!, 10);

View File

@@ -120,6 +120,7 @@ export function dispatch_normal_event(event) {
switch (event.op) { switch (event.op) {
case "add": { case "add": {
channel_folders.add(event.channel_folder); channel_folders.add(event.channel_folder);
inbox_ui.complete_rerender();
break; break;
} }
default: default:

View File

@@ -106,14 +106,17 @@ function build_stream_popover(opts: {elt: HTMLElement; stream_id: number}): void
return; return;
} }
const is_triggered_from_inbox = elt.classList.contains("inbox-stream-menu");
const stream_hash = hash_util.channel_url_by_user_setting(stream_id); const stream_hash = hash_util.channel_url_by_user_setting(stream_id);
const show_go_to_channel_feed = const show_go_to_channel_feed =
user_settings.web_channel_default_view !== (is_triggered_from_inbox ||
web_channel_default_view_values.channel_feed.code && user_settings.web_channel_default_view !==
web_channel_default_view_values.channel_feed.code) &&
!stream_data.is_empty_topic_only_channel(stream_id); !stream_data.is_empty_topic_only_channel(stream_id);
const show_go_to_list_of_topics = const show_go_to_list_of_topics =
user_settings.web_channel_default_view !== (is_triggered_from_inbox ||
web_channel_default_view_values.list_of_topics.code && user_settings.web_channel_default_view !==
web_channel_default_view_values.list_of_topics.code) &&
!stream_data.is_empty_topic_only_channel(stream_id); !stream_data.is_empty_topic_only_channel(stream_id);
const stream_unread = unread.unread_count_info_for_stream(stream_id); const stream_unread = unread.unread_count_info_for_stream(stream_id);
const stream_unread_count = stream_unread.unmuted_count + stream_unread.muted_count; const stream_unread_count = stream_unread.unmuted_count + stream_unread.muted_count;

View File

@@ -1928,6 +1928,7 @@
hsl(0deg 0% 20%) hsl(0deg 0% 20%)
); );
--color-icons-inbox: light-dark(hsl(0deg 0% 0%), hsl(0deg 0% 100%)); --color-icons-inbox: light-dark(hsl(0deg 0% 0%), hsl(0deg 0% 100%));
--color-folder-header: light-dark(hsl(216deg 43% 20%), hsl(216deg 50% 75%));
/* Navbar dropdown menu constants - Values from Figma design */ /* Navbar dropdown menu constants - Values from Figma design */
--box-shadow-popover-menu: --box-shadow-popover-menu:

View File

@@ -61,8 +61,6 @@
#inbox-list { #inbox-list {
overflow: hidden; overflow: hidden;
border-radius: 5px;
border: 1px solid hsl(0deg 0% 0% / 20%);
/* search box left border (1px) + search box right border (1px) /* search box left border (1px) + search box right border (1px)
+ dropdown left border (1px) + dropdown right border (1px) = 4px at 16px em */ + dropdown left border (1px) + dropdown right border (1px) = 4px at 16px em */
max-width: calc( max-width: calc(
@@ -102,28 +100,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
padding: 1px 6px;
outline: 0; outline: 0;
cursor: pointer;
& a { .inbox-header-name-text {
margin: 0 4px; margin: 0;
padding: 1px 0; padding: 1px 0;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} font-weight: 600;
.inbox-header-name-focus-border {
display: flex;
align-items: center;
overflow: hidden;
border: 2px solid transparent;
border-radius: 3px;
padding-left: 4px;
}
&:focus-visible .inbox-header-name-focus-border {
border-color: var(--color-outline-focus);
} }
} }
@@ -153,10 +139,10 @@
margin-right: 3px; margin-right: 3px;
} }
.zulip-icon-user,
.stream-privacy.filter-icon { .stream-privacy.filter-icon {
/* 0 5px at 16px/1em */
padding: 0 0.3125em;
margin: 0; margin: 0;
margin-right: 1px;
} }
.zulip-icon-user { .zulip-icon-user {
@@ -167,24 +153,14 @@
.collapsible-button { .collapsible-button {
grid-area: collapse_button; grid-area: collapse_button;
visibility: hidden;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
.zulip-icon-arrow-down { .zulip-icon-chevron-down {
padding: 0.3125em 0.25em; /* 5px 4px at 16px em */ padding: 0.3125em 0.25em; /* 5px 4px at 16px em */
margin-right: 0.5625em; /* 9px at 16px em */ transform: rotate(180deg);
opacity: 0.5;
}
&.icon-collapsed-state {
visibility: visible;
.zulip-icon-arrow-down {
transform: rotate(270deg);
}
} }
} }
@@ -197,12 +173,20 @@
/* 16px at 8.5328px/1em */ /* 16px at 8.5328px/1em */
min-width: 1.8751em; min-width: 1.8751em;
top: 0; top: 0;
text-align: center;
} }
.zulip-icon-bot, .zulip-icon-bot,
.conversation-partners-icon { .conversation-partners-icon {
opacity: 0.7; opacity: 0.7;
margin-right: 5px; /* Required to align DM fullnames in user circle icon */
/* 2px at 16px / 1em */
margin-left: 0.125em;
}
.user_block .zulip-icon {
/* 0 5px at 16px/1em */
padding: 0 0.3125em;
} }
.inbox-row { .inbox-row {
@@ -231,12 +215,11 @@
grid-template-areas: "match_topic_and_dm_start recipient_info unread_mention_info unread_count"; grid-template-areas: "match_topic_and_dm_start recipient_info unread_mention_info unread_count";
} }
.fake-collapse-button,
.inbox-topic-container .user-circle { .inbox-topic-container .user-circle {
grid-area: match_topic_and_dm_start; grid-area: match_topic_and_dm_start;
} }
.recipient_info, .recipients_info,
.inbox-topic-name { .inbox-topic-name {
grid-area: recipient_info; grid-area: recipient_info;
} }
@@ -305,6 +288,8 @@
} }
.inbox-topic-name { .inbox-topic-name {
/* 16px channel icon width + 10px padding */
padding-left: 1.625em; /* 26x at 16px em */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -330,12 +315,6 @@
} }
} }
#inbox-direct-messages-container .inbox-left-part,
.inbox-topic-container .inbox-left-part {
/* 37px (50px - space occupied by user circle icon) at 16px */
padding-left: 2.3125em;
}
.inbox-left-part { .inbox-left-part {
width: 100%; width: 100%;
display: grid; display: grid;
@@ -370,6 +349,8 @@
.user_block { .user_block {
display: flex; display: flex;
align-items: center; align-items: center;
/* 3px at 16px / 1em */
margin-left: 0.1875em;
} }
} }
} }
@@ -438,12 +419,53 @@
display: none; display: none;
position: relative; position: relative;
#inbox-dm-header { .inbox-folder {
background-color: var(--color-background-private-message-header); margin-bottom: 1px;
background-color: transparent;
.inbox-focus-border {
/* Align the folder title with the channel privacy icons; 5px at 16px */
padding-left: 0.3125em;
}
.inbox-header-name-text,
.collapsible-button .zulip-icon,
.unread_mention_info,
.unread_count {
color: var(--color-folder-header);
opacity: 0.5;
}
.inbox-header-name-text {
font-style: normal;
font-weight: var(--font-weight-sidebar-heading);
line-height: 112.5%;
letter-spacing: var(--letter-spacing-sidebar-heading);
text-transform: uppercase;
outline: none;
/* 16px at 16px / 1em */
font-size: 1em;
}
&:focus-visible,
&:hover {
.inbox-header-name-text,
.collapsible-button .zulip-icon,
.unread_mention_info,
.unread_count {
opacity: 1;
}
}
&:focus-visible {
background: light-dark(
transparent,
var(--color-background-hover-popover-menu)
);
}
} }
.hidden_by_filters, .hidden_by_filters {
.collapsed_container {
display: none !important; display: none !important;
} }
} }
@@ -581,3 +603,74 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.inbox-container #inbox-pane .inbox-folder .unread_count {
display: none;
transition: none;
background-color: transparent;
cursor: default;
&:hover {
outline: 0;
}
}
.inbox-container #inbox-pane #inbox-dm-header .unread_count {
display: inline;
cursor: pointer;
}
#inbox-pane #inbox-list .collapsible-button {
visibility: hidden;
}
#inbox-pane #inbox-list .inbox-collapsed-state .collapsible-button {
visibility: visible;
.zulip-icon-chevron-down {
transform: rotate(0deg);
}
}
.inbox-container #inbox-pane .inbox-folder .unread_mention_info {
display: none;
}
.inbox-container
#inbox-pane
.inbox-folder.inbox-collapsed-state
.unread_mention_info,
.inbox-container #inbox-pane .inbox-folder.inbox-collapsed-state .unread_count {
display: inline;
}
.inbox-folder-components {
border-radius: 5px;
border: 0.5px solid hsl(0deg 0% 0% / 13%);
/* 8px at 16px / 1em */
margin-bottom: 0.5em;
overflow: hidden;
}
.inbox-folder.inbox-collapsed-state,
.inbox-folder.hidden_by_filters {
+ .inbox-folder-components {
border: 0;
}
}
#inbox-pane #inbox-list .inbox-collapsed-state {
+ .inbox-folder-components,
+ .inbox-topic-container {
display: none;
}
}
#inbox-pane
#inbox-list
.inbox-streams-container
.inbox-header
.inbox-header-name {
/* 5px at 16px / 1em */
padding: 1px 0.3125em 1px 0;
}

View File

@@ -1,21 +1,22 @@
<div id="inbox-dm-header" tabindex="0" class="inbox-header {{#unless has_dms_post_filter}}hidden_by_filters{{/unless}}"> <div id="{{ header_id }}" tabindex="0" class="inbox-header inbox-folder {{#unless is_header_visible}}hidden_by_filters{{/unless}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}">
<div class="inbox-focus-border"> <div class="inbox-focus-border">
<div class="inbox-left-part-wrapper"> <div class="inbox-left-part-wrapper">
<div class="inbox-left-part"> <div class="inbox-left-part">
<div tabindex="0" class="inbox-header-name"> <div class="inbox-header-name">
<div class="inbox-header-name-focus-border"> <span class="inbox-header-name-text">
<i class="zulip-icon zulip-icon-user"></i> {{#if is_dm_header}}
<a tabindex="-1" role="button" href="/#narrow/is/private">{{t 'Direct messages'}}</a> {{t 'DIRECT MESSAGES'}}
</div> {{else}}
{{name}}
{{/if}}
</span>
</div> </div>
<div class="collapsible-button toggle-inbox-header-icon {{#if is_collapsed}}icon-collapsed-state{{/if}}"><i class="zulip-icon zulip-icon-arrow-down"></i></div> <div class="collapsible-button"><i class="zulip-icon zulip-icon-chevron-down"></i></div>
<span class="unread_mention_info tippy-zulip-tooltip <span class="unread_mention_info tippy-zulip-tooltip
{{#unless has_unread_mention}}hidden{{/unless}}" {{#unless has_unread_mention}}hidden{{/unless}}"
data-tippy-content="{{t 'You have unread mentions' }}">@</span> data-tippy-content="{{t 'You have unread mentions' }}">@</span>
<div class="unread-count-focus-outline" tabindex="0"> <div class="unread-count-focus-outline">
<span class="unread_count tippy-zulip-tooltip on_hover_all_dms_read" <span class="unread_count quiet-count">{{unread_count}}</span>
data-tippy-content="{{t 'Mark as read' }}"
aria-label="{{t 'Mark as read' }}">{{unread_dms_count}}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,25 @@
{{> inbox_folder_row
name=name
header_id=header_id
is_header_visible=is_header_visible
is_dm_header=false
is_collapsed=is_collapsed
has_unread_mention=has_unread_mention
unread_count=unread_count
}}
<div class="inbox-streams-container inbox-folder-components">
{{#each topics_dict as |key_value_list _index|}}
{{#each ../streams_dict as |stream_key_value _stream_index|}}
{{#if (and (eq stream_key_value.[1].folder_id ../../id) (eq stream_key_value.[0] key_value_list.[0]))}}
<div id="{{key_value_list.[0]}}">
{{> inbox_row stream_key_value.[1]}}
<div class="inbox-topic-container">
{{#each key_value_list.[1]}}
{{> inbox_row this.[1]}}
{{/each}}
</div>
</div>
{{/if}}
{{/each}}
{{/each}}
</div>

View File

@@ -1,10 +1,26 @@
{{> inbox_folder_row .}} {{> inbox_folder_row
<div id="inbox-direct-messages-container"> header_id="inbox-dm-header"
is_header_visible=has_dms_post_filter
is_dm_header=true
is_collapsed=is_dms_collapsed
has_unread_mention=has_unread_mention
unread_count=unread_dms_count
}}
<div id="inbox-direct-messages-container" class="inbox-folder-components">
{{#each dms_dict}} {{#each dms_dict}}
{{> inbox_row this.[1]}} {{> inbox_row this.[1]}}
{{/each}} {{/each}}
</div> </div>
{{#each channel_folders_dict as |channel_folder_key_value_list _index|}}
<div id="inbox-streams-container"> {{> inbox_folder_with_channels
{{> inbox_stream_container . }} id=channel_folder_key_value_list.[1].id
</div> name=channel_folder_key_value_list.[1].name
header_id=channel_folder_key_value_list.[1].header_id
is_header_visible=channel_folder_key_value_list.[1].is_header_visible
is_collapsed=channel_folder_key_value_list.[1].is_collapsed
has_unread_mention=channel_folder_key_value_list.[1].has_unread_mention
unread_count=channel_folder_key_value_list.[1].unread_count
topics_dict=../topics_dict
streams_dict=../streams_dict
}}
{{/each}}

View File

@@ -1,11 +1,10 @@
{{#if is_stream}} {{#if is_stream}}
{{> inbox_stream_header_row .}} {{> inbox_stream_header_row .}}
{{else}} {{else}}
<div id="inbox-row-conversation-{{conversation_key}}" class="inbox-row {{#if is_hidden}}hidden_by_filters{{/if}} {{#if is_collapsed}}collapsed_container{{/if}}" tabindex="0" data-col-index="{{ column_indexes.RECIPIENT }}"> <div id="inbox-row-conversation-{{conversation_key}}" class="inbox-row {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0" data-col-index="{{ column_indexes.RECIPIENT }}">
<div class="inbox-focus-border"> <div class="inbox-focus-border">
<div class="inbox-left-part-wrapper"> <div class="inbox-left-part-wrapper">
<div class="inbox-left-part"> <div class="inbox-left-part">
<div class="hide fake-collapse-button" tabindex="0" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}"></div>
{{#if is_direct}} {{#if is_direct}}
<a class="recipients_info {{#unless user_circle_class}}inbox-group-or-bot-dm{{/unless}}" href="{{dm_url}}" tabindex="-1"> <a class="recipients_info {{#unless user_circle_class}}inbox-group-or-bot-dm{{/unless}}" href="{{dm_url}}" tabindex="-1">
<span class="user_block"> <span class="user_block">
@@ -19,6 +18,7 @@
<span class="recipients_name">{{{rendered_dm_with}}}</span> <span class="recipients_name">{{{rendered_dm_with}}}</span>
</span> </span>
</a> </a>
<div class="hide fake-collapse-button" tabindex="0" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}"></div>
<span class="unread_mention_info tippy-zulip-tooltip <span class="unread_mention_info tippy-zulip-tooltip
{{#unless has_unread_mention}}hidden{{/unless}}" {{#unless has_unread_mention}}hidden{{/unless}}"
data-tippy-content="{{t 'You have unread mentions' }}">@</span> data-tippy-content="{{t 'You have unread mentions' }}">@</span>
@@ -28,11 +28,10 @@
aria-label="{{t 'Mark as read' }}">{{unread_count}}</span> aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
</div> </div>
{{else if is_topic}} {{else if is_topic}}
{{!-- Invisible user circle element for alignment of topic text with DM user name --}}
<span class="user-circle-active user-circle invisible"></span>
<div class="inbox-topic-name"> <div class="inbox-topic-name">
<a tabindex="-1" href="{{topic_url}}" {{#if is_empty_string_topic}}class="empty-topic-display"{{/if}}>{{topic_display_name}}</a> <a tabindex="-1" href="{{topic_url}}" {{#if is_empty_string_topic}}class="empty-topic-display"{{/if}}>{{topic_display_name}}</a>
</div> </div>
<div class="hide fake-collapse-button" tabindex="0" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}"></div>
<span class="unread_mention_info tippy-zulip-tooltip <span class="unread_mention_info tippy-zulip-tooltip
{{#unless mention_in_unread}}hidden{{/unless}}" {{#unless mention_in_unread}}hidden{{/unless}}"
data-tippy-content="{{t 'You have unread mentions'}}">@</span> data-tippy-content="{{t 'You have unread mentions'}}">@</span>

View File

@@ -1,25 +1,23 @@
<div id="inbox-stream-header-{{stream_id}}" class="inbox-header {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0" data-stream-id="{{stream_id}}" style="background: {{stream_header_color}};"> <div id="inbox-stream-header-{{stream_id}}" class="inbox-header {{#if is_hidden}}hidden_by_filters{{/if}} {{#if is_collapsed}}inbox-collapsed-state{{/if}}" data-col-index="{{ column_indexes.COLLAPSE_BUTTON }}" tabindex="0" data-stream-id="{{stream_id}}" style="background: {{stream_header_color}};">
<div class="inbox-focus-border"> <div class="inbox-focus-border">
<div class="inbox-left-part-wrapper"> <div class="inbox-left-part-wrapper">
<div class="inbox-left-part"> <div class="inbox-left-part">
<div tabindex="0" class="inbox-header-name"> <div class="inbox-header-name">
<div class="inbox-header-name-focus-border"> <span class="stream-privacy-original-color-{{stream_id}} stream-privacy filter-icon" style="color: {{stream_color}}">
<span class="stream-privacy-original-color-{{stream_id}} stream-privacy filter-icon" style="color: {{stream_color}}"> {{> ../stream_privacy . }}
{{> ../stream_privacy . }} </span>
<span class="inbox-header-name-text">{{stream_name}}</span>
{{#if is_archived}}
<span class="inbox-header-stream-archived">
<i class="archived-indicator">({{t 'archived' }})</i>
</span> </span>
<a tabindex="-1" href="{{stream_url}}">{{stream_name}}</a> {{/if}}
{{#if is_archived}}
<span class="inbox-header-stream-archived">
<i class="archived-indicator">({{t 'archived' }})</i>
</span>
{{/if}}
</div>
</div> </div>
<div class="collapsible-button toggle-inbox-header-icon {{#if is_collapsed}}icon-collapsed-state{{/if}}"><i class="zulip-icon zulip-icon-arrow-down"></i></div> <div class="collapsible-button toggle-inbox-header-icon {{#if is_collapsed}}icon-collapsed-state{{/if}}"><i class="zulip-icon zulip-icon-chevron-down"></i></div>
<span class="unread_mention_info tippy-zulip-tooltip <span class="unread_mention_info tippy-zulip-tooltip
{{#unless mention_in_unread}}hidden{{/unless}}" {{#unless mention_in_unread}}hidden{{/unless}}"
data-tippy-content="{{t 'You have unread mentions'}}">@</span> data-tippy-content="{{t 'You have unread mentions'}}">@</span>
<div class="unread-count-focus-outline" tabindex="0"> <div class="unread-count-focus-outline" tabindex="0" data-col-index="{{ column_indexes.UNREAD_COUNT }}">
<span class="unread_count tippy-zulip-tooltip on_hover_topic_read" <span class="unread_count tippy-zulip-tooltip on_hover_topic_read"
data-stream-id="{{stream_id}}" data-tippy-content="{{t 'Mark as read' }}" data-stream-id="{{stream_id}}" data-tippy-content="{{t 'Mark as read' }}"
aria-label="{{t 'Mark as read' }}">{{unread_count}}</span> aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
@@ -29,7 +27,7 @@
</div> </div>
<div class="inbox-right-part-wrapper"> <div class="inbox-right-part-wrapper">
<div class="inbox-right-part"> <div class="inbox-right-part">
<div class="inbox-action-button inbox-stream-menu" data-stream-id="{{stream_id}}" tabindex="0"> <div class="inbox-action-button inbox-stream-menu" data-stream-id="{{stream_id}}" tabindex="0" data-col-index="{{ column_indexes.ACTION_MENU }}">
<i class="zulip-icon zulip-icon-more-vertical" aria-hidden="true"></i> <i class="zulip-icon zulip-icon-more-vertical" aria-hidden="true"></i>
</div> </div>
</div> </div>

View File

@@ -59,4 +59,9 @@ run_test("basics", () => {
assert.ok(channel_folders.is_valid_folder_id(frontend_folder.id)); assert.ok(channel_folders.is_valid_folder_id(frontend_folder.id));
assert.ok(!channel_folders.is_valid_folder_id(999)); assert.ok(!channel_folders.is_valid_folder_id(999));
assert.equal(
channel_folders.get_channel_folder_name_from_id(frontend_folder.id),
frontend_folder.name,
);
}); });