stream_list: Sort muted to end of sections and add labels.

The stream list left sidebar currently has 3 sections:
* Pinned (+ Muted pinned streams)
* Active (+ Muted active streams)
* Inactive streams

Previously, these sections were separated by horizontal lines, which
did not provide an easy way to discern why there were sections. We add
labels to these section dividers to help with this.

Additionally, within each section, we now sort all muted streams to
the bottom, so that they general minimal clutter.

Fixes #19812.
This commit is contained in:
madrix01 2022-02-21 02:02:14 +05:30 committed by Tim Abbott
parent 0a278c39d2
commit c1b5021d84
11 changed files with 244 additions and 41 deletions

View File

@ -318,6 +318,12 @@ test("is_active", () => {
assert.ok(stream_data.is_active(sub));
});
test("is_muted_active", () => {
const sub = {name: "cats", subscribed: true, stream_id: 111, is_muted: true};
stream_data.add_sub(sub);
assert.ok(stream_data.is_muted_active(sub));
});
test("admin_options", () => {
function make_sub() {
const sub = {

View File

@ -124,6 +124,7 @@ test("update_property", ({override}) => {
{
const stub = make_stub();
override(stream_muting, "update_is_muted", stub.f);
override(stream_list, "refresh_muted_or_unmuted_stream", noop);
stream_events.update_property(stream_id, "in_home_view", false);
assert.equal(stub.num_calls, 1);
const args = stub.get_args("sub", "val");

View File

@ -12,10 +12,11 @@ set_global("document", "document-stub");
page_params.is_admin = false;
page_params.realm_users = [];
const noop = () => {};
// We use this with override.
let num_unread_for_stream;
const noop = () => {};
const narrow_state = mock_esm("../../static/js/narrow_state");
const topic_list = mock_esm("../../static/js/topic_list");
mock_esm("../../static/js/keydown_util", {
@ -49,6 +50,11 @@ const social = {
subscribed: true,
};
// 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");
@ -87,6 +93,22 @@ function create_social_sidebar_row({mock_template}) {
assert.equal($social_count.text(), "99");
}
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();
@ -105,8 +127,10 @@ test_ui("create_sidebar_row", ({override_rewire, mock_template}) => {
create_devel_sidebar_row({mock_template});
create_social_sidebar_row({mock_template});
create_stream_subheader({mock_template});
const split = '<hr class="stream-split">';
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>");
@ -123,14 +147,16 @@ test_ui("create_sidebar_row", ({override_rewire, mock_template}) => {
stream_list.build_stream_list();
assert.ok(topic_list_cleared);
const expected_elems = [
$pinned_subheader.html(), // separator
$devel_sidebar, // pinned
split, // separator
$active_subheader.html(), // separator
$social_sidebar, // not pinned
];
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
assert.ok(active_subheader_flag);
const $social_li = $("<social-sidebar-row-stub>");
const stream_id = social.stream_id;
@ -181,6 +207,7 @@ test_ui("pinned_streams_never_inactive", ({override_rewire, 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>");
@ -306,12 +333,12 @@ test_ui("zoom_in_and_zoom_out", () => {
children: [elem($label1), elem($label2)],
});
const $splitter = $.create("hr stub");
const $splitter = $.create("<active-subheader-stub>");
$splitter.show();
assert.ok($splitter.visible());
$.create(".stream-split", {
$.create(".streams_subheader", {
children: [elem($splitter)],
});
@ -363,7 +390,8 @@ test_ui("zoom_in_and_zoom_out", () => {
assert.ok($("#streams_list").hasClass("zoom-out"));
});
test_ui("narrowing", () => {
test_ui("narrowing", ({mock_template}) => {
create_stream_subheader({mock_template});
initialize_stream_data();
topic_list.close = noop;
@ -429,7 +457,13 @@ test_ui("focus_user_filter", () => {
click_handler(e);
});
test_ui("sort_streams", ({override_rewire}) => {
test_ui("sort_streams", ({override_rewire, mock_template}) => {
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.
stream_list.build_stream_list();
@ -444,19 +478,25 @@ test_ui("sort_streams", ({override_rewire}) => {
stream_list.build_stream_list();
const split = '<hr class="stream-split">';
const $pinned_subheader = $("<pinned-subheader-stub>");
const $active_subheader = $("<active-subheader-stub>");
const $inactive_subheader = $("<inactive-subheader-stub>");
const expected_elems = [
$pinned_subheader.html(),
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
$("<test-sidebar-row-stub>"),
split,
$active_subheader.html(),
$("<announce-sidebar-row-stub>"),
$("<Denmark-sidebar-row-stub>"),
split,
$inactive_subheader.html(),
$("<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_sort.get_streams();
@ -479,9 +519,13 @@ test_ui("sort_streams", ({override_rewire}) => {
assert.ok(!stream_list.stream_sidebar.has_row_for(stream_id));
});
test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
test_ui("separators_only_pinned_and_dormant", ({override_rewire, mock_template}) => {
// Test only pinned and dormant streams
create_stream_subheader({mock_template});
pinned_subheader_flag = false;
inactive_subheader_flag = false;
// Get coverage on early-exit.
stream_list.build_stream_list();
@ -522,22 +566,27 @@ test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
stream_list.build_stream_list();
const split = '<hr class="stream-split">';
const $pinned_subheader = $("<pinned-subheader-stub>");
const $inactive_subheader = $("<inactive-subheader-stub>");
const expected_elems = [
// pinned
$pinned_subheader.html(), // pinned
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
split,
// dormant
$inactive_subheader.html(), // dormant
$("<Denmark-sidebar-row-stub>"),
];
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
assert.ok(inactive_subheader_flag);
});
test_ui("separators_only_pinned", () => {
test_ui("separators_only_pinned", ({mock_template}) => {
// Test only pinned streams
create_stream_subheader({mock_template});
pinned_subheader_flag = false;
// Get coverage on early-exit.
stream_list.build_stream_list();
@ -566,20 +615,22 @@ test_ui("separators_only_pinned", () => {
};
stream_list.build_stream_list();
const $pinned_subheader = $("<pinned-subheader-stub>");
const expected_elems = [
// pinned
$pinned_subheader.html(), // pinned
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
// no separator at the end as no stream follows
];
assert.deepEqual(appended_elems, expected_elems);
assert.ok(pinned_subheader_flag);
});
narrow_state.active = () => false;
test_ui("rename_stream", ({mock_template}) => {
create_stream_subheader({mock_template});
initialize_stream_data();
const sub = stream_data.get_sub_by_name("devel");

View File

@ -44,6 +44,20 @@ const stream_hyphen_underscore_slash = {
stream_id: 6,
pin_to_top: false,
};
const muted_active = {
subscribed: true,
name: "muted active",
stream_id: 7,
pin_to_top: false,
is_muted: true,
};
const muted_pinned = {
subscribed: true,
name: "muted pinned",
stream_id: 8,
pin_to_top: true,
is_muted: true,
};
function sort_groups(query) {
const streams = stream_data.subscribed_stream_ids();
@ -61,6 +75,8 @@ test("no_subscribed_streams", () => {
const sorted = sort_groups("");
assert.deepEqual(sorted, {
dormant_streams: [],
muted_active_streams: [],
muted_pinned_streams: [],
normal_streams: [],
pinned_streams: [],
same_as_before: sorted.same_as_before,
@ -75,6 +91,8 @@ test("basics", ({override_rewire}) => {
stream_data.add_sub(clarinet);
stream_data.add_sub(weaving);
stream_data.add_sub(stream_hyphen_underscore_slash);
stream_data.add_sub(muted_active);
stream_data.add_sub(muted_pinned);
override_rewire(stream_data, "is_active", (sub) => sub.name !== "pneumonia");
@ -86,13 +104,16 @@ test("basics", ({override_rewire}) => {
fast_tortoise.stream_id,
stream_hyphen_underscore_slash.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]);
// Test cursor helpers.
assert.equal(stream_sort.first_stream_id(), scalene.stream_id);
assert.equal(stream_sort.prev_stream_id(scalene.stream_id), undefined);
assert.equal(stream_sort.prev_stream_id(clarinet.stream_id), scalene.stream_id);
assert.equal(stream_sort.prev_stream_id(muted_pinned.stream_id), scalene.stream_id);
assert.equal(stream_sort.prev_stream_id(clarinet.stream_id), muted_pinned.stream_id);
assert.equal(
stream_sort.next_stream_id(fast_tortoise.stream_id),
@ -100,8 +121,13 @@ test("basics", ({override_rewire}) => {
);
assert.equal(
stream_sort.next_stream_id(stream_hyphen_underscore_slash.stream_id),
pneumonia.stream_id,
muted_active.stream_id,
);
assert.equal(
stream_sort.next_stream_id(fast_tortoise.stream_id),
stream_hyphen_underscore_slash.stream_id,
);
assert.equal(stream_sort.next_stream_id(muted_active.stream_id), pneumonia.stream_id);
assert.equal(stream_sort.next_stream_id(pneumonia.stream_id), undefined);
// Test filtering

View File

@ -204,6 +204,10 @@ export function is_active(sub) {
return stream_topic_history.stream_has_topics(sub.stream_id) || sub.newly_subscribed;
}
export function is_muted_active(sub) {
return sub.is_muted && is_active(sub);
}
export function rename_sub(sub, new_name) {
const old_name = sub.name;

View File

@ -46,6 +46,7 @@ export function update_property(stream_id, property, value, other_values) {
break;
case "in_home_view":
stream_muting.update_is_muted(sub, !value);
stream_list.refresh_muted_or_unmuted_stream(sub);
recent_topics_ui.complete_rerender();
break;
case "desktop_notifications":

View File

@ -3,10 +3,12 @@ import _ from "lodash";
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 * as blueslip from "./blueslip";
import * as color_class from "./color_class";
import * as hash_util from "./hash_util";
import {$t} from "./i18n";
import * as keydown_util from "./keydown_util";
import {ListCursor} from "./list_cursor";
import * as narrow from "./narrow";
@ -100,10 +102,12 @@ export function create_initial_sidebar_rows() {
}
export function build_stream_list(force_rerender) {
// This function assumes we have already created the individual
// sidebar rows. Our job here is to build the bigger widget,
// which largely is a matter of arranging the individual rows in
// the right order.
// The stream list in the left sidebar contains 3 sections:
// pinned, normal, and dormant streams, with headings above them
// as appropriate.
//
// Within the first two sections, muted streams are sorted to the
// bottom; we skip that for dormant streams to simplify discovery.
const streams = stream_data.subscribed_stream_ids();
const $parent = $("#stream_filters");
if (streams.length === 0) {
@ -112,7 +116,7 @@ export function build_stream_list(force_rerender) {
}
// The main logic to build the list is in stream_sort.js, and
// we get three lists of streams (pinned/normal/dormant).
// we get five lists of streams (pinned/normal/muted_pinned/muted_normal/dormant).
const stream_groups = stream_sort.sort_groups(streams, get_search_term());
if (stream_groups.same_as_before && !force_rerender) {
@ -130,24 +134,54 @@ export function build_stream_list(force_rerender) {
topic_list.clear();
$parent.empty();
for (const stream_id of stream_groups.pinned_streams) {
add_sidebar_li(stream_id);
}
const any_pinned_streams = stream_groups.pinned_streams.length > 0;
const any_normal_streams = stream_groups.normal_streams.length > 0;
const any_dormant_streams = stream_groups.dormant_streams.length > 0;
if (any_pinned_streams && (any_normal_streams || any_dormant_streams)) {
elems.push('<hr class="stream-split">');
if (any_pinned_streams) {
elems.push(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Pinned",
}),
}),
);
}
for (const stream_id of stream_groups.pinned_streams) {
add_sidebar_li(stream_id);
}
for (const stream_id of stream_groups.muted_pinned_streams) {
add_sidebar_li(stream_id);
}
if (any_normal_streams) {
elems.push(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Active",
}),
}),
);
}
for (const stream_id of stream_groups.normal_streams) {
add_sidebar_li(stream_id);
}
if (any_dormant_streams && any_normal_streams) {
elems.push('<hr class="stream-split">');
for (const stream_id of stream_groups.muted_active_streams) {
add_sidebar_li(stream_id);
}
if (any_dormant_streams) {
elems.push(
render_stream_subheader({
subheader_name: $t({
defaultMessage: "Inactive",
}),
}),
);
}
for (const stream_id of stream_groups.dormant_streams) {
@ -195,7 +229,7 @@ export function zoom_in_topics(options) {
$(".stream-filters-label").each(function () {
$(this).hide();
});
$(".stream-split").each(function () {
$(".streams_subheader").each(function () {
$(this).hide();
});
@ -216,7 +250,7 @@ export function zoom_out_topics() {
$(".stream-filters-label").each(function () {
$(this).show();
});
$(".stream-split").each(function () {
$(".streams_subheader").each(function () {
$(this).show();
});
@ -376,6 +410,11 @@ export function refresh_pinned_or_unpinned_stream(sub) {
}
}
export function refresh_muted_or_unmuted_stream(sub) {
build_stream_sidebar_row(sub);
update_streams_sidebar();
}
export function get_sidebar_stream_topic_info(filter) {
const result = {
stream_id: undefined,

View File

@ -5,6 +5,8 @@ import * as util from "./util";
let previous_pinned;
let previous_normal;
let previous_dormant;
let previous_muted_active;
let previous_muted_pinned;
let all_streams = [];
export function get_streams() {
@ -41,15 +43,25 @@ export function sort_groups(streams, search_term) {
const pinned_streams = [];
const normal_streams = [];
const muted_pinned_streams = [];
const muted_active_streams = [];
const dormant_streams = [];
for (const stream of streams) {
const sub = sub_store.get(stream);
const pinned = sub.pin_to_top;
if (pinned) {
pinned_streams.push(stream);
if (!sub.is_muted) {
pinned_streams.push(stream);
} else {
muted_pinned_streams.push(stream);
}
} else if (is_normal(sub)) {
normal_streams.push(stream);
if (!sub.is_muted) {
normal_streams.push(stream);
} else {
muted_active_streams.push(stream);
}
} else {
dormant_streams.push(stream);
}
@ -57,20 +69,31 @@ export function sort_groups(streams, search_term) {
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);
const same_as_before =
previous_pinned !== undefined &&
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);
if (!same_as_before) {
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.concat(normal_streams, dormant_streams);
all_streams = pinned_streams.concat(
muted_pinned_streams,
normal_streams,
muted_active_streams,
dormant_streams,
);
}
return {
@ -78,6 +101,8 @@ export function sort_groups(streams, search_term) {
pinned_streams,
normal_streams,
dormant_streams,
muted_pinned_streams,
muted_active_streams,
};
}

View File

@ -1089,7 +1089,9 @@ body.dark-theme {
.sub-unsub-message span::before,
.sub-unsub-message span::after,
.date_row span::before,
.date_row span::after {
.date_row span::after,
.streams_subheader span::before,
.streams_subheader span::after {
opacity: 0.2;
}

View File

@ -169,6 +169,15 @@ li.show-more-topics {
.inactive_stream {
opacity: 0.5;
}
.toggle_stream_mute {
margin-right: 3px;
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
.narrows_panel {
@ -561,6 +570,42 @@ li.expanded_private_message {
}
}
.streams_subheader {
font-size: 0.8em;
font-weight: normal;
padding-left: $far_left_gutter_size;
cursor: pointer;
text-align: center;
margin-right: 12px;
span {
display: flex;
flex-direction: row;
width: 100%;
left: 0.5em;
right: 0.5em;
opacity: 0.5;
}
span::before,
span::after {
content: " ";
flex: 1 1;
vertical-align: middle;
margin: auto;
border-top: 1px solid hsl(0, 0%, 88%);
border-bottom: 1px solid hsl(0, 0%, 100%);
}
span::before {
margin-right: 0.5em;
}
span::after {
margin-left: 0.5em;
}
}
.stream-list-filter {
width: 216px;
white-space: nowrap;

View File

@ -0,0 +1,3 @@
<div class="streams_subheader">
<span>{{ subheader_name }}</span>
</div>