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 {
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");
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);
}
}

View File

@@ -6,9 +6,9 @@ import * as tippy from "tippy.js";
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_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_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 * 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 type {Filter} from "./filter.ts";
import * as hash_util from "./hash_util.ts";
import {$t} from "./i18n.ts";
import * as keydown_util from "./keydown_util.ts";
import {ListCursor} from "./list_cursor.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 stream_data from "./stream_data.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_util from "./stream_topic_history_util.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;
const collapsed_sections = new Set<string>();
export function is_zoomed_in(): boolean {
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 {
// The stream list in the left sidebar contains 3 sections:
// pinned, normal, and dormant streams, with headings above them
@@ -277,88 +292,53 @@ export function build_stream_list(force_rerender: boolean): void {
return;
}
const $parent = $("#stream_filters");
const elems = [];
function add_sidebar_li(stream_id: number): void {
function add_sidebar_li(stream_id: number, $list: JQuery): void {
const sidebar_row = stream_sidebar.get_row(stream_id);
assert(sidebar_row !== undefined);
sidebar_row.update_whether_active();
elems.push(sidebar_row.get_li());
$list.append($(sidebar_row.get_li()));
}
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 =
stream_groups.pinned_streams.length > 0 || stream_groups.muted_pinned_streams.length > 0;
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 [...section.streams, ...section.muted_streams]) {
add_sidebar_li(stream_id, $(`#stream-list-${section.id}`));
}
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) {
add_sidebar_li(stream_id);
function toggle_section_collapse($container: JQuery): void {
$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) {
elems.push(
$(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Active",
}),
}),
),
);
function collapse_collapsed_sections(): void {
for (const section_id of collapsed_sections) {
const $container = $(`#stream-list-${section_id}-container`);
$container.toggleClass("collapsed", true);
$container
.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 {
@@ -416,11 +396,6 @@ export function zoom_in_topics(options: {stream_id: number | undefined}): void {
$("#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 () {
const $elt = $(this);
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 {
// Show pinned stream splitter
$(".streams_subheader").each(function () {
$(this).show();
});
$("#streams_list").expectOne().removeClass("zoom-in").addClass("zoom-out");
$("#stream_filters li.narrow-filter").toggleClass("hide", false);
// Remove search box for topics list from DOM.
@@ -1113,6 +1083,15 @@ export function set_event_handlers({
stream_cursor.clear();
});
$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 {
@@ -1147,15 +1126,29 @@ export function clear_search(): void {
$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");
if ($stream_li.length !== 1) {
blueslip.error("Invalid stream_li was passed in");
return;
}
const stream_header_height = $("#streams_header").outerHeight();
scroll_util.scroll_element_into_container($stream_li, $container, stream_header_height);
const stream_filter_height = $(".stream_search_section").outerHeight()!;
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 {

View File

@@ -1,5 +1,6 @@
import assert from "minimalistic-assert";
import {$t} from "./i18n.ts";
import * as settings_config from "./settings_config.ts";
import * as stream_data from "./stream_data.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";
let first_render_completed = false;
let previous_pinned: number[] = [];
let previous_normal: number[] = [];
let previous_dormant: number[] = [];
let previous_muted_active: number[] = [];
let previous_muted_pinned: number[] = [];
let current_sections: StreamListSection[] = [];
let all_streams: number[] = [];
// 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;
}
export type StreamListSection = {
id: string;
section_title: string;
streams: number[];
muted_streams: number[]; // Not used for the inactive section
};
type StreamListSortResult = {
same_as_before: boolean;
pinned_streams: number[];
normal_streams: number[];
dormant_streams: number[];
muted_pinned_streams: number[];
muted_active_streams: number[];
sections: StreamListSection[];
};
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);
}
const pinned_streams = [];
const normal_streams = [];
const muted_pinned_streams = [];
const muted_active_streams = [];
const dormant_streams = [];
const pinned_section: StreamListSection = {
id: "pinned-streams",
section_title: $t({defaultMessage: "PINNED CHANNELS"}),
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) {
const sub = sub_store.get(stream_id);
assert(sub);
const pinned = sub.pin_to_top;
if (sub.is_archived) {
continue;
}
if (pinned) {
if (!sub.is_muted) {
pinned_streams.push(stream_id);
if (sub.pin_to_top) {
if (sub.is_muted) {
pinned_section.muted_streams.push(stream_id);
} else {
muted_pinned_streams.push(stream_id);
pinned_section.streams.push(stream_id);
}
} else if (is_normal(sub)) {
if (!sub.is_muted) {
normal_streams.push(stream_id);
if (sub.is_muted) {
normal_section.muted_streams.push(stream_id);
} else {
muted_active_streams.push(stream_id);
normal_section.streams.push(stream_id);
}
} else {
dormant_streams.push(stream_id);
dormant_section.streams.push(stream_id);
}
}
pinned_streams.sort(compare_function);
normal_streams.sort(compare_function);
muted_pinned_streams.sort(compare_function);
muted_active_streams.sort(compare_function);
dormant_streams.sort(compare_function);
// This needs to have the same ordering as the order they're displayed in the sidebar.
const new_sections = [pinned_section, normal_section, dormant_section];
for (const section of new_sections) {
section.streams.sort(compare_function);
section.muted_streams.sort(compare_function);
}
const same_as_before =
first_render_completed &&
util.array_compare(previous_pinned, pinned_streams) &&
util.array_compare(previous_normal, normal_streams) &&
util.array_compare(previous_muted_pinned, muted_pinned_streams) &&
util.array_compare(previous_muted_active, muted_active_streams) &&
util.array_compare(previous_dormant, dormant_streams);
new_sections.entries().every(([i, new_section]) => {
const current_section = current_sections.at(i);
return (
current_section !== undefined &&
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) {
first_render_completed = true;
previous_pinned = pinned_streams;
previous_normal = normal_streams;
previous_muted_pinned = muted_pinned_streams;
previous_muted_active = muted_active_streams;
previous_dormant = dormant_streams;
all_streams = [
...pinned_streams,
...muted_pinned_streams,
...normal_streams,
...muted_active_streams,
...dormant_streams,
];
current_sections = new_sections;
all_streams = new_sections.flatMap((section) => [
...section.streams,
...section.muted_streams,
]);
}
return {
same_as_before,
pinned_streams,
normal_streams,
dormant_streams,
muted_pinned_streams,
muted_active_streams,
sections: new_sections,
};
}

View File

@@ -81,7 +81,7 @@
}
}
.selected-home-view,
#left-sidebar-navigation-list .selected-home-view,
#streams_header {
&.hide-unread-messages-count {
.masked_unread_count {
@@ -101,7 +101,7 @@
}
}
.selected-home-view:hover,
#left-sidebar-navigation-list .selected-home-view:hover,
#streams_header:hover,
.selected-home-view.top-left-active-filter {
&.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 {
/* Properly offset all the grid rows
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
that the background color appears above the grouping
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;
}
@@ -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 {
visibility: hidden;
}
@@ -1929,13 +1951,14 @@ li.topic-list-item {
.narrow-filter > .bottom_left_row {
position: sticky;
/* We subtract a quarter pixel of space to correct
for possible bleedthrough under certain viewing
conditions (e.g., external monitors.) This same
technique is used on #streams_header. */
/* We need to hold the space where the BACK TO CHANNELS
line sits, so the channel info doesn't run over the
top of it when scrolling down. These are the same
variables for setting the space on the BACK TO CHANNELS
grid row plus its top padding: */
top: calc(
var(--left-sidebar-sections-vertical-gutter) +
var(--line-height-sidebar-row-prominent) - 0.25px
var(--line-height-sidebar-row-prominent) +
var(--left-sidebar-sections-vertical-gutter)
);
z-index: 2;
padding-bottom: 1px;

View File

@@ -195,7 +195,8 @@
</div>
<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>
<div class="left-sidebar-controls">
<span id="add_streams_tooltip" class="hidden-for-spectators">
@@ -208,12 +209,10 @@
<i class="zulip-icon zulip-icon-masked-unread"></i>
</span>
</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 type="text" class="input-element stream-list-filter home-page-input" autocomplete="off" placeholder="{{t 'Filter channels' }}" />
{{/input_wrapper}}
</div>
</div>
<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>
</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;
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() {},
get_scroll_element: ($element) => $element,
});
@@ -101,11 +101,6 @@ const social = {
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}) {
const $devel_count = $.create("devel-count");
const $subscription_block = $.create("devel-block");
@@ -152,22 +147,6 @@ function create_social_sidebar_row({mock_template}) {
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) {
run_test(label, (helpers) => {
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
// generate the right markup as well as play nice with get_stream_li().
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(social);
create_devel_sidebar_row({mock_template});
create_social_sidebar_row({mock_template});
create_stream_subheader({mock_template});
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 $social_sidebar = $("<social-sidebar-row-stub>");
let appended_elems;
$("#stream_filters").append = (elems) => {
appended_elems = elems;
};
let topics_closed;
topic_list.close = () => {
topics_closed = true;
};
stream_list.build_stream_list();
assert.ok(topics_closed);
const expected_elems = [
$pinned_subheader, // separator
$devel_sidebar, // pinned
$active_subheader, // separator
$social_sidebar, // not pinned
];
assert.deepEqual(appended_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
assert.ok(active_subheader_flag);
assert.deepEqual(pinned_streams, [$devel_sidebar]);
assert.deepEqual(normal_streams, [$social_sidebar]);
const $social_li = $("<social-sidebar-row-stub>");
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_social_sidebar_row({mock_template});
create_stream_subheader({mock_template});
// non-pinned streams can be made inactive
const $social_sidebar = $("<social-sidebar-row-stub>");
let stream_id = social.stream_id;
@@ -397,15 +372,6 @@ function elem($obj) {
test_ui("zoom_in_and_zoom_out", ({mock_template}) => {
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_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});
assert.ok(!$splitter.visible());
assert.ok(!$stream_li1.hasClass("hide"));
assert.ok($stream_li2.hasClass("hide"));
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});
assert.ok($splitter.visible());
assert.ok(!$stream_li1.hasClass("hide"));
assert.ok(!$stream_li2.hasClass("hide"));
assert.ok($("#streams_list").hasClass("zoom-out"));
assert.ok(!filter_topics_appended);
});
test_ui("narrowing", ({mock_template}) => {
create_stream_subheader({mock_template});
test_ui("narrowing", ({override_rewire}) => {
initialize_stream_data();
topic_list.close = noop;
topic_list.rebuild_left_sidebar = noop;
topic_list.active_stream_id = 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"));
@@ -529,44 +492,45 @@ test_ui("focus_user_filter", () => {
click_handler(e);
});
test_ui("sort_streams", ({mock_template}) => {
create_stream_subheader({mock_template});
// Set subheader flag to false
pinned_subheader_flag = false;
active_subheader_flag = false;
inactive_subheader_flag = false;
test_ui("sort_streams", ({override_rewire}) => {
// Get coverage on early-exit.
stream_list.build_stream_list();
initialize_stream_data();
let appended_elems;
$("#stream_filters").append = (elems) => {
appended_elems = elems;
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);
};
const inactive_streams = [];
$("#stream-list-dormant-streams").append = (stream) => {
inactive_streams.push(stream);
};
stream_list.build_stream_list(true);
const $pinned_subheader = $("<pinned-subheader-stub>");
const $active_subheader = $("<active-subheader-stub>");
const $inactive_subheader = $("<inactive-subheader-stub>");
const expected_elems = [
$pinned_subheader,
assert.deepEqual(appended_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
assert.deepEqual(pinned_streams, [
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
$("<test-sidebar-row-stub>"),
$active_subheader,
]);
assert.deepEqual(normal_streams, [
$("<announce-sidebar-row-stub>"),
$("<Denmark-sidebar-row-stub>"),
$inactive_subheader,
$("<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);
]);
assert.deepEqual(inactive_streams, [$("<cars-sidebar-row-stub>")]);
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));
});
test_ui("separators_only_pinned_and_dormant", ({mock_template}) => {
// Test only pinned and dormant streams
create_stream_subheader({mock_template});
pinned_subheader_flag = false;
inactive_subheader_flag = false;
test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
// Get coverage on early-exit.
stream_list.build_stream_list();
@@ -630,73 +588,31 @@ test_ui("separators_only_pinned_and_dormant", ({mock_template}) => {
};
add_row(DenmarkSub);
let appended_elems;
$("#stream_filters").append = (elems) => {
appended_elems = elems;
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 inactive_streams = [];
$("#stream-list-dormant-streams").append = (stream) => {
inactive_streams.push(stream);
};
stream_list.build_stream_list();
const $pinned_subheader = $("<pinned-subheader-stub>");
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_sections, ["pinned-streams", "normal-streams", "dormant-streams"]);
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
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);
assert.deepEqual(pinned_streams, [$("<devel-sidebar-row-stub>"), $("<Rome-sidebar-row-stub>")]);
assert.deepEqual(inactive_streams, [$("<Denmark-sidebar-row-stub>")]);
});
test_ui("rename_stream", ({mock_template, override}) => {
override(user_settings, "web_stream_unreads_count_display_policy", 3);
override(current_user, "user_id", me.user_id);
create_stream_subheader({mock_template});
initialize_stream_data();
const sub = stream_data.get_sub_by_name("devel");
@@ -737,7 +653,7 @@ test_ui("rename_stream", ({mock_template, override}) => {
develSub.name = "devel"; // Resets
});
test_ui("refresh_pin", ({override, override_rewire, mock_template}) => {
test_ui("refresh_pin", ({override_rewire, mock_template}) => {
initialize_stream_data();
const sub = {
@@ -762,10 +678,9 @@ test_ui("refresh_pin", ({override, override_rewire, mock_template}) => {
override_rewire(stream_list, "update_count_in_dom", noop);
$("#stream_filters").append = noop;
$("#streams_header").outerHeight = () => 0;
let scrolled;
override(scroll_util, "scroll_element_into_container", ($li) => {
override_rewire(stream_list, "scroll_stream_into_view", ($li) => {
if ($li === $li_stub) {
scrolled = true;
}

View File

@@ -100,11 +100,26 @@ function test(label, f) {
test("no_subscribed_streams", () => {
const sorted = sort_groups("");
assert.deepEqual(sorted, {
dormant_streams: [],
muted_active_streams: [],
muted_pinned_streams: [],
normal_streams: [],
pinned_streams: [],
sections: [
{
id: "pinned-streams",
muted_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,
});
assert.equal(stream_list_sort.first_stream_id(), undefined);
@@ -122,16 +137,22 @@ test("basics", () => {
stream_data.add_sub(archived);
// Test sorting into categories/alphabetized
let sorted = sort_groups("");
assert.deepEqual(sorted.pinned_streams, [scalene.stream_id]);
assert.deepEqual(sorted.normal_streams, [
let sorted_sections = sort_groups("").sections;
const pinned = sorted_sections[0];
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,
fast_tortoise.stream_id,
stream_hyphen_underscore_slash_colon.stream_id,
]);
assert.deepEqual(sorted.muted_pinned_streams, [muted_pinned.stream_id]);
assert.deepEqual(sorted.muted_active_streams, [muted_active.stream_id]);
assert.deepEqual(sorted.dormant_streams, [pneumonia.stream_id]);
assert.deepEqual(normal.muted_streams, [muted_active.stream_id]);
const dormant = sorted_sections[2];
assert.deepEqual(dormant.id, "dormant-streams");
assert.deepEqual(dormant.streams, [pneumonia.stream_id]);
// Test cursor helpers.
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);
// Test filtering
sorted = sort_groups("s");
assert.deepEqual(sorted.pinned_streams, [scalene.stream_id]);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("s").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].id, "pinned-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.next_stream_id(clarinet.stream_id), undefined);
// Test searching entire word, case-insensitive
sorted = sort_groups("PnEuMoNiA");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, []);
assert.deepEqual(sorted.dormant_streams, [pneumonia.stream_id]);
sorted_sections = sort_groups("PnEuMoNiA").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].streams, []);
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
sorted = sort_groups("tortoise");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [fast_tortoise.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("tortoise").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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
sorted = sort_groups("fast t");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [fast_tortoise.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("fast t").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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
sorted = sort_groups("hyphen");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("hyphen").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("hyphen_underscore").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("colon").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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");
assert.deepEqual(sorted.pinned_streams, []);
assert.deepEqual(sorted.normal_streams, [stream_hyphen_underscore_slash_colon.stream_id]);
assert.deepEqual(sorted.dormant_streams, []);
sorted_sections = sort_groups("underscore").sections;
assert.deepEqual(sorted_sections.length, 3);
assert.deepEqual(sorted_sections[0].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}) => {