left_sidebar: Change channel sections to togglable sections.

This commit is contained in:
Evy Kassirer
2025-06-20 21:30:50 -07:00
committed by Tim Abbott
parent dc013b6a10
commit 27092d5543
9 changed files with 373 additions and 396 deletions

View File

@@ -134,10 +134,10 @@ export function update_invite_user_option(): void {
export function update_unread_counts_visibility(): void { export function update_unread_counts_visibility(): void {
const hidden = !user_settings.web_left_sidebar_unreads_count_summary; const hidden = !user_settings.web_left_sidebar_unreads_count_summary;
const $streams_header: JQuery = $("#streams_header"); const $channel_sections: JQuery = $(".stream-list-subsection-header");
const $home_view_li: JQuery = $(".top_left_row"); const $home_view_li: JQuery = $(".top_left_row");
for (const $el of [$home_view_li, $streams_header]) { for (const $el of [$home_view_li, $channel_sections]) {
$el.toggleClass("hide-unread-messages-count", hidden); $el.toggleClass("hide-unread-messages-count", hidden);
} }
} }

View File

@@ -6,9 +6,9 @@ import * as tippy from "tippy.js";
import render_filter_topics from "../templates/filter_topics.hbs"; import render_filter_topics from "../templates/filter_topics.hbs";
import render_go_to_channel_feed_tooltip from "../templates/go_to_channel_feed_tooltip.hbs"; import render_go_to_channel_feed_tooltip from "../templates/go_to_channel_feed_tooltip.hbs";
import render_go_to_channel_list_of_topics_tooltip from "../templates/go_to_channel_list_of_topics_tooltip.hbs"; import render_go_to_channel_list_of_topics_tooltip from "../templates/go_to_channel_list_of_topics_tooltip.hbs";
import render_stream_list_section_container from "../templates/stream_list_section_container.hbs";
import render_stream_privacy from "../templates/stream_privacy.hbs"; import render_stream_privacy from "../templates/stream_privacy.hbs";
import render_stream_sidebar_row from "../templates/stream_sidebar_row.hbs"; import render_stream_sidebar_row from "../templates/stream_sidebar_row.hbs";
import render_stream_subheader from "../templates/streams_subheader.hbs";
import render_subscribe_to_more_streams from "../templates/subscribe_to_more_streams.hbs"; import render_subscribe_to_more_streams from "../templates/subscribe_to_more_streams.hbs";
import * as blueslip from "./blueslip.ts"; import * as blueslip from "./blueslip.ts";
@@ -16,7 +16,6 @@ import * as browser_history from "./browser_history.ts";
import * as compose_actions from "./compose_actions.ts"; import * as compose_actions from "./compose_actions.ts";
import type {Filter} from "./filter.ts"; import type {Filter} from "./filter.ts";
import * as hash_util from "./hash_util.ts"; import * as hash_util from "./hash_util.ts";
import {$t} from "./i18n.ts";
import * as keydown_util from "./keydown_util.ts"; import * as keydown_util from "./keydown_util.ts";
import {ListCursor} from "./list_cursor.ts"; import {ListCursor} from "./list_cursor.ts";
import * as narrow_state from "./narrow_state.ts"; import * as narrow_state from "./narrow_state.ts";
@@ -29,6 +28,7 @@ import * as settings_data from "./settings_data.ts";
import * as sidebar_ui from "./sidebar_ui.ts"; import * as sidebar_ui from "./sidebar_ui.ts";
import * as stream_data from "./stream_data.ts"; import * as stream_data from "./stream_data.ts";
import * as stream_list_sort from "./stream_list_sort.ts"; import * as stream_list_sort from "./stream_list_sort.ts";
import type {StreamListSection} from "./stream_list_sort.ts";
import * as stream_topic_history from "./stream_topic_history.ts"; import * as stream_topic_history from "./stream_topic_history.ts";
import * as stream_topic_history_util from "./stream_topic_history_util.ts"; import * as stream_topic_history_util from "./stream_topic_history_util.ts";
import * as sub_store from "./sub_store.ts"; import * as sub_store from "./sub_store.ts";
@@ -58,6 +58,8 @@ export function rewire_stream_cursor(value: typeof stream_cursor): void {
let has_scrolled = false; let has_scrolled = false;
const collapsed_sections = new Set<string>();
export function is_zoomed_in(): boolean { export function is_zoomed_in(): boolean {
return zoomed_in; return zoomed_in;
} }
@@ -260,6 +262,19 @@ export function create_initial_sidebar_rows(force_rerender = false): void {
} }
} }
export let stream_list_section_container_html = function (section: StreamListSection): string {
return render_stream_list_section_container({
id: section.id,
section_title: section.section_title,
});
};
export function rewire_stream_list_section_container_html(
value: typeof stream_list_section_container_html,
): void {
stream_list_section_container_html = value;
}
export function build_stream_list(force_rerender: boolean): void { export function build_stream_list(force_rerender: boolean): void {
// The stream list in the left sidebar contains 3 sections: // The stream list in the left sidebar contains 3 sections:
// pinned, normal, and dormant streams, with headings above them // pinned, normal, and dormant streams, with headings above them
@@ -277,88 +292,53 @@ export function build_stream_list(force_rerender: boolean): void {
return; return;
} }
const $parent = $("#stream_filters"); function add_sidebar_li(stream_id: number, $list: JQuery): void {
const elems = [];
function add_sidebar_li(stream_id: number): void {
const sidebar_row = stream_sidebar.get_row(stream_id); const sidebar_row = stream_sidebar.get_row(stream_id);
assert(sidebar_row !== undefined); assert(sidebar_row !== undefined);
sidebar_row.update_whether_active(); sidebar_row.update_whether_active();
elems.push(sidebar_row.get_li()); $list.append($(sidebar_row.get_li()));
} }
clear_topics(); clear_topics();
$parent.empty(); $("#stream_filters").empty();
for (const section of stream_groups.sections) {
$("#stream_filters").append($(stream_list_section_container_html(section)));
const is_empty = section.streams.length === 0 && section.muted_streams.length === 0;
$(`#stream-list-${section.id}-container`).toggleClass("no-display", is_empty);
const any_pinned_streams = for (const stream_id of [...section.streams, ...section.muted_streams]) {
stream_groups.pinned_streams.length > 0 || stream_groups.muted_pinned_streams.length > 0; add_sidebar_li(stream_id, $(`#stream-list-${section.id}`));
const any_normal_streams =
stream_groups.normal_streams.length > 0 || stream_groups.muted_active_streams.length > 0;
const any_dormant_streams = stream_groups.dormant_streams.length > 0;
const need_section_subheaders =
(any_pinned_streams ? 1 : 0) +
(any_normal_streams ? 1 : 0) +
(any_dormant_streams ? 1 : 0) >=
2;
if (any_pinned_streams && need_section_subheaders) {
elems.push(
$(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Pinned",
}),
}),
),
);
} }
for (const stream_id of stream_groups.pinned_streams) {
add_sidebar_li(stream_id);
} }
sidebar_ui.update_unread_counts_visibility();
collapse_collapsed_sections();
}
for (const stream_id of stream_groups.muted_pinned_streams) { function toggle_section_collapse($container: JQuery): void {
add_sidebar_li(stream_id); $container.toggleClass("collapsed");
const is_collapsed = $container.hasClass("collapsed");
$container
.find(".stream-list-section-toggle")
.toggleClass("rotate-icon-down", !is_collapsed)
.toggleClass("rotate-icon-right", is_collapsed);
const section_id = $container.attr("data-section-id")!;
if (is_collapsed) {
collapsed_sections.add(section_id);
} else {
collapsed_sections.delete(section_id);
} }
}
if (any_normal_streams && need_section_subheaders) { function collapse_collapsed_sections(): void {
elems.push( for (const section_id of collapsed_sections) {
$( const $container = $(`#stream-list-${section_id}-container`);
render_stream_subheader({ $container.toggleClass("collapsed", true);
subheader_name: $t({ $container
defaultMessage: "Active", .find(".stream-list-section-toggle")
}), .toggleClass("rotate-icon-down", false)
}), .toggleClass("rotate-icon-right", true);
),
);
} }
for (const stream_id of stream_groups.normal_streams) {
add_sidebar_li(stream_id);
}
for (const stream_id of stream_groups.muted_active_streams) {
add_sidebar_li(stream_id);
}
if (any_dormant_streams && need_section_subheaders) {
elems.push(
$(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Inactive",
}),
}),
),
);
}
for (const stream_id of stream_groups.dormant_streams) {
add_sidebar_li(stream_id);
}
$parent.append(elems); // eslint-disable-line no-jquery/no-append-html
} }
export function get_stream_li(stream_id: number): JQuery | undefined { export function get_stream_li(stream_id: number): JQuery | undefined {
@@ -416,11 +396,6 @@ export function zoom_in_topics(options: {stream_id: number | undefined}): void {
$("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in"); $("#streams_list").expectOne().removeClass("zoom-out").addClass("zoom-in");
// Hide pinned stream splitter
$(".streams_subheader").each(function () {
$(this).hide();
});
$("#stream_filters li.narrow-filter").each(function () { $("#stream_filters li.narrow-filter").each(function () {
const $elt = $(this); const $elt = $(this);
const stream_id = options.stream_id; const stream_id = options.stream_id;
@@ -438,11 +413,6 @@ export function zoom_in_topics(options: {stream_id: number | undefined}): void {
} }
export function zoom_out_topics(): void { export function zoom_out_topics(): void {
// Show pinned stream splitter
$(".streams_subheader").each(function () {
$(this).show();
});
$("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out"); $("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out");
$("#stream_filters li.narrow-filter").toggleClass("hide", false); $("#stream_filters li.narrow-filter").toggleClass("hide", false);
// Remove search box for topics list from DOM. // Remove search box for topics list from DOM.
@@ -1113,6 +1083,15 @@ export function set_event_handlers({
stream_cursor.clear(); stream_cursor.clear();
}); });
$search_input.on("input", update_streams_for_search); $search_input.on("input", update_streams_for_search);
$("#streams_list").on(
"click",
".stream-list-section-container .stream-list-subsection-header",
function (this: HTMLElement, e: JQuery.ClickEvent) {
e.stopPropagation();
toggle_section_collapse($(this).closest(".stream-list-section-container"));
},
);
} }
export function searching(): boolean { export function searching(): boolean {
@@ -1147,15 +1126,29 @@ export function clear_search(): void {
$filter.trigger("blur"); $filter.trigger("blur");
} }
function scroll_stream_into_view($stream_li: JQuery): void { export let scroll_stream_into_view = function ($stream_li: JQuery): void {
const $container = $("#left_sidebar_scroll_container"); const $container = $("#left_sidebar_scroll_container");
if ($stream_li.length !== 1) { if ($stream_li.length !== 1) {
blueslip.error("Invalid stream_li was passed in"); blueslip.error("Invalid stream_li was passed in");
return; return;
} }
const stream_header_height = $("#streams_header").outerHeight(); const stream_filter_height = $(".stream_search_section").outerHeight()!;
scroll_util.scroll_element_into_container($stream_li, $container, stream_header_height); const header_height = $stream_li
.closest(".stream-list-section-container")
.children(".stream-list-subsection-header")
.outerHeight()!;
scroll_util.scroll_element_into_container(
$stream_li,
$container,
stream_filter_height + header_height,
);
// Note: If the stream is in a collapsed folder, we don't uncollapse the
// folder.
};
export function rewire_scroll_stream_into_view(value: typeof scroll_stream_into_view): void {
scroll_stream_into_view = value;
} }
export function maybe_scroll_narrow_into_view(first_messages_fetch_done: boolean): void { export function maybe_scroll_narrow_into_view(first_messages_fetch_done: boolean): void {

View File

@@ -1,5 +1,6 @@
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import {$t} from "./i18n.ts";
import * as settings_config from "./settings_config.ts"; import * as settings_config from "./settings_config.ts";
import * as stream_data from "./stream_data.ts"; import * as stream_data from "./stream_data.ts";
import * as sub_store from "./sub_store.ts"; import * as sub_store from "./sub_store.ts";
@@ -8,11 +9,7 @@ import {user_settings} from "./user_settings.ts";
import * as util from "./util.ts"; import * as util from "./util.ts";
let first_render_completed = false; let first_render_completed = false;
let previous_pinned: number[] = []; let current_sections: StreamListSection[] = [];
let previous_normal: number[] = [];
let previous_dormant: number[] = [];
let previous_muted_active: number[] = [];
let previous_muted_pinned: number[] = [];
let all_streams: number[] = []; let all_streams: number[] = [];
// Because we need to check whether we are filtering inactive streams // Because we need to check whether we are filtering inactive streams
@@ -71,13 +68,16 @@ export function has_recent_activity(sub: StreamSubscription): boolean {
return sub.is_recently_active || sub.newly_subscribed; return sub.is_recently_active || sub.newly_subscribed;
} }
export type StreamListSection = {
id: string;
section_title: string;
streams: number[];
muted_streams: number[]; // Not used for the inactive section
};
type StreamListSortResult = { type StreamListSortResult = {
same_as_before: boolean; same_as_before: boolean;
pinned_streams: number[]; sections: StreamListSection[];
normal_streams: number[];
dormant_streams: number[];
muted_pinned_streams: number[];
muted_active_streams: number[];
}; };
export function sort_groups(stream_ids: number[], search_term: string): StreamListSortResult { export function sort_groups(stream_ids: number[], search_term: string): StreamListSortResult {
@@ -95,74 +95,81 @@ export function sort_groups(stream_ids: number[], search_term: string): StreamLi
return has_recent_activity(sub); return has_recent_activity(sub);
} }
const pinned_streams = []; const pinned_section: StreamListSection = {
const normal_streams = []; id: "pinned-streams",
const muted_pinned_streams = []; section_title: $t({defaultMessage: "PINNED CHANNELS"}),
const muted_active_streams = []; streams: [],
const dormant_streams = []; muted_streams: [],
};
const normal_section: StreamListSection = {
id: "normal-streams",
section_title: $t({defaultMessage: "ACTIVE CHANNELS"}),
streams: [],
muted_streams: [],
};
const dormant_section: StreamListSection = {
id: "dormant-streams",
section_title: $t({defaultMessage: "INACTIVE CHANNELS"}),
streams: [],
muted_streams: [], // Not used for the dormant section
};
for (const stream_id of stream_ids) { for (const stream_id of stream_ids) {
const sub = sub_store.get(stream_id); const sub = sub_store.get(stream_id);
assert(sub); assert(sub);
const pinned = sub.pin_to_top;
if (sub.is_archived) { if (sub.is_archived) {
continue; continue;
} }
if (pinned) { if (sub.pin_to_top) {
if (!sub.is_muted) { if (sub.is_muted) {
pinned_streams.push(stream_id); pinned_section.muted_streams.push(stream_id);
} else { } else {
muted_pinned_streams.push(stream_id); pinned_section.streams.push(stream_id);
} }
} else if (is_normal(sub)) { } else if (is_normal(sub)) {
if (!sub.is_muted) { if (sub.is_muted) {
normal_streams.push(stream_id); normal_section.muted_streams.push(stream_id);
} else { } else {
muted_active_streams.push(stream_id); normal_section.streams.push(stream_id);
} }
} else { } else {
dormant_streams.push(stream_id); dormant_section.streams.push(stream_id);
} }
} }
pinned_streams.sort(compare_function); // This needs to have the same ordering as the order they're displayed in the sidebar.
normal_streams.sort(compare_function); const new_sections = [pinned_section, normal_section, dormant_section];
muted_pinned_streams.sort(compare_function);
muted_active_streams.sort(compare_function); for (const section of new_sections) {
dormant_streams.sort(compare_function); section.streams.sort(compare_function);
section.muted_streams.sort(compare_function);
}
const same_as_before = const same_as_before =
first_render_completed && first_render_completed &&
util.array_compare(previous_pinned, pinned_streams) && new_sections.entries().every(([i, new_section]) => {
util.array_compare(previous_normal, normal_streams) && const current_section = current_sections.at(i);
util.array_compare(previous_muted_pinned, muted_pinned_streams) && return (
util.array_compare(previous_muted_active, muted_active_streams) && current_section !== undefined &&
util.array_compare(previous_dormant, dormant_streams); new_section.id === current_section.id &&
new_section.section_title === current_section.section_title &&
util.array_compare(new_section.streams, current_section.streams) &&
util.array_compare(new_section.muted_streams, current_section.muted_streams)
);
});
if (!same_as_before) { if (!same_as_before) {
first_render_completed = true; first_render_completed = true;
previous_pinned = pinned_streams; current_sections = new_sections;
previous_normal = normal_streams; all_streams = new_sections.flatMap((section) => [
previous_muted_pinned = muted_pinned_streams; ...section.streams,
previous_muted_active = muted_active_streams; ...section.muted_streams,
previous_dormant = dormant_streams; ]);
all_streams = [
...pinned_streams,
...muted_pinned_streams,
...normal_streams,
...muted_active_streams,
...dormant_streams,
];
} }
return { return {
same_as_before, same_as_before,
pinned_streams, sections: new_sections,
normal_streams,
dormant_streams,
muted_pinned_streams,
muted_active_streams,
}; };
} }

View File

@@ -81,7 +81,7 @@
} }
} }
.selected-home-view, #left-sidebar-navigation-list .selected-home-view,
#streams_header { #streams_header {
&.hide-unread-messages-count { &.hide-unread-messages-count {
.masked_unread_count { .masked_unread_count {
@@ -101,7 +101,7 @@
} }
} }
.selected-home-view:hover, #left-sidebar-navigation-list .selected-home-view:hover,
#streams_header:hover, #streams_header:hover,
.selected-home-view.top-left-active-filter { .selected-home-view.top-left-active-filter {
&.hide-unread-messages-count { &.hide-unread-messages-count {
@@ -310,6 +310,71 @@
} }
} }
.stream_search_section {
position: sticky;
top: 0;
/* Must be more than .stream-list-subsection-header */
z-index: 3;
/* Must be padding not margin so that the sticky headers don't show behind it */
padding: var(--left-sidebar-sections-vertical-gutter)
var(--left-sidebar-right-margin) 3px 5px;
}
.stream-list-subsection-header {
display: grid;
align-items: center;
grid-template:
"arrow row-content" var(--line-height-sidebar-row-prominent)
/ var(--right-sidebar-header-icon-toggle-width) minmax(0, 1fr);
cursor: pointer;
background-color: var(--color-background);
position: sticky;
/* Input is 31px tall at 16px/1em.
Input container has 8px and 3px top/bottom padding = 11px total padding */
top: calc(1.9375em + 11px);
/* Must be more than .sidebar-topic-check and less than #stream-search-and-add */
z-index: 2;
color: var(--color-text-default);
/* There seems to be a bug where when the header returns to normal position after
being sticky, it's 0.5px below its actual position and thus overlaps the channel
below. This extra margin prevents that overlap. */
margin-bottom: 1px;
&:hover {
background-color: var(--color-background-opaque-hover-narrow-filter);
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
/* We only set the border radius on the hover/popover states,
so as to prevent the background on highlighted channels
from bleeding through. */
border-radius: 4px;
.left-sidebar-title,
.stream-list-section-toggle {
opacity: var(--opacity-sidebar-heading-hover);
}
}
.stream-list-section-toggle {
color: var(--color-text-sidebar-heading);
opacity: var(--opacity-sidebar-heading-icon);
justify-self: center;
}
}
.stream-list-section {
margin: 0;
}
.stream-list-section-container.no-display {
display: none;
}
.stream-list-section-container.collapsed {
.narrow-filter {
display: none;
}
}
.direct-messages-container { .direct-messages-container {
/* Properly offset all the grid rows /* Properly offset all the grid rows
in the DM section. */ in the DM section. */
@@ -1407,7 +1472,7 @@ li.top_left_scheduled_messages {
/* As a grid item, adjust the checkmark's z-index here so /* As a grid item, adjust the checkmark's z-index here so
that the background color appears above the grouping that the background color appears above the grouping
bracket's bottom line. Its value must less than bracket's bottom line. Its value must less than
the z-index set on the #streams_header selector. */ the z-index set on .stream-list-subsection-header */
z-index: 1; z-index: 1;
} }
@@ -1871,49 +1936,6 @@ li.topic-list-item {
} }
} }
.streams_subheader {
/* 14px at 16px/1em */
font-size: 0.875em;
font-weight: normal;
/* 16px line-height at 0.8em (11.2px at 14px legacy em) */
line-height: 1.4286em;
letter-spacing: 0.04em;
padding-left: var(--left-sidebar-toggle-width-offset);
cursor: pointer;
text-align: center;
margin-right: var(--left-sidebar-right-margin);
& .streams-subheader-wrapper {
display: flex;
flex-direction: row;
width: 100%;
left: 0.5em;
right: 0.5em;
color: var(--color-text-sidebar-base);
}
& .streams-subheader-wrapper::before,
.streams-subheader-wrapper::after {
content: " ";
flex: 1 1;
vertical-align: middle;
margin: auto;
border-top: 1px solid var(--color-border-sidebar-subheader);
}
& .streams-subheader-wrapper::before {
margin-right: 0.2em;
}
& .streams-subheader-wrapper::after {
margin-left: 0.2em;
}
.streams-subheader-name {
opacity: 0.4;
}
}
.zero_count { .zero_count {
visibility: hidden; visibility: hidden;
} }
@@ -1929,13 +1951,14 @@ li.topic-list-item {
.narrow-filter > .bottom_left_row { .narrow-filter > .bottom_left_row {
position: sticky; position: sticky;
/* We subtract a quarter pixel of space to correct /* We need to hold the space where the BACK TO CHANNELS
for possible bleedthrough under certain viewing line sits, so the channel info doesn't run over the
conditions (e.g., external monitors.) This same top of it when scrolling down. These are the same
technique is used on #streams_header. */ variables for setting the space on the BACK TO CHANNELS
grid row plus its top padding: */
top: calc( top: calc(
var(--left-sidebar-sections-vertical-gutter) + var(--line-height-sidebar-row-prominent) +
var(--line-height-sidebar-row-prominent) - 0.25px var(--left-sidebar-sections-vertical-gutter)
); );
z-index: 2; z-index: 2;
padding-bottom: 1px; padding-bottom: 1px;

View File

@@ -195,7 +195,8 @@
</div> </div>
<div id="streams_list" class="zoom-out"> <div id="streams_list" class="zoom-out">
<div id="streams_header" class="showing-stream-search-section zoom-in-hide"> {{!-- TODO: Future commits will hide #streams_header completely, but for smaller reviewable commits this commit just hides the header for now --}}
<div id="streams_header" style="display: none;" class="showing-stream-search-section zoom-in-hide">
<h4 class="left-sidebar-title"><span class="streams-tooltip-target">{{t 'CHANNELS' }}</span></h4> <h4 class="left-sidebar-title"><span class="streams-tooltip-target">{{t 'CHANNELS' }}</span></h4>
<div class="left-sidebar-controls"> <div class="left-sidebar-controls">
<span id="add_streams_tooltip" class="hidden-for-spectators"> <span id="add_streams_tooltip" class="hidden-for-spectators">
@@ -208,12 +209,10 @@
<i class="zulip-icon zulip-icon-masked-unread"></i> <i class="zulip-icon zulip-icon-masked-unread"></i>
</span> </span>
</div> </div>
<div class="left-sidebar-filter-input-container"> </div>
{{#> input_wrapper input_type="filter-input" custom_classes="stream_search_section" icon="search" input_button_icon="close"}} {{#> input_wrapper input_type="filter-input" custom_classes="stream_search_section" icon="search" input_button_icon="close"}}
<input type="text" class="input-element stream-list-filter home-page-input" autocomplete="off" placeholder="{{t 'Filter channels' }}" /> <input type="text" class="input-element stream-list-filter home-page-input" autocomplete="off" placeholder="{{t 'Filter channels' }}" />
{{/input_wrapper}} {{/input_wrapper}}
</div>
</div>
<div id="topics_header"> <div id="topics_header">
<a class="show-all-streams trigger-click-on-enter" tabindex="0">{{t 'Back to channels' }}</a> <span class="unread_count quiet-count"></span> <a class="show-all-streams trigger-click-on-enter" tabindex="0">{{t 'Back to channels' }}</a> <span class="unread_count quiet-count"></span>
</div> </div>

View File

@@ -0,0 +1,9 @@
<div id="stream-list-{{id}}-container" data-section-id="{{id}}" class="stream-list-section-container">
<div class="stream-list-subsection-header zoom-in-hide">
<i class="stream-list-section-toggle zulip-icon zulip-icon-heading-triangle-right rotate-icon-down" aria-hidden="true"></i>
<h4 class="left-sidebar-title">
{{section_title}}
</h4>
</div>
<ul id="stream-list-{{id}}" class="stream-list-section"></ul>
</div>

View File

@@ -1,7 +0,0 @@
<div class="streams_subheader">
<span class="streams-subheader-wrapper">
<span class="streams-subheader-name">
{{ subheader_name }}
</span>
</span>
</div>

View File

@@ -21,7 +21,7 @@ let unread_unmuted_count;
let stream_has_any_unread_mentions; let stream_has_any_unread_mentions;
const topic_list = mock_esm("../src/topic_list"); const topic_list = mock_esm("../src/topic_list");
const scroll_util = mock_esm("../src/scroll_util", { mock_esm("../src/scroll_util", {
scroll_element_into_container() {}, scroll_element_into_container() {},
get_scroll_element: ($element) => $element, get_scroll_element: ($element) => $element,
}); });
@@ -101,11 +101,6 @@ const social = {
can_send_message_group: everyone_group.id, can_send_message_group: everyone_group.id,
}; };
// flag to check if subheader is rendered
let pinned_subheader_flag = false;
let active_subheader_flag = false;
let inactive_subheader_flag = false;
function create_devel_sidebar_row({mock_template}) { function create_devel_sidebar_row({mock_template}) {
const $devel_count = $.create("devel-count"); const $devel_count = $.create("devel-count");
const $subscription_block = $.create("devel-block"); const $subscription_block = $.create("devel-block");
@@ -152,22 +147,6 @@ function create_social_sidebar_row({mock_template}) {
assert.equal($social_unread_mention_info.text(), "@"); assert.equal($social_unread_mention_info.text(), "@");
} }
function create_stream_subheader({mock_template}) {
mock_template("streams_subheader.hbs", false, (data) => {
if (data.subheader_name === "translated: Pinned") {
pinned_subheader_flag = true;
return "<pinned-subheader-stub>";
} else if (data.subheader_name === "translated: Active") {
active_subheader_flag = true;
return "<active-subheader-stub>";
}
assert.ok(data.subheader_name === "translated: Inactive");
inactive_subheader_flag = true;
return "<inactive-subheader-stub>";
});
}
function test_ui(label, f) { function test_ui(label, f) {
run_test(label, (helpers) => { run_test(label, (helpers) => {
stream_data.clear_subscriptions(); stream_data.clear_subscriptions();
@@ -176,48 +155,46 @@ function test_ui(label, f) {
}); });
} }
test_ui("create_sidebar_row", ({override, mock_template}) => { test_ui("create_sidebar_row", ({override, override_rewire, mock_template}) => {
// Make a couple calls to create_sidebar_row() and make sure they // Make a couple calls to create_sidebar_row() and make sure they
// generate the right markup as well as play nice with get_stream_li(). // generate the right markup as well as play nice with get_stream_li().
override(user_settings, "demote_inactive_streams", 1); override(user_settings, "demote_inactive_streams", 1);
const appended_sections = [];
override_rewire(stream_list, "stream_list_section_container_html", (section) => {
appended_sections.push(section.id);
return `<stub-section-${section.id}>`;
});
const pinned_streams = [];
$("#stream-list-pinned-streams").append = (stream) => {
pinned_streams.push(stream);
};
const normal_streams = [];
$("#stream-list-normal-streams").append = (stream) => {
normal_streams.push(stream);
};
stream_data.add_sub(devel); stream_data.add_sub(devel);
stream_data.add_sub(social); stream_data.add_sub(social);
create_devel_sidebar_row({mock_template}); create_devel_sidebar_row({mock_template});
create_social_sidebar_row({mock_template}); create_social_sidebar_row({mock_template});
create_stream_subheader({mock_template});
topic_list.get_stream_li = noop; topic_list.get_stream_li = noop;
const $pinned_subheader = $("<pinned-subheader-stub>");
const $active_subheader = $("<active-subheader-stub>");
const $devel_sidebar = $("<devel-sidebar-row-stub>"); const $devel_sidebar = $("<devel-sidebar-row-stub>");
const $social_sidebar = $("<social-sidebar-row-stub>"); const $social_sidebar = $("<social-sidebar-row-stub>");
let appended_elems;
$("#stream_filters").append = (elems) => {
appended_elems = elems;
};
let topics_closed; let topics_closed;
topic_list.close = () => { topic_list.close = () => {
topics_closed = true; topics_closed = true;
}; };
stream_list.build_stream_list(); stream_list.build_stream_list();
assert.ok(topics_closed); assert.ok(topics_closed);
const expected_elems = [ assert.deepEqual(appended_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
$pinned_subheader, // separator
$devel_sidebar, // pinned
$active_subheader, // separator
$social_sidebar, // not pinned
];
assert.deepEqual(appended_elems, expected_elems); assert.deepEqual(pinned_streams, [$devel_sidebar]);
assert.ok(pinned_subheader_flag); assert.deepEqual(normal_streams, [$social_sidebar]);
assert.ok(active_subheader_flag);
const $social_li = $("<social-sidebar-row-stub>"); const $social_li = $("<social-sidebar-row-stub>");
const stream_id = social.stream_id; const stream_id = social.stream_id;
@@ -267,8 +244,6 @@ test_ui("pinned_streams_never_inactive", ({mock_template}) => {
create_devel_sidebar_row({mock_template}); create_devel_sidebar_row({mock_template});
create_social_sidebar_row({mock_template}); create_social_sidebar_row({mock_template});
create_stream_subheader({mock_template});
// non-pinned streams can be made inactive // non-pinned streams can be made inactive
const $social_sidebar = $("<social-sidebar-row-stub>"); const $social_sidebar = $("<social-sidebar-row-stub>");
let stream_id = social.stream_id; let stream_id = social.stream_id;
@@ -397,15 +372,6 @@ function elem($obj) {
test_ui("zoom_in_and_zoom_out", ({mock_template}) => { test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
topic_list.setup_topic_search_typeahead = noop; topic_list.setup_topic_search_typeahead = noop;
const $splitter = $.create("<active-subheader-stub>");
$splitter.show();
assert.ok($splitter.visible());
$.create(".streams_subheader", {
children: [elem($splitter)],
});
const $stream_li1 = $.create("stream1 stub"); const $stream_li1 = $.create("stream1 stub");
const $stream_li2 = $.create("stream2 stub"); const $stream_li2 = $.create("stream2 stub");
@@ -439,7 +405,6 @@ test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
}); });
stream_list.zoom_in_topics({stream_id: 42}); stream_list.zoom_in_topics({stream_id: 42});
assert.ok(!$splitter.visible());
assert.ok(!$stream_li1.hasClass("hide")); assert.ok(!$stream_li1.hasClass("hide"));
assert.ok($stream_li2.hasClass("hide")); assert.ok($stream_li2.hasClass("hide"));
assert.ok($("#streams_list").hasClass("zoom-in")); assert.ok($("#streams_list").hasClass("zoom-in"));
@@ -456,22 +421,20 @@ test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
}; };
stream_list.zoom_out_topics({$stream_li: $stream_li1}); stream_list.zoom_out_topics({$stream_li: $stream_li1});
assert.ok($splitter.visible());
assert.ok(!$stream_li1.hasClass("hide")); assert.ok(!$stream_li1.hasClass("hide"));
assert.ok(!$stream_li2.hasClass("hide")); assert.ok(!$stream_li2.hasClass("hide"));
assert.ok($("#streams_list").hasClass("zoom-out")); assert.ok($("#streams_list").hasClass("zoom-out"));
assert.ok(!filter_topics_appended); assert.ok(!filter_topics_appended);
}); });
test_ui("narrowing", ({mock_template}) => { test_ui("narrowing", ({override_rewire}) => {
create_stream_subheader({mock_template});
initialize_stream_data(); initialize_stream_data();
topic_list.close = noop; topic_list.close = noop;
topic_list.rebuild_left_sidebar = noop; topic_list.rebuild_left_sidebar = noop;
topic_list.active_stream_id = noop; topic_list.active_stream_id = noop;
topic_list.get_stream_li = noop; topic_list.get_stream_li = noop;
$("#streams_header").outerHeight = () => 0; override_rewire(stream_list, "scroll_stream_into_view", noop);
assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter")); assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter"));
@@ -529,44 +492,45 @@ test_ui("focus_user_filter", () => {
click_handler(e); click_handler(e);
}); });
test_ui("sort_streams", ({mock_template}) => { test_ui("sort_streams", ({override_rewire}) => {
create_stream_subheader({mock_template});
// Set subheader flag to false
pinned_subheader_flag = false;
active_subheader_flag = false;
inactive_subheader_flag = false;
// Get coverage on early-exit. // Get coverage on early-exit.
stream_list.build_stream_list(); stream_list.build_stream_list();
initialize_stream_data(); initialize_stream_data();
let appended_elems; const appended_sections = [];
$("#stream_filters").append = (elems) => { override_rewire(stream_list, "stream_list_section_container_html", (section) => {
appended_elems = elems; appended_sections.push(section.id);
return `<stub-section-${section.id}>`;
});
const pinned_streams = [];
$("#stream-list-pinned-streams").append = (stream) => {
pinned_streams.push(stream);
};
const normal_streams = [];
$("#stream-list-normal-streams").append = (stream) => {
normal_streams.push(stream);
};
const inactive_streams = [];
$("#stream-list-dormant-streams").append = (stream) => {
inactive_streams.push(stream);
}; };
stream_list.build_stream_list(true); stream_list.build_stream_list(true);
const $pinned_subheader = $("<pinned-subheader-stub>"); assert.deepEqual(appended_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
const $active_subheader = $("<active-subheader-stub>");
const $inactive_subheader = $("<inactive-subheader-stub>"); assert.deepEqual(pinned_streams, [
const expected_elems = [
$pinned_subheader,
$("<devel-sidebar-row-stub>"), $("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"), $("<Rome-sidebar-row-stub>"),
$("<test-sidebar-row-stub>"), $("<test-sidebar-row-stub>"),
$active_subheader, ]);
assert.deepEqual(normal_streams, [
$("<announce-sidebar-row-stub>"), $("<announce-sidebar-row-stub>"),
$("<Denmark-sidebar-row-stub>"), $("<Denmark-sidebar-row-stub>"),
$inactive_subheader, ]);
$("<cars-sidebar-row-stub>"), assert.deepEqual(inactive_streams, [$("<cars-sidebar-row-stub>")]);
];
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
assert.ok(active_subheader_flag);
assert.ok(inactive_subheader_flag);
const streams = stream_list_sort.get_stream_ids(); const streams = stream_list_sort.get_stream_ids();
@@ -589,13 +553,7 @@ test_ui("sort_streams", ({mock_template}) => {
assert.ok(!stream_list.stream_sidebar.has_row_for(stream_id)); assert.ok(!stream_list.stream_sidebar.has_row_for(stream_id));
}); });
test_ui("separators_only_pinned_and_dormant", ({mock_template}) => { test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
// Test only pinned and dormant streams
create_stream_subheader({mock_template});
pinned_subheader_flag = false;
inactive_subheader_flag = false;
// Get coverage on early-exit. // Get coverage on early-exit.
stream_list.build_stream_list(); stream_list.build_stream_list();
@@ -630,73 +588,31 @@ test_ui("separators_only_pinned_and_dormant", ({mock_template}) => {
}; };
add_row(DenmarkSub); add_row(DenmarkSub);
let appended_elems; const appended_sections = [];
$("#stream_filters").append = (elems) => { override_rewire(stream_list, "stream_list_section_container_html", (section) => {
appended_elems = elems; appended_sections.push(section.id);
return `<stub-section-${section.id}>`;
});
const pinned_streams = [];
$("#stream-list-pinned-streams").append = (stream) => {
pinned_streams.push(stream);
};
const inactive_streams = [];
$("#stream-list-dormant-streams").append = (stream) => {
inactive_streams.push(stream);
}; };
stream_list.build_stream_list(); stream_list.build_stream_list();
const $pinned_subheader = $("<pinned-subheader-stub>"); assert.deepEqual(appended_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
const $inactive_subheader = $("<inactive-subheader-stub>");
const expected_elems = [
$pinned_subheader, // pinned
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
$inactive_subheader, // dormant
$("<Denmark-sidebar-row-stub>"),
];
assert.deepEqual(appended_elems, expected_elems); assert.deepEqual(pinned_streams, [$("<devel-sidebar-row-stub>"), $("<Rome-sidebar-row-stub>")]);
assert.ok(pinned_subheader_flag); assert.deepEqual(inactive_streams, [$("<Denmark-sidebar-row-stub>")]);
assert.ok(inactive_subheader_flag);
});
test_ui("separators_only_pinned", () => {
// Test only pinned streams
// Get coverage on early-exit.
stream_list.build_stream_list();
// pinned streams
const develSub = {
name: "devel",
stream_id: 1000,
color: "blue",
pin_to_top: true,
subscribed: true,
};
add_row(develSub);
const RomeSub = {
name: "Rome",
stream_id: 2000,
color: "blue",
pin_to_top: true,
subscribed: true,
};
add_row(RomeSub);
let appended_elems;
$("#stream_filters").append = (elems) => {
appended_elems = elems;
};
stream_list.build_stream_list();
const expected_elems = [
// no section sub-header since there is only one section
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
// no separator at the end as no stream follows
];
assert.deepEqual(appended_elems, expected_elems);
}); });
test_ui("rename_stream", ({mock_template, override}) => { test_ui("rename_stream", ({mock_template, override}) => {
override(user_settings, "web_stream_unreads_count_display_policy", 3); override(user_settings, "web_stream_unreads_count_display_policy", 3);
override(current_user, "user_id", me.user_id); override(current_user, "user_id", me.user_id);
create_stream_subheader({mock_template});
initialize_stream_data(); initialize_stream_data();
const sub = stream_data.get_sub_by_name("devel"); const sub = stream_data.get_sub_by_name("devel");
@@ -737,7 +653,7 @@ test_ui("rename_stream", ({mock_template, override}) => {
develSub.name = "devel"; // Resets develSub.name = "devel"; // Resets
}); });
test_ui("refresh_pin", ({override, override_rewire, mock_template}) => { test_ui("refresh_pin", ({override_rewire, mock_template}) => {
initialize_stream_data(); initialize_stream_data();
const sub = { const sub = {
@@ -762,10 +678,9 @@ test_ui("refresh_pin", ({override, override_rewire, mock_template}) => {
override_rewire(stream_list, "update_count_in_dom", noop); override_rewire(stream_list, "update_count_in_dom", noop);
$("#stream_filters").append = noop; $("#stream_filters").append = noop;
$("#streams_header").outerHeight = () => 0;
let scrolled; let scrolled;
override(scroll_util, "scroll_element_into_container", ($li) => { override_rewire(stream_list, "scroll_stream_into_view", ($li) => {
if ($li === $li_stub) { if ($li === $li_stub) {
scrolled = true; scrolled = true;
} }

View File

@@ -100,11 +100,26 @@ function test(label, f) {
test("no_subscribed_streams", () => { test("no_subscribed_streams", () => {
const sorted = sort_groups(""); const sorted = sort_groups("");
assert.deepEqual(sorted, { assert.deepEqual(sorted, {
dormant_streams: [], sections: [
muted_active_streams: [], {
muted_pinned_streams: [], id: "pinned-streams",
normal_streams: [], muted_streams: [],
pinned_streams: [], section_title: "translated: PINNED CHANNELS",
streams: [],
},
{
id: "normal-streams",
muted_streams: [],
section_title: "translated: ACTIVE CHANNELS",
streams: [],
},
{
id: "dormant-streams",
muted_streams: [],
section_title: "translated: INACTIVE CHANNELS",
streams: [],
},
],
same_as_before: sorted.same_as_before, same_as_before: sorted.same_as_before,
}); });
assert.equal(stream_list_sort.first_stream_id(), undefined); assert.equal(stream_list_sort.first_stream_id(), undefined);
@@ -122,16 +137,22 @@ test("basics", () => {
stream_data.add_sub(archived); stream_data.add_sub(archived);
// Test sorting into categories/alphabetized // Test sorting into categories/alphabetized
let sorted = sort_groups(""); let sorted_sections = sort_groups("").sections;
assert.deepEqual(sorted.pinned_streams, [scalene.stream_id]); const pinned = sorted_sections[0];
assert.deepEqual(sorted.normal_streams, [ assert.deepEqual(pinned.id, "pinned-streams");
assert.deepEqual(pinned.streams, [scalene.stream_id]);
assert.deepEqual(pinned.muted_streams, [muted_pinned.stream_id]);
const normal = sorted_sections[1];
assert.deepEqual(normal.id, "normal-streams");
assert.deepEqual(normal.streams, [
clarinet.stream_id, clarinet.stream_id,
fast_tortoise.stream_id, fast_tortoise.stream_id,
stream_hyphen_underscore_slash_colon.stream_id, stream_hyphen_underscore_slash_colon.stream_id,
]); ]);
assert.deepEqual(sorted.muted_pinned_streams, [muted_pinned.stream_id]); assert.deepEqual(normal.muted_streams, [muted_active.stream_id]);
assert.deepEqual(sorted.muted_active_streams, [muted_active.stream_id]); const dormant = sorted_sections[2];
assert.deepEqual(sorted.dormant_streams, [pneumonia.stream_id]); assert.deepEqual(dormant.id, "dormant-streams");
assert.deepEqual(dormant.streams, [pneumonia.stream_id]);
// Test cursor helpers. // Test cursor helpers.
assert.equal(stream_list_sort.first_stream_id(), scalene.stream_id); assert.equal(stream_list_sort.first_stream_id(), scalene.stream_id);
@@ -156,53 +177,70 @@ test("basics", () => {
assert.equal(stream_list_sort.next_stream_id(pneumonia.stream_id), undefined); assert.equal(stream_list_sort.next_stream_id(pneumonia.stream_id), undefined);
// Test filtering // Test filtering
sorted = sort_groups("s"); sorted_sections = sort_groups("s").sections;
assert.deepEqual(sorted.pinned_streams, [scalene.stream_id]); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]); assert.deepEqual(sorted_sections[0].id, "pinned-streams");
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[0].streams, [scalene.stream_id]);
assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
assert.equal(stream_list_sort.prev_stream_id(clarinet.stream_id), undefined); assert.equal(stream_list_sort.prev_stream_id(clarinet.stream_id), undefined);
assert.equal(stream_list_sort.next_stream_id(clarinet.stream_id), undefined); assert.equal(stream_list_sort.next_stream_id(clarinet.stream_id), undefined);
// Test searching entire word, case-insensitive // Test searching entire word, case-insensitive
sorted = sort_groups("PnEuMoNiA"); sorted_sections = sort_groups("PnEuMoNiA").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, []); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, [pneumonia.stream_id]); assert.deepEqual(sorted_sections[1].streams, []);
assert.deepEqual(sorted_sections[2].id, "dormant-streams");
assert.deepEqual(sorted_sections[2].streams, [pneumonia.stream_id]);
// Test searching part of word // Test searching part of word
sorted = sort_groups("tortoise"); sorted_sections = sort_groups("tortoise").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [fast_tortoise.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [fast_tortoise.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
// Test searching stream with spaces // Test searching stream with spaces
sorted = sort_groups("fast t"); sorted_sections = sort_groups("fast t").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [fast_tortoise.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [fast_tortoise.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
// Test searching part of stream name with non space word separators // Test searching part of stream name with non space word separators
sorted = sort_groups("hyphen"); sorted_sections = sort_groups("hyphen").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
sorted = sort_groups("hyphen_underscore"); sorted_sections = sort_groups("hyphen_underscore").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
sorted = sort_groups("colon"); sorted_sections = sort_groups("colon").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
sorted = sort_groups("underscore"); sorted_sections = sort_groups("underscore").sections;
assert.deepEqual(sorted.pinned_streams, []); assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]); assert.deepEqual(sorted_sections[0].streams, []);
assert.deepEqual(sorted.dormant_streams, []); assert.deepEqual(sorted_sections[1].id, "normal-streams");
assert.deepEqual(sorted_sections[1].streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted_sections[2].streams, []);
}); });
test("filter inactives", ({override}) => { test("filter inactives", ({override}) => {