buddy list: Create section in buddy list for users from narrow.

Fixes #21285.
This commit is contained in:
evykassirer 2023-08-22 17:18:53 -07:00 committed by Tim Abbott
parent 12699cdb1d
commit 231aa098cb
28 changed files with 1294 additions and 280 deletions

View File

@ -62,6 +62,7 @@ EXEMPT_FILES = make_set(
"web/src/blueslip.ts", "web/src/blueslip.ts",
"web/src/blueslip_stacktrace.ts", "web/src/blueslip_stacktrace.ts",
"web/src/browser_history.ts", "web/src/browser_history.ts",
"web/src/buddy_list.js",
"web/src/click_handlers.js", "web/src/click_handlers.js",
"web/src/compose.js", "web/src/compose.js",
"web/src/compose_actions.js", "web/src/compose_actions.js",

View File

@ -422,24 +422,21 @@ async function test_stream_search_filters_stream_list(page: Page): Promise<void>
async function test_users_search(page: Page): Promise<void> { async function test_users_search(page: Page): Promise<void> {
console.log("Search users using right sidebar"); console.log("Search users using right sidebar");
async function assert_in_list(page: Page, name: string): Promise<void> { async function assert_in_list(page: Page, name: string): Promise<void> {
await page.waitForSelector( await page.waitForSelector(`#buddy-list-other-users li [data-name="${CSS.escape(name)}"]`, {
`#buddy-list-users-matching-view li [data-name="${CSS.escape(name)}"]`, visible: true,
{ });
visible: true,
},
);
} }
async function assert_selected(page: Page, name: string): Promise<void> { async function assert_selected(page: Page, name: string): Promise<void> {
await page.waitForSelector( await page.waitForSelector(
`#buddy-list-users-matching-view li.highlighted_user [data-name="${CSS.escape(name)}"]`, `#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`,
{visible: true}, {visible: true},
); );
} }
async function assert_not_selected(page: Page, name: string): Promise<void> { async function assert_not_selected(page: Page, name: string): Promise<void> {
await page.waitForSelector( await page.waitForSelector(
`#buddy-list-users-matching-view li.highlighted_user [data-name="${CSS.escape(name)}"]`, `#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`,
{hidden: true}, {hidden: true},
); );
} }
@ -451,9 +448,7 @@ async function test_users_search(page: Page): Promise<void> {
// Enter the search box and test selected suggestion navigation // Enter the search box and test selected suggestion navigation
await page.click("#user_filter_icon"); await page.click("#user_filter_icon");
await page.waitForSelector("#buddy-list-users-matching-view .highlighted_user", { await page.waitForSelector("#buddy-list-other-users .highlighted_user", {visible: true});
visible: true,
});
await assert_selected(page, "Desdemona"); await assert_selected(page, "Desdemona");
await assert_not_selected(page, "Cordelia, Lear's daughter"); await assert_not_selected(page, "Cordelia, Lear's daughter");
await assert_not_selected(page, "King Hamlet"); await assert_not_selected(page, "King Hamlet");
@ -475,12 +470,9 @@ async function test_users_search(page: Page): Promise<void> {
await arrow(page, "Down"); await arrow(page, "Down");
// Now Iago must be highlighted // Now Iago must be highlighted
await page.waitForSelector( await page.waitForSelector('#buddy-list-other-users li.highlighted_user [data-name="Iago"]', {
'#buddy-list-users-matching-view li.highlighted_user [data-name="Iago"]', visible: true,
{ });
visible: true,
},
);
await assert_not_selected(page, "King Hamlet"); await assert_not_selected(page, "King Hamlet");
await assert_not_selected(page, "aaron"); await assert_not_selected(page, "aaron");
await assert_not_selected(page, "Desdemona"); await assert_not_selected(page, "Desdemona");

View File

@ -93,13 +93,14 @@ export function build_user_sidebar() {
return undefined; return undefined;
} }
const filter_text = get_filter_text(); const filter_text = user_filter.text();
const all_user_ids = buddy_data.get_filtered_and_sorted_user_ids(filter_text); const all_user_ids = buddy_data.get_filtered_and_sorted_user_ids(filter_text);
buddy_list.populate({all_user_ids}); buddy_list.populate({all_user_ids});
render_empty_user_list_message_if_needed(buddy_list.$container); render_empty_user_list_message_if_needed(buddy_list.$users_matching_view_container);
render_empty_user_list_message_if_needed(buddy_list.$other_users_container);
return all_user_ids; // for testing return all_user_ids; // for testing
} }

View File

@ -3,9 +3,12 @@ import * as compose_fade_users from "./compose_fade_users";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as muted_users from "./muted_users"; import * as muted_users from "./muted_users";
import * as narrow_state from "./narrow_state";
import * as people from "./people"; import * as people from "./people";
import * as presence from "./presence"; import * as presence from "./presence";
import {realm} from "./state_data"; import {realm} from "./state_data";
import * as stream_data from "./stream_data";
import type {StreamSubscription} from "./sub_store";
import * as timerender from "./timerender"; import * as timerender from "./timerender";
import * as unread from "./unread"; import * as unread from "./unread";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
@ -24,6 +27,11 @@ import * as util from "./util";
export const max_size_before_shrinking = 600; export const max_size_before_shrinking = 600;
let is_searching_users = false; let is_searching_users = false;
export function get_is_searching_users(): boolean {
return is_searching_users;
}
export function set_is_searching_users(val: boolean): void { export function set_is_searching_users(val: boolean): void {
is_searching_users = val; is_searching_users = val;
} }
@ -71,7 +79,35 @@ export function level(user_id: number): number {
} }
} }
export function compare_function(a: number, b: number): number { export function user_matches_narrow(
user_id: number,
pm_ids: Set<number>,
stream_id?: number | null,
): boolean {
if (stream_id) {
return stream_data.is_user_subscribed(stream_id, user_id);
}
if (pm_ids.size > 0) {
return pm_ids.has(user_id) || people.is_my_user_id(user_id);
}
return false;
}
export function compare_function(
a: number,
b: number,
current_sub: StreamSubscription | undefined,
pm_ids: Set<number>,
): number {
const a_would_receive_message = user_matches_narrow(a, pm_ids, current_sub?.stream_id);
const b_would_receive_message = user_matches_narrow(b, pm_ids, current_sub?.stream_id);
if (a_would_receive_message && !b_would_receive_message) {
return -1;
}
if (!a_would_receive_message && b_would_receive_message) {
return 1;
}
const level_a = level(a); const level_a = level(a);
const level_b = level(b); const level_b = level(b);
const diff = level_a - level_b; const diff = level_a - level_b;
@ -91,7 +127,9 @@ export function compare_function(a: number, b: number): number {
export function sort_users(user_ids: number[]): number[] { export function sort_users(user_ids: number[]): number[] {
// TODO sort by unread count first, once we support that // TODO sort by unread count first, once we support that
user_ids.sort(compare_function); const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
user_ids.sort((a, b) => compare_function(a, b, current_sub, pm_ids_set));
return user_ids; return user_ids;
} }
@ -276,7 +314,11 @@ function maybe_shrink_list(user_ids: number[], user_filter_text: string): number
return user_ids; return user_ids;
} }
user_ids = user_ids.filter((user_id) => user_is_recently_active(user_id)); // We want to always show PM recipients even if they're inactive.
const pm_ids_set = narrow_state.pm_ids_set();
user_ids = user_ids.filter(
(user_id) => user_is_recently_active(user_id) || user_matches_narrow(user_id, pm_ids_set),
);
return user_ids; return user_ids;
} }
@ -340,6 +382,13 @@ function get_filtered_user_id_list(user_filter_text: string): number[] {
if (!base_user_id_list.includes(my_user_id)) { if (!base_user_id_list.includes(my_user_id)) {
base_user_id_list = [my_user_id, ...base_user_id_list]; base_user_id_list = [my_user_id, ...base_user_id_list];
} }
// We want to always show PM recipients even if they're inactive.
const pm_ids_set = narrow_state.pm_ids_set();
if (pm_ids_set.size) {
const base_user_id_set = new Set([...base_user_id_list, ...pm_ids_set]);
base_user_id_list = [...base_user_id_set];
}
} }
const user_ids = filter_user_ids(user_filter_text, base_user_id_list); const user_ids = filter_user_ids(user_filter_text, base_user_id_list);

View File

@ -1,16 +1,40 @@
import $ from "jquery"; import $ from "jquery";
import tippy from "tippy.js";
import render_section_header from "../templates/buddy_list/section_header.hbs";
import render_view_all_subscribers from "../templates/buddy_list/view_all_subscribers.hbs";
import render_view_all_users from "../templates/buddy_list/view_all_users.hbs";
import render_empty_list_widget_for_list from "../templates/empty_list_widget_for_list.hbs";
import render_presence_row from "../templates/presence_row.hbs"; import render_presence_row from "../templates/presence_row.hbs";
import render_presence_rows from "../templates/presence_rows.hbs"; import render_presence_rows from "../templates/presence_rows.hbs";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as buddy_data from "./buddy_data"; import * as buddy_data from "./buddy_data";
import {media_breakpoints_num} from "./css_variables";
import * as hash_util from "./hash_util";
import {$t} from "./i18n";
import * as message_viewport from "./message_viewport"; import * as message_viewport from "./message_viewport";
import * as narrow_state from "./narrow_state";
import * as padded_widget from "./padded_widget"; import * as padded_widget from "./padded_widget";
import * as peer_data from "./peer_data";
import * as people from "./people";
import * as scroll_util from "./scroll_util"; import * as scroll_util from "./scroll_util";
import * as stream_data from "./stream_data";
import {INTERACTIVE_HOVER_DELAY} from "./tippyjs";
import {user_settings} from "./user_settings";
function get_formatted_sub_count(sub_count) {
if (sub_count < 1000) {
return sub_count;
}
return new Intl.NumberFormat(user_settings.default_language, {notation: "compact"}).format(
sub_count,
);
}
class BuddyListConf { class BuddyListConf {
container_selector = "#buddy-list-users-matching-view"; matching_view_list_selector = "#buddy-list-users-matching-view";
other_user_list_selector = "#buddy-list-other-users";
scroll_container_selector = "#buddy_list_wrapper"; scroll_container_selector = "#buddy_list_wrapper";
item_selector = "li.user_sidebar_entry"; item_selector = "li.user_sidebar_entry";
padding_selector = "#buddy_list_wrapper_padding"; padding_selector = "#buddy_list_wrapper_padding";
@ -27,8 +51,10 @@ class BuddyListConf {
get_li_from_user_id(opts) { get_li_from_user_id(opts) {
const user_id = opts.user_id; const user_id = opts.user_id;
const $container = $(this.container_selector); const $buddy_list_container = $("#buddy_list_wrapper");
return $container.find(`${this.item_selector}[data-user-id='${CSS.escape(user_id)}']`); return $buddy_list_container.find(
`${this.item_selector}[data-user-id='${CSS.escape(user_id)}']`,
);
} }
get_user_id_from_li(opts) { get_user_id_from_li(opts) {
@ -55,16 +81,260 @@ class BuddyListConf {
export class BuddyList extends BuddyListConf { export class BuddyList extends BuddyListConf {
all_user_ids = []; all_user_ids = [];
users_matching_view_ids = [];
other_user_ids = [];
users_matching_view_is_collapsed = false;
other_users_is_collapsed = false;
initialize_tooltips() {
$("#right-sidebar").on("mouseenter", ".buddy-list-heading", (e) => {
e.stopPropagation();
const $elem = $(e.currentTarget);
let placement = "left";
if (window.innerWidth < media_breakpoints_num.md) {
// On small devices display tooltips based on available space.
// This will default to "bottom" placement for this tooltip.
placement = "auto";
}
const get_total_subscriber_count = this.total_subscriber_count;
tippy($elem[0], {
// Because the buddy list subheadings are potential click targets
// for purposes having nothing to do with the subscriber count
// (collapsing/expanding), we delay showing the tooltip until the
// user lingers a bit longer.
delay: INTERACTIVE_HOVER_DELAY,
// Don't show tooltip on touch devices (99% mobile) since touch
// pressing on users in the left or right sidebar leads to narrow
// being changed and the sidebar is hidden. So, there is no user
// displayed to show tooltip for. It is safe to show the tooltip
// on long press but it not worth the inconvenience of having a
// tooltip hanging around on a small mobile screen if anything
// going wrong.
touch: false,
arrow: true,
placement,
showOnCreate: true,
onShow(instance) {
let tooltip_text;
const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
const subscriber_count = get_total_subscriber_count(current_sub, pm_ids_set);
const elem_id = $elem.attr("id");
if (elem_id === "buddy-list-users-matching-view-section-heading") {
if (current_sub) {
tooltip_text = $t(
{
defaultMessage:
"{N, plural, one {# subscriber} other {# subscribers}}",
},
{N: subscriber_count},
);
} else {
tooltip_text = $t(
{
defaultMessage:
"{N, plural, one {# participant} other {# participants}}",
},
{N: subscriber_count},
);
}
} else {
const total_user_count = people.get_active_human_count();
const other_users_count = total_user_count - subscriber_count;
tooltip_text = $t(
{
defaultMessage:
"{N, plural, one {# other user} other {# other users}}",
},
{N: other_users_count},
);
}
instance.setContent(tooltip_text);
},
onHidden(instance) {
instance.destroy();
},
appendTo: () => document.body,
});
});
}
populate(opts) { populate(opts) {
this.render_count = 0; this.render_count = 0;
this.$container.empty(); this.$users_matching_view_container.empty();
this.users_matching_view_ids = [];
this.$other_users_container.empty();
this.other_user_ids = [];
// We rely on our caller to give us items // We rely on our caller to give us items
// in already-sorted order. // in already-sorted order.
this.all_user_ids = opts.all_user_ids; this.all_user_ids = opts.all_user_ids;
this.fill_screen_with_content(); this.fill_screen_with_content();
// We do a handful of things once we're done rendering all the users,
// and each of these tasks need shared data that we'll compute first.
const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
// If we have only "other users" and aren't in a stream/DM view
// then we don't show section headers and only show one untitled
// section.
const hide_headers = this.should_hide_headers(current_sub, pm_ids_set);
const subscriber_count = this.total_subscriber_count(current_sub, pm_ids_set);
const total_user_count = people.get_active_human_count();
const other_users_count = total_user_count - subscriber_count;
const has_inactive_users_matching_view =
subscriber_count > this.users_matching_view_ids.length;
const has_inactive_other_users = other_users_count > this.other_user_ids.length;
const data = {
current_sub,
hide_headers,
subscriber_count,
other_users_count,
has_inactive_users_matching_view,
has_inactive_other_users,
};
$("#buddy-list-users-matching-view-container .view-all-subscribers-link").remove();
$("#buddy-list-other-users-container .view-all-users-link").remove();
if (!buddy_data.get_is_searching_users()) {
this.render_view_user_list_links(data);
}
this.render_section_headers(data);
if (!hide_headers) {
this.update_empty_list_placeholders(data);
}
}
update_empty_list_placeholders({has_inactive_users_matching_view, has_inactive_other_users}) {
let matching_view_empty_list_message;
let other_users_empty_list_message;
if (buddy_data.get_is_searching_users()) {
matching_view_empty_list_message = $t({defaultMessage: "No matching users."});
other_users_empty_list_message = $t({defaultMessage: "No matching users."});
} else {
if (has_inactive_users_matching_view) {
matching_view_empty_list_message = $t({defaultMessage: "No active users."});
} else {
matching_view_empty_list_message = $t({defaultMessage: "None."});
}
if (has_inactive_other_users) {
other_users_empty_list_message = $t({defaultMessage: "No active users."});
} else {
other_users_empty_list_message = $t({defaultMessage: "None."});
}
}
$("#buddy-list-users-matching-view").data(
"search-results-empty",
matching_view_empty_list_message,
);
if ($("#buddy-list-users-matching-view .empty-list-message").length) {
const empty_list_widget = render_empty_list_widget_for_list({
matching_view_empty_list_message,
});
$("#buddy-list-users-matching-view").empty();
$("#buddy-list-users-matching-view").append(empty_list_widget);
}
$("#buddy-list-other-users").data("search-results-empty", other_users_empty_list_message);
if ($("#buddy-list-other-users .empty-list-message").length) {
const empty_list_widget = render_empty_list_widget_for_list({
other_users_empty_list_message,
});
$("#buddy-list-other-users").empty();
$("#buddy-list-other-users").append(empty_list_widget);
}
}
render_section_headers({current_sub, hide_headers, subscriber_count, other_users_count}) {
$("#buddy-list-users-matching-view-container .buddy-list-subsection-header").empty();
$("#buddy-list-other-users-container .buddy-list-subsection-header").empty();
$("#buddy-list-users-matching-view-container").toggleClass("no-display", hide_headers);
// Usually we show the user counts in the headers, but if we're hiding
// those headers then we show the total user count in the main title.
const default_userlist_title = $t({defaultMessage: "USERS"});
if (hide_headers) {
const total_user_count = people.get_active_human_count();
const formatted_count = get_formatted_sub_count(total_user_count);
const userlist_title = `${default_userlist_title} (${formatted_count})`;
$("#userlist-title").text(userlist_title);
return;
}
$("#userlist-title").text(default_userlist_title);
let header_text;
if (current_sub) {
header_text = $t({defaultMessage: "In this stream"});
} else {
header_text = $t({defaultMessage: "In this conversation"});
}
$("#buddy-list-users-matching-view-container .buddy-list-subsection-header").append(
render_section_header({
id: "buddy-list-users-matching-view-section-heading",
header_text,
user_count: get_formatted_sub_count(subscriber_count),
toggle_class: "toggle-users-matching-view",
is_collapsed: this.users_matching_view_is_collapsed,
}),
);
$("#buddy-list-other-users-container .buddy-list-subsection-header").append(
render_section_header({
id: "buddy-list-other-users-section-heading",
header_text: $t({defaultMessage: "Others"}),
user_count: get_formatted_sub_count(other_users_count),
toggle_class: "toggle-other-users",
is_collapsed: this.other_users_is_collapsed,
}),
);
}
toggle_users_matching_view_section() {
this.users_matching_view_is_collapsed = !this.users_matching_view_is_collapsed;
$("#buddy-list-users-matching-view-container").toggleClass(
"collapsed",
this.users_matching_view_is_collapsed,
);
$("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass(
"fa-caret-down",
!this.users_matching_view_is_collapsed,
);
$("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass(
"fa-caret-right",
this.users_matching_view_is_collapsed,
);
// Collapsing and uncollapsing sections has a similar effect to
// scrolling, so we make sure to fill screen with content here as well.
this.fill_screen_with_content();
}
toggle_other_users_section() {
this.other_users_is_collapsed = !this.other_users_is_collapsed;
$("#buddy-list-other-users-container").toggleClass(
"collapsed",
this.other_users_is_collapsed,
);
$("#buddy-list-other-users-container .toggle-other-users").toggleClass(
"fa-caret-down",
!this.other_users_is_collapsed,
);
$("#buddy-list-other-users-container .toggle-other-users").toggleClass(
"fa-caret-right",
this.other_users_is_collapsed,
);
// Collapsing and uncollapsing sections has a similar effect to
// scrolling, so we make sure to fill screen with content here as well.
this.fill_screen_with_content();
} }
render_more(opts) { render_more(opts) {
@ -80,12 +350,57 @@ export class BuddyList extends BuddyListConf {
} }
const items = this.get_data_from_user_ids(more_user_ids); const items = this.get_data_from_user_ids(more_user_ids);
const subscribed_users = [];
const other_users = [];
const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
const html = this.items_to_html({ for (const item of items) {
items, if (buddy_data.user_matches_narrow(item.user_id, pm_ids_set, current_sub?.stream_id)) {
subscribed_users.push(item);
this.users_matching_view_ids.push(item.user_id);
} else {
other_users.push(item);
this.other_user_ids.push(item.user_id);
}
}
// Remove the empty list message before adding users
if (
$(`${this.matching_view_list_selector} .empty-list-message`).length > 0 &&
subscribed_users.length
) {
this.$users_matching_view_container.empty();
}
const subscribed_users_html = this.items_to_html({
items: subscribed_users,
});
this.$users_matching_view_container = $(this.matching_view_list_selector);
this.$users_matching_view_container.append(subscribed_users_html);
// Remove the empty list message before adding users
if (
$(`${this.other_user_list_selector} .empty-list-message`).length > 0 &&
other_users.length
) {
this.$other_users_container.empty();
}
const other_users_html = this.items_to_html({
items: other_users,
});
this.$other_users_container = $(this.other_user_list_selector);
this.$other_users_container.append(other_users_html);
const hide_headers = this.should_hide_headers(current_sub, pm_ids_set);
const subscriber_count = this.total_subscriber_count(current_sub, pm_ids_set);
const total_user_count = people.get_active_human_count();
const other_users_count = total_user_count - subscriber_count;
this.render_section_headers({
current_sub,
hide_headers,
subscriber_count,
other_users_count,
}); });
this.$container = $(this.container_selector);
this.$container.append(html);
// Invariant: more_user_ids.length >= items.length. // Invariant: more_user_ids.length >= items.length.
// (Usually they're the same, but occasionally user ids // (Usually they're the same, but occasionally user ids
@ -98,44 +413,162 @@ export class BuddyList extends BuddyListConf {
} }
get_items() { get_items() {
const $obj = this.$container.find(`${this.item_selector}`); const $user_matching_view_obj = this.$users_matching_view_container.find(
return $obj.map((_i, elem) => $(elem)); `${this.item_selector}`,
);
const $users_matching_view_elems = $user_matching_view_obj.map((_i, elem) => $(elem));
const $other_user_obj = this.$other_users_container.find(`${this.item_selector}`);
const $other_user_elems = $other_user_obj.map((_i, elem) => $(elem));
return [...$users_matching_view_elems, ...$other_user_elems];
}
should_hide_headers(current_sub, pm_ids_set) {
// If we have only "other users" and aren't in a stream/DM view
// then we don't show section headers and only show one untitled
// section.
return this.users_matching_view_ids.length === 0 && !current_sub && !pm_ids_set.size;
}
total_subscriber_count(current_sub, pm_ids_set) {
// Includes inactive users who might not show up in the buddy list.
if (current_sub) {
return peer_data.get_subscriber_count(current_sub.stream_id, false);
} else if (pm_ids_set.size) {
const pm_ids_list = [...pm_ids_set];
// Plus one for the "me" user, who isn't in the recipients list (except
// for when it's a private message conversation with only "me" in it).
if (pm_ids_list.length === 1 && people.is_my_user_id(pm_ids_list[0])) {
return 1;
}
return pm_ids_list.length + 1;
}
return 0;
}
render_view_user_list_links({
current_sub,
has_inactive_users_matching_view,
has_inactive_other_users,
}) {
// For stream views, we show a link at the bottom of the list of subscribed users that
// lets a user find the full list of subscribed users and information about them.
if (
current_sub &&
stream_data.can_view_subscribers(current_sub) &&
has_inactive_users_matching_view
) {
const stream_edit_hash = hash_util.stream_edit_url(current_sub, "subscribers");
$("#buddy-list-users-matching-view-container").append(
render_view_all_subscribers({
stream_edit_hash,
}),
);
}
// We give a link to view the list of all users to help reduce confusion about
// there being hidden (inactive) "other" users.
if (has_inactive_other_users) {
$("#buddy-list-other-users-container").append(render_view_all_users());
}
} }
// From `type List<Key>`, where the key is a user_id. // From `type List<Key>`, where the key is a user_id.
first_key() { first_key() {
return this.all_user_ids[0]; if (this.users_matching_view_ids.length) {
return this.users_matching_view_ids[0];
}
if (this.other_user_ids.length) {
return this.other_user_ids[0];
}
return undefined;
} }
// From `type List<Key>`, where the key is a user_id. // From `type List<Key>`, where the key is a user_id.
prev_key(key) { prev_key(key) {
const i = this.all_user_ids.indexOf(key); let i = this.users_matching_view_ids.indexOf(key);
// This would be the middle of the list of users matching view,
if (i <= 0) { // moving to a prev user matching the view.
if (i > 0) {
return this.users_matching_view_ids[i - 1];
}
// If it's the first user matching the view, we don't move the selection.
if (i === 0) {
return undefined; return undefined;
} }
return this.all_user_ids[i - 1]; // This would be the middle of the other users list moving to a prev other user.
i = this.other_user_ids.indexOf(key);
if (i > 0) {
return this.other_user_ids[i - 1];
}
// The key before the first other user is the last user matching view, if that exists,
// and if it doesn't then we don't move the selection.
if (i === 0) {
if (this.users_matching_view_ids.length > 0) {
return this.users_matching_view_ids.at(-1);
}
return undefined;
}
// The only way we reach here is if the key isn't found in either list,
// which shouldn't happen.
blueslip.error("Couldn't find key in buddy list", {
key,
users_matching_view_ids: this.users_matching_view_ids,
other_user_ids: this.other_user_ids,
});
return undefined;
} }
// From `type List<Key>`, where the key is a user_id. // From `type List<Key>`, where the key is a user_id.
next_key(key) { next_key(key) {
const i = this.all_user_ids.indexOf(key); let i = this.users_matching_view_ids.indexOf(key);
// Moving from users matching the view to the list of other users,
if (i < 0) { // if they exist, otherwise do nothing.
if (i >= 0 && i === this.users_matching_view_ids.length - 1) {
if (this.other_user_ids.length > 0) {
return this.other_user_ids[0];
}
return undefined; return undefined;
} }
// This is a regular move within the list of users matching the view.
if (i >= 0) {
return this.users_matching_view_ids[i + 1];
}
return this.all_user_ids[i + 1]; i = this.other_user_ids.indexOf(key);
// If we're at the end of other users, we don't do anything.
if (i >= 0 && i === this.other_user_ids.length - 1) {
return undefined;
}
// This is a regular move within other users.
if (i >= 0) {
return this.other_user_ids[i + 1];
}
// The only way we reach here is if the key isn't found in either list,
// which shouldn't happen.
blueslip.error("Couldn't find key in buddy list", {
key,
users_matching_view_ids: this.users_matching_view_ids,
other_user_ids: this.other_user_ids,
});
return undefined;
} }
maybe_remove_user_id(opts) { maybe_remove_user_id(opts) {
const pos = this.all_user_ids.indexOf(opts.user_id); let pos = this.users_matching_view_ids.indexOf(opts.user_id);
if (pos >= 0) {
if (pos < 0) { this.users_matching_view_ids.splice(pos, 1);
return; } else {
pos = this.other_user_ids.indexOf(opts.user_id);
if (pos < 0) {
return;
}
this.other_user_ids.splice(pos, 1);
} }
pos = this.all_user_ids.indexOf(opts.user_id);
this.all_user_ids.splice(pos, 1); this.all_user_ids.splice(pos, 1);
if (pos < this.render_count) { if (pos < this.render_count) {
@ -150,15 +583,20 @@ export class BuddyList extends BuddyListConf {
const user_id = opts.user_id; const user_id = opts.user_id;
let i; let i;
for (i = 0; i < this.all_user_ids.length; i += 1) { const user_id_list = opts.user_id_list;
const list_user_id = this.all_user_ids[i];
if (this.compare_function(user_id, list_user_id) < 0) { const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
for (i = 0; i < user_id_list.length; i += 1) {
const list_user_id = user_id_list[i];
if (this.compare_function(user_id, list_user_id, current_sub, pm_ids_set) < 0) {
return i; return i;
} }
} }
return this.all_user_ids.length; return user_id_list.length;
} }
force_render(opts) { force_render(opts) {
@ -199,6 +637,8 @@ export class BuddyList extends BuddyListConf {
return $li; return $li;
} }
// We reference all_user_ids to see if we've rendered
// it yet.
const pos = this.all_user_ids.indexOf(user_id); const pos = this.all_user_ids.indexOf(user_id);
if (pos < 0) { if (pos < 0) {
@ -221,12 +661,18 @@ export class BuddyList extends BuddyListConf {
insert_new_html(opts) { insert_new_html(opts) {
const user_id_following_insertion = opts.new_user_id; const user_id_following_insertion = opts.new_user_id;
const html = opts.html; const html = opts.html;
const new_pos_in_all_users = opts.pos; const new_pos_in_all_users = opts.new_pos_in_all_users;
const is_subscribed_user = opts.is_subscribed_user;
// This means we're inserting at the end
if (user_id_following_insertion === undefined) { if (user_id_following_insertion === undefined) {
if (new_pos_in_all_users === this.render_count) { if (new_pos_in_all_users === this.render_count) {
this.render_count += 1; this.render_count += 1;
this.$container.append(html); if (is_subscribed_user) {
this.$users_matching_view_container.append(html);
} else {
this.$other_users_container.append(html);
}
this.update_padding(); this.update_padding();
} }
return; return;
@ -246,22 +692,40 @@ export class BuddyList extends BuddyListConf {
this.maybe_remove_user_id({user_id}); this.maybe_remove_user_id({user_id});
const pos = this.find_position({ const new_pos_in_all_users = this.find_position({
user_id, user_id,
user_id_list: this.all_user_ids,
});
const current_sub = narrow_state.stream_sub();
const pm_ids_set = narrow_state.pm_ids_set();
const is_subscribed_user = buddy_data.user_matches_narrow(
user_id,
pm_ids_set,
current_sub?.stream_id,
);
const user_id_list = is_subscribed_user
? this.users_matching_view_ids
: this.other_user_ids;
const new_pos_in_user_list = this.find_position({
user_id,
user_id_list,
}); });
// Order is important here--get the new_user_id // Order is important here--get the new_user_id
// before mutating our list. An undefined value // before mutating our list. An undefined value
// corresponds to appending. // corresponds to appending.
const new_user_id = this.all_user_ids[pos]; const new_user_id = user_id_list[new_pos_in_user_list];
this.all_user_ids.splice(pos, 0, user_id); user_id_list.splice(new_pos_in_user_list, 0, user_id);
this.all_user_ids.splice(new_pos_in_all_users, 0, user_id);
const html = this.item_to_html({item}); const html = this.item_to_html({item});
this.insert_new_html({ this.insert_new_html({
pos, new_pos_in_all_users,
html, html,
new_user_id, new_user_id,
is_subscribed_user,
}); });
} }
@ -293,7 +757,8 @@ export class BuddyList extends BuddyListConf {
// This is a bit of a hack to make sure we at least have // This is a bit of a hack to make sure we at least have
// an empty list to start, before we get the initial payload. // an empty list to start, before we get the initial payload.
$container = $(this.container_selector); $users_matching_view_container = $(this.matching_view_list_selector);
$other_users_container = $(this.other_user_list_selector);
start_scroll_handler() { start_scroll_handler() {
// We have our caller explicitly call this to make // We have our caller explicitly call this to make
@ -309,7 +774,7 @@ export class BuddyList extends BuddyListConf {
padded_widget.update_padding({ padded_widget.update_padding({
shown_rows: this.render_count, shown_rows: this.render_count,
total_rows: this.all_user_ids.length, total_rows: this.all_user_ids.length,
content_selector: this.container_selector, content_selector: "#buddy_list_wrapper",
padding_selector: this.padding_selector, padding_selector: this.padding_selector,
}); });
} }

View File

@ -474,18 +474,16 @@ export function initialize() {
}); });
// SIDEBARS // SIDEBARS
$("#buddy-list-users-matching-view") $(".buddy-list-section").on("click", ".selectable_sidebar_block", (e) => {
.expectOne() const $li = $(e.target).parents("li");
.on("click", ".selectable_sidebar_block", (e) => {
const $li = $(e.target).parents("li");
activity_ui.narrow_for_user({$li}); activity_ui.narrow_for_user({$li});
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
sidebar_ui.hide_userlist_sidebar(); sidebar_ui.hide_userlist_sidebar();
$(".tooltip").remove(); $(".tooltip").remove();
}); });
// Doesn't show tooltip on touch devices. // Doesn't show tooltip on touch devices.
function do_render_buddy_list_tooltip( function do_render_buddy_list_tooltip(
@ -549,7 +547,7 @@ export function initialize() {
} }
// BUDDY LIST TOOLTIPS (not displayed on touch devices) // BUDDY LIST TOOLTIPS (not displayed on touch devices)
$("#buddy-list-users-matching-view").on("mouseenter", ".selectable_sidebar_block", (e) => { $(".buddy-list-section").on("mouseenter", ".selectable_sidebar_block", (e) => {
e.stopPropagation(); e.stopPropagation();
const $elem = $(e.currentTarget).closest(".user_sidebar_entry").find(".user-presence-link"); const $elem = $(e.currentTarget).closest(".user_sidebar_entry").find(".user-presence-link");
const user_id_string = $elem.attr("data-user-id"); const user_id_string = $elem.attr("data-user-id");
@ -557,7 +555,7 @@ export function initialize() {
// `target_node` is the `ul` element since it stays in DOM even after updates. // `target_node` is the `ul` element since it stays in DOM even after updates.
function get_target_node() { function get_target_node() {
return document.querySelector("#buddy-list-users-matching-view"); return $(e.target).parents(".buddy-list-section")[0];
} }
function check_reference_removed(mutation, instance) { function check_reference_removed(mutation, instance) {

View File

@ -4,6 +4,7 @@ const list_selectors = [
"#stream_filters", "#stream_filters",
"#left-sidebar-navigation-list", "#left-sidebar-navigation-list",
"#buddy-list-users-matching-view", "#buddy-list-users-matching-view",
"#buddy-list-other-users",
"#send_later_options", "#send_later_options",
]; ];

View File

@ -2,6 +2,7 @@ import * as Sentry from "@sentry/browser";
import $ from "jquery"; import $ from "jquery";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import * as activity_ui from "./activity_ui";
import {all_messages_data} from "./all_messages_data"; import {all_messages_data} from "./all_messages_data";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as browser_history from "./browser_history"; import * as browser_history from "./browser_history";
@ -1031,6 +1032,7 @@ function handle_post_view_change(msg_list) {
left_sidebar_navigation_area.handle_narrow_activated(filter); left_sidebar_navigation_area.handle_narrow_activated(filter);
stream_list.handle_narrow_activated(filter); stream_list.handle_narrow_activated(filter);
pm_list.handle_narrow_activated(filter); pm_list.handle_narrow_activated(filter);
activity_ui.build_user_sidebar();
} }
function handle_post_narrow_deactivate_processes(msg_list) { function handle_post_narrow_deactivate_processes(msg_list) {

View File

@ -3,6 +3,7 @@ import $ from "jquery";
import render_left_sidebar from "../templates/left_sidebar.hbs"; import render_left_sidebar from "../templates/left_sidebar.hbs";
import render_right_sidebar from "../templates/right_sidebar.hbs"; import render_right_sidebar from "../templates/right_sidebar.hbs";
import {buddy_list} from "./buddy_list";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as rendered_markdown from "./rendered_markdown"; import * as rendered_markdown from "./rendered_markdown";
import * as resize from "./resize"; import * as resize from "./resize";
@ -161,6 +162,9 @@ export function initialize_right_sidebar(): void {
}); });
$("#right-sidebar-container").html(rendered_sidebar); $("#right-sidebar-container").html(rendered_sidebar);
buddy_list.initialize_tooltips();
update_invite_user_option(); update_invite_user_option();
if (page_params.is_spectator) { if (page_params.is_spectator) {
rendered_markdown.update_elements( rendered_markdown.update_elements(
@ -187,4 +191,18 @@ export function initialize_right_sidebar(): void {
} }
} }
}); });
$("#buddy-list-users-matching-view-container").on(
"click",
".buddy-list-subsection-header",
(e) => {
e.stopPropagation();
buddy_list.toggle_users_matching_view_section();
},
);
$("#buddy-list-other-users-container").on("click", ".buddy-list-subsection-header", (e) => {
e.stopPropagation();
buddy_list.toggle_other_users_section();
});
} }

View File

@ -2,10 +2,13 @@ import $ from "jquery";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import tippy, {delegate} from "tippy.js"; import tippy, {delegate} from "tippy.js";
import render_buddy_list_title_tooltip from "../templates/buddy_list/title_tooltip.hbs";
import render_tooltip_templates from "../templates/tooltip_templates.hbs"; import render_tooltip_templates from "../templates/tooltip_templates.hbs";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as people from "./people";
import * as popovers from "./popovers"; import * as popovers from "./popovers";
import * as ui_util from "./ui_util";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
// For tooltips without data-tippy-content, we use the HTML content of // For tooltips without data-tippy-content, we use the HTML content of
@ -240,7 +243,6 @@ export function initialize(): void {
delegate("body", { delegate("body", {
target: [ target: [
"#streams_header .streams-tooltip-target", "#streams_header .streams-tooltip-target",
"#userlist-title",
"#user_filter_icon", "#user_filter_icon",
"#scroll-to-bottom-button-clickable-area", "#scroll-to-bottom-button-clickable-area",
".spectator_narrow_login_button", ".spectator_narrow_login_button",
@ -561,4 +563,16 @@ export function initialize(): void {
}, },
appendTo: () => document.body, appendTo: () => document.body,
}); });
delegate("body", {
target: "#userlist-header",
placement: "top",
appendTo: () => document.body,
onShow(instance) {
const total_user_count = people.get_active_human_count();
instance.setContent(
ui_util.parse_html(render_buddy_list_title_tooltip({total_user_count})),
);
},
});
} }

View File

@ -747,6 +747,7 @@ export function initialize_everything(state_data) {
sidebar_ui.hide_all(); sidebar_ui.hide_all();
popovers.hide_all(); popovers.hide_all();
narrow.by("stream", sub.name, {trigger}); narrow.by("stream", sub.name, {trigger});
activity_ui.build_user_sidebar();
}, },
}); });
stream_list_sort.initialize(); stream_list_sort.initialize();
@ -792,7 +793,6 @@ export function initialize_everything(state_data) {
search.initialize({ search.initialize({
on_narrow_search: narrow.activate, on_narrow_search: narrow.activate,
}); });
tutorial.initialize();
desktop_notifications.initialize(); desktop_notifications.initialize();
audible_notifications.initialize(); audible_notifications.initialize();
compose_notifications.initialize({ compose_notifications.initialize({
@ -814,9 +814,6 @@ export function initialize_everything(state_data) {
settings_toggle.initialize(); settings_toggle.initialize();
about_zulip.initialize(); about_zulip.initialize();
// All overlays must be initialized before hashchange.js
hashchange.initialize();
initialize_unread_ui(); initialize_unread_ui();
activity.initialize(); activity.initialize();
activity_ui.initialize({ activity_ui.initialize({
@ -824,6 +821,13 @@ export function initialize_everything(state_data) {
narrow.by("dm", email, {trigger: "sidebar"}); narrow.by("dm", email, {trigger: "sidebar"});
}, },
}); });
// This needs to happen after activity_ui.initialize, so that user_filter
// is defined.
tutorial.initialize();
// All overlays, and also activity_ui, must be initialized before hashchange.js
hashchange.initialize();
emoji_picker.initialize(); emoji_picker.initialize();
user_group_popover.initialize(); user_group_popover.initialize();
user_card_popover.initialize(); user_card_popover.initialize();

View File

@ -810,13 +810,13 @@ function register_click_handlers() {
$("body").on("click", ".update_status_text", open_user_status_modal); $("body").on("click", ".update_status_text", open_user_status_modal);
// Clicking on one's own status emoji should open the user status modal. // Clicking on one's own status emoji should open the user status modal.
$("#buddy-list-users-matching-view").on( $(".buddy-list-section").on(
"click", "click",
".user_sidebar_entry_me .status-emoji", ".user_sidebar_entry_me .status-emoji",
open_user_status_modal, open_user_status_modal,
); );
$("#buddy-list-users-matching-view").on("click", ".user-list-sidebar-menu-icon", (e) => { $(".buddy-list-section").on("click", ".user-list-sidebar-menu-icon", (e) => {
e.stopPropagation(); e.stopPropagation();
const $target = $(e.currentTarget).closest("li"); const $target = $(e.currentTarget).closest("li");

View File

@ -1,5 +1,6 @@
import $ from "jquery"; import $ from "jquery";
import * as activity_ui from "./activity_ui";
import * as compose_actions from "./compose_actions"; import * as compose_actions from "./compose_actions";
import * as compose_recipient from "./compose_recipient"; import * as compose_recipient from "./compose_recipient";
import * as dropdown_widget from "./dropdown_widget"; import * as dropdown_widget from "./dropdown_widget";
@ -88,6 +89,10 @@ export function show(opts) {
opts.complete_rerender(); opts.complete_rerender();
compose_actions.on_show_navigation_view(); compose_actions.on_show_navigation_view();
// This has to happen after resetting the current narrow filter, so
// that the buddy list is rendered with the correct narrow state.
activity_ui.build_user_sidebar();
// Misc. // Misc.
if (opts.is_recent_view) { if (opts.is_recent_view) {
resize.update_recent_view_filters_height(); resize.update_recent_view_filters_height();

View File

@ -197,6 +197,7 @@
--color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 60%); --color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 60%);
--color-background-popover: hsl(0deg 0% 100%); --color-background-popover: hsl(0deg 0% 100%);
--color-background-alert-word: hsl(18deg 100% 84%); --color-background-alert-word: hsl(18deg 100% 84%);
--color-buddy-list-highlighted-user: hsl(120deg 12.3% 71.4% / 38%);
/* Compose box colors */ /* Compose box colors */
--color-compose-send-button-icon-color: hsl(0deg 0% 100%); --color-compose-send-button-icon-color: hsl(0deg 0% 100%);
@ -247,6 +248,8 @@
--color-text-personal-menu-some-status: hsl(0deg 0% 40%); --color-text-personal-menu-some-status: hsl(0deg 0% 40%);
--color-text-sidebar-heading: hsl(0deg 0% 43%); --color-text-sidebar-heading: hsl(0deg 0% 43%);
--color-text-sidebar-popover-menu: hsl(0deg 0% 20%); --color-text-sidebar-popover-menu: hsl(0deg 0% 20%);
--color-text-url: hsl(200deg 100% 40%);
--color-text-url-hover: hsl(200deg 100% 25%);
/* Markdown code colors */ /* Markdown code colors */
--color-markdown-code-text: hsl(0deg 0% 0%); --color-markdown-code-text: hsl(0deg 0% 0%);
@ -484,6 +487,7 @@
--color-outline-tab-picker-tab-option: hsl(0deg 0% 100% / 12%); --color-outline-tab-picker-tab-option: hsl(0deg 0% 100% / 12%);
--color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 5%); --color-background-tab-picker-tab-option-hover: hsl(0deg 0% 100% / 5%);
--color-background-alert-word: hsl(22deg 70% 35%); --color-background-alert-word: hsl(22deg 70% 35%);
--color-buddy-list-highlighted-user: hsl(136deg 25% 73% / 20%);
/* Compose box colors */ /* Compose box colors */
--color-compose-send-button-focus-shadow: hsl(0deg 0% 100% / 80%); --color-compose-send-button-focus-shadow: hsl(0deg 0% 100% / 80%);
@ -532,6 +536,7 @@
--color-text-search: hsl(0deg 0% 100% / 75%); --color-text-search: hsl(0deg 0% 100% / 75%);
--color-text-search-hover: hsl(0deg 0% 100%); --color-text-search-hover: hsl(0deg 0% 100%);
--color-text-search-placeholder: hsl(0deg 0% 100% / 50%); --color-text-search-placeholder: hsl(0deg 0% 100% / 50%);
--color-text-empty-list-message: hsl(0deg 0% 67%);
--color-text-dropdown-menu: hsl(0deg 0% 100% / 80%); --color-text-dropdown-menu: hsl(0deg 0% 100% / 80%);
--color-text-full-name: hsl(0deg 0% 100%); --color-text-full-name: hsl(0deg 0% 100%);
--color-text-item: hsl(0deg 0% 50%); --color-text-item: hsl(0deg 0% 50%);
@ -547,6 +552,7 @@
hsl(0deg 0% 75%) 75%, hsl(0deg 0% 75%) 75%,
hsl(0deg 0% 11%) hsl(0deg 0% 11%)
); );
--color-text-url-hover: hsl(200deg 79% 66%);
/* Markdown code colors */ /* Markdown code colors */
/* Note that Markdown code-link colors are identical /* Note that Markdown code-link colors are identical

View File

@ -687,11 +687,6 @@
} }
} }
#buddy-list-users-matching-view li:hover,
#buddy-list-users-matching-view li.highlighted_user {
background-color: hsl(136deg 25% 73% / 20%);
}
.group-row.active, .group-row.active,
.stream-row.active { .stream-row.active {
background-color: hsl(0deg 0% 0% / 20%); background-color: hsl(0deg 0% 0% / 20%);

View File

@ -20,8 +20,44 @@ $user_status_emoji_width: 24px;
overflow: auto; overflow: auto;
} }
#buddy-list-users-matching-view { .toggle-narrow-users,
max-width: 95%; .toggle-other-users {
width: 7px;
}
#buddy-list-users-matching-view-container,
#buddy-list-other-users-container {
margin-bottom: 10px;
&.no-display {
display: none;
}
.view-all-subscribers-link,
.view-all-users-link {
margin-left: 15px;
}
}
#buddy-list-users-matching-view-container.collapsed {
#buddy-list-users-matching-view,
.view-all-subscribers-link {
display: none;
}
}
#buddy-list-other-users-container.collapsed {
#buddy-list-other-users,
.view-all-users-link {
display: none;
}
}
#buddy-list-users-matching-view,
#buddy-list-other-users {
width: 90%;
margin-left: 10px;
margin-bottom: 0;
overflow-x: hidden; overflow-x: hidden;
list-style-position: inside; /* Draw the bullets inside our box */ list-style-position: inside; /* Draw the bullets inside our box */
@ -31,7 +67,6 @@ $user_status_emoji_width: 24px;
text-overflow: ellipsis; text-overflow: ellipsis;
list-style-type: none; list-style-type: none;
border-radius: 4px; border-radius: 4px;
padding-right: 15px;
padding-top: 1px; padding-top: 1px;
padding-bottom: 2px; padding-bottom: 2px;
@ -81,7 +116,7 @@ $user_status_emoji_width: 24px;
&:hover, &:hover,
&.highlighted_user { &.highlighted_user {
background-color: hsl(120deg 12.3% 71.4% / 38%); background-color: var(--color-buddy-list-highlighted-user);
} }
} }
@ -93,19 +128,60 @@ $user_status_emoji_width: 24px;
} }
.empty-list-message { .empty-list-message {
font-size: 1.2em; font-style: italic;
color: var(--color-text-empty-list-message);
/* Overwrite default empty list font size, to look better under the subheaders. */
font-size: 14px;
/* Override .empty-list-message !important styling */
padding: 0 !important;
margin-left: 5px;
text-align: left;
&:hover { &:hover {
background-color: inherit; background-color: inherit;
} }
} }
/* Overwrite some stray list rules (including one in left_sidebar.css) to turn color
back to the bootstrap default. */
.view-all-subscribers-link,
.view-all-users-link {
color: var(--color-text-url);
&:hover {
color: var(--color-text-url-hover);
}
}
& a { & a {
color: inherit; color: inherit;
margin-left: 0; margin-left: 0;
} }
} }
.buddy-list-subsection-header {
display: flex;
align-items: center;
cursor: pointer;
background-color: var(--color-background);
line-height: 1;
position: sticky;
top: 0;
z-index: 1;
color: var(--color-text-default);
}
.buddy-list-heading {
user-select: none;
font-weight: 600;
margin: 0;
padding: 5px;
}
.buddy-list-subsection-header.no-display {
display: none;
}
.user-presence-link, .user-presence-link,
.user_sidebar_entry .selectable_sidebar_block { .user_sidebar_entry .selectable_sidebar_block {
display: flex; display: flex;

View File

@ -0,0 +1,4 @@
<h5 id="{{id}}" class="buddy-list-heading no-style hidden-for-spectators">
{{header_text}} ({{user_count}})
</h5>
<i class="{{toggle_class}} fa fa-sm {{#if is_collapsed}}fa-caret-right{{else}}fa-caret-down{{/if}}" aria-hidden="true"></i>

View File

@ -0,0 +1,4 @@
{{#tr}}
Search {total_user_count, plural, =1 {1 person} other {# people}}
{{/tr}}
{{tooltip_hotkey_hints "W"}}

View File

@ -0,0 +1 @@
<a class="view-all-subscribers-link" href="{{stream_edit_hash}}">View all subscribers</a>

View File

@ -0,0 +1 @@
<a class="view-all-users-link" href="#organization/user-list-admin">View all users</a>

View File

@ -2,8 +2,7 @@
<div class="right-sidebar-items"> <div class="right-sidebar-items">
<div id="user-list"> <div id="user-list">
<div id="userlist-header"> <div id="userlist-header">
<h4 class='right-sidebar-title' data-tooltip-template-id="search-people-tooltip-template" <h4 class='right-sidebar-title' id='userlist-title'>
id='userlist-title'>
{{t 'USERS' }} {{t 'USERS' }}
</h4> </h4>
<i id="user_filter_icon" class="fa fa-search" <i id="user_filter_icon" class="fa fa-search"
@ -18,7 +17,14 @@
</button> </button>
</div> </div>
<div id="buddy_list_wrapper" class="scrolling_list" data-simplebar> <div id="buddy_list_wrapper" class="scrolling_list" data-simplebar>
<ul id="buddy-list-users-matching-view" class="filters" data-search-results-empty="{{t 'No matching users.' }}"></ul> <div id="buddy-list-users-matching-view-container">
<div class="buddy-list-subsection-header"></div>
<ul id="buddy-list-users-matching-view" class="buddy-list-section filters" data-search-results-empty="{{t 'None.' }}"></ul>
</div>
<div id="buddy-list-other-users-container">
<div class="buddy-list-subsection-header"></div>
<ul id="buddy-list-other-users" class="buddy-list-section filters" data-search-results-empty="{{t 'None.' }}"></ul>
</div>
<div id="buddy_list_wrapper_padding"></div> <div id="buddy_list_wrapper_padding"></div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,13 @@
const {strict: assert} = require("assert"); const {strict: assert} = require("assert");
const {
clear_buddy_list,
override_user_matches_narrow,
buddy_list_add_user_matching_view,
buddy_list_add_other_user,
stub_buddy_list_elements,
} = require("./lib/buddy_list");
const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace"); const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace");
const {run_test, noop} = require("./lib/test"); const {run_test, noop} = require("./lib/test");
const blueslip = require("./lib/zblueslip"); const blueslip = require("./lib/zblueslip");
@ -19,7 +26,6 @@ const _document = {
}; };
const channel = mock_esm("../src/channel"); const channel = mock_esm("../src/channel");
const compose_state = mock_esm("../src/compose_state");
const padded_widget = mock_esm("../src/padded_widget"); const padded_widget = mock_esm("../src/padded_widget");
const pm_list = mock_esm("../src/pm_list"); const pm_list = mock_esm("../src/pm_list");
const popovers = mock_esm("../src/popovers"); const popovers = mock_esm("../src/popovers");
@ -32,7 +38,6 @@ const watchdog = mock_esm("../src/watchdog");
set_global("document", _document); set_global("document", _document);
const huddle_data = zrequire("huddle_data"); const huddle_data = zrequire("huddle_data");
const compose_fade = zrequire("compose_fade");
const keydown_util = zrequire("keydown_util"); const keydown_util = zrequire("keydown_util");
const muted_users = zrequire("muted_users"); const muted_users = zrequire("muted_users");
const presence = zrequire("presence"); const presence = zrequire("presence");
@ -41,7 +46,11 @@ const buddy_data = zrequire("buddy_data");
const {buddy_list} = zrequire("buddy_list"); const {buddy_list} = zrequire("buddy_list");
const activity = zrequire("activity"); const activity = zrequire("activity");
const activity_ui = zrequire("activity_ui"); const activity_ui = zrequire("activity_ui");
const stream_data = zrequire("stream_data");
const narrow_state = zrequire("narrow_state");
const peer_data = zrequire("peer_data");
const util = zrequire("util"); const util = zrequire("util");
const {Filter} = zrequire("../src/filter");
const me = { const me = {
email: "me@zulip.com", email: "me@zulip.com",
@ -90,10 +99,13 @@ people.add_active_user(zoe);
people.add_active_user(me); people.add_active_user(me);
people.initialize_current_user(me.user_id); people.initialize_current_user(me.user_id);
function clear_buddy_list() { const $alice_stub = $.create("alice stub");
buddy_list.populate({ const $fred_stub = $.create("fred stub");
all_user_ids: [],
}); const rome_sub = {name: "Rome", subscribed: true, stream_id: 1001};
function add_sub_and_set_as_current_narrow(sub) {
stream_data.add_sub(sub);
narrow_state.set_current_filter(new Filter([{operator: "stream", operand: sub.name}]));
} }
function test(label, f) { function test(label, f) {
@ -109,6 +121,10 @@ function test(label, f) {
}); });
}); });
stub_buddy_list_elements();
helpers.override(buddy_list, "render_section_headers", noop);
helpers.override(buddy_list, "render_view_user_list_links", noop);
presence.presence_info.set(alice.user_id, {status: "active"}); presence.presence_info.set(alice.user_id, {status: "active"});
presence.presence_info.set(fred.user_id, {status: "active"}); presence.presence_info.set(fred.user_id, {status: "active"});
presence.presence_info.set(jill.user_id, {status: "active"}); presence.presence_info.set(jill.user_id, {status: "active"});
@ -117,7 +133,7 @@ function test(label, f) {
presence.presence_info.set(zoe.user_id, {status: "active"}); presence.presence_info.set(zoe.user_id, {status: "active"});
presence.presence_info.set(me.user_id, {status: "active"}); presence.presence_info.set(me.user_id, {status: "active"});
clear_buddy_list(); clear_buddy_list(buddy_list);
muted_users.set_muted_users([]); muted_users.set_muted_users([]);
activity.clear_for_testing(); activity.clear_for_testing();
@ -213,14 +229,12 @@ test("huddle_data.process_loaded_messages", () => {
test("presence_list_full_update", ({override, mock_template}) => { test("presence_list_full_update", ({override, mock_template}) => {
override(padded_widget, "update_padding", noop); override(padded_widget, "update_padding", noop);
let presence_rows = [];
mock_template("presence_rows.hbs", false, (data) => { mock_template("presence_rows.hbs", false, (data) => {
assert.equal(data.presence_rows.length, 7); presence_rows = [...presence_rows, ...data.presence_rows];
assert.equal(data.presence_rows[0].user_id, me.user_id);
}); });
$(".user-list-filter").trigger("focus"); $(".user-list-filter").trigger("focus");
compose_state.private_message_recipient = () => fred.email;
compose_fade.set_focused_recipient("private");
const user_ids = activity_ui.build_user_sidebar(); const user_ids = activity_ui.build_user_sidebar();
@ -233,6 +247,9 @@ test("presence_list_full_update", ({override, mock_template}) => {
zoe.user_id, zoe.user_id,
mark.user_id, mark.user_id,
]); ]);
assert.equal(presence_rows.length, 7);
assert.equal(presence_rows[0].user_id, me.user_id);
}); });
function simulate_right_column_buddy_list() { function simulate_right_column_buddy_list() {
@ -242,20 +259,11 @@ function simulate_right_column_buddy_list() {
}; };
} }
function buddy_list_add(user_id, $stub) {
if ($stub.attr) {
$stub.attr("data-user-id", user_id);
}
$stub.length = 1;
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
$("#buddy-list-users-matching-view").set_find_results(sel, $stub);
}
test("direct_message_update_dom_counts", () => { test("direct_message_update_dom_counts", () => {
const $count = $.create("alice-unread-count"); const $count = $.create("alice-unread-count");
const pm_key = alice.user_id.toString(); const pm_key = alice.user_id.toString();
const $li = $.create("alice stub"); const $li = $.create("alice stub");
buddy_list_add(pm_key, $li); buddy_list_add_user_matching_view(pm_key, $li);
$li.set_find_results(".unread_count", $count); $li.set_find_results(".unread_count", $count);
$count.set_parents_result("li", $li); $count.set_parents_result("li", $li);
@ -289,9 +297,22 @@ test("handlers", ({override, override_rewire, mock_template}) => {
// This is kind of weak coverage; we are mostly making sure that // This is kind of weak coverage; we are mostly making sure that
// keys and clicks got mapped to functions that don't crash. // keys and clicks got mapped to functions that don't crash.
let $me_li; const $me_li = $.create("me stub");
let $alice_li; const $alice_li = $.create("alice stub");
let $fred_li; const $fred_li = $.create("fred stub");
// Simulate a small window by having the
// fill_screen_with_content render the entire
// list in one pass. We will do more refined
// testing in the buddy_list node tests.
override(buddy_list, "fill_screen_with_content", () => {
buddy_list.render_more({
chunk_size: 100,
});
buddy_list_add_user_matching_view(me.user_id, $me_li);
buddy_list_add_user_matching_view(alice.user_id, $alice_li);
buddy_list_add_user_matching_view(fred.user_id, $fred_li);
});
let narrowed; let narrowed;
@ -302,24 +323,16 @@ test("handlers", ({override, override_rewire, mock_template}) => {
function init() { function init() {
$.clear_all_elements(); $.clear_all_elements();
buddy_list.populate({ stub_buddy_list_elements();
all_user_ids: [me.user_id, alice.user_id, fred.user_id],
});
buddy_list.start_scroll_handler = noop; buddy_list.start_scroll_handler = noop;
override_rewire(util, "call_function_periodically", noop); override_rewire(util, "call_function_periodically", noop);
override_rewire(activity, "send_presence_to_server", noop); override_rewire(activity, "send_presence_to_server", noop);
activity_ui.initialize({narrow_by_email}); activity_ui.initialize({narrow_by_email});
$("#buddy-list-users-matching-view").empty = noop; buddy_list.populate({
all_user_ids: [me.user_id, alice.user_id, fred.user_id],
$me_li = $.create("me stub"); });
$alice_li = $.create("alice stub");
$fred_li = $.create("fred stub");
buddy_list_add(me.user_id, $me_li);
buddy_list_add(alice.user_id, $alice_li);
buddy_list_add(fred.user_id, $fred_li);
} }
(function test_filter_keys() { (function test_filter_keys() {
@ -382,79 +395,80 @@ test("handlers", ({override, override_rewire, mock_template}) => {
})(); })();
}); });
test("first/prev/next", ({override, mock_template}) => { test("first/prev/next", ({override, override_rewire, mock_template}) => {
let rendered_alice; override_rewire(buddy_data, "user_matches_narrow", override_user_matches_narrow);
let rendered_fred; mock_template("presence_rows.hbs", false, noop);
user_settings.user_list_style = 2;
mock_template("presence_row.hbs", false, (data) => {
switch (data.user_id) {
case alice.user_id:
rendered_alice = true;
assert.deepEqual(data, {
faded: true,
href: "#narrow/dm/1-Alice-Smith",
is_current_user: false,
name: "Alice Smith",
num_unread: 0,
user_circle_class: "user_circle_green",
user_id: alice.user_id,
status_emoji_info: undefined,
status_text: undefined,
user_list_style: {
COMPACT: false,
WITH_STATUS: true,
WITH_AVATAR: false,
},
should_add_guest_user_indicator: false,
});
break;
case fred.user_id:
rendered_fred = true;
assert.deepEqual(data, {
href: "#narrow/dm/2-Fred-Flintstone",
name: "Fred Flintstone",
user_id: fred.user_id,
is_current_user: false,
num_unread: 0,
user_circle_class: "user_circle_green",
faded: false,
status_emoji_info: undefined,
status_text: undefined,
user_list_style: {
COMPACT: false,
WITH_STATUS: true,
WITH_AVATAR: false,
},
should_add_guest_user_indicator: false,
});
break;
/* istanbul ignore next */
default:
throw new Error(`we did not expect to have to render a row for ${data.name}`);
}
});
override(padded_widget, "update_padding", noop); override(padded_widget, "update_padding", noop);
stub_buddy_list_elements();
// empty list
assert.equal(buddy_list.first_key(), undefined); assert.equal(buddy_list.first_key(), undefined);
blueslip.reset();
blueslip.expect("error", "Couldn't find key in buddy list");
assert.equal(buddy_list.prev_key(alice.user_id), undefined);
blueslip.reset();
blueslip.expect("error", "Couldn't find key in buddy list");
assert.equal(buddy_list.next_key(alice.user_id), undefined);
blueslip.reset();
// one user matching the view
clear_buddy_list(buddy_list);
buddy_list_add_user_matching_view(alice.user_id, $alice_stub);
buddy_list.populate({
all_user_ids: [alice.user_id],
});
assert.equal(buddy_list.first_key(), alice.user_id);
assert.equal(buddy_list.prev_key(alice.user_id), undefined); assert.equal(buddy_list.prev_key(alice.user_id), undefined);
assert.equal(buddy_list.next_key(alice.user_id), undefined); assert.equal(buddy_list.next_key(alice.user_id), undefined);
override(buddy_list.$container, "append", noop); // two users matching the view
clear_buddy_list(buddy_list);
activity_ui.redraw_user(alice.user_id); buddy_list_add_user_matching_view(alice.user_id, $alice_stub);
activity_ui.redraw_user(fred.user_id); buddy_list_add_user_matching_view(fred.user_id, $fred_stub);
buddy_list.populate({
all_user_ids: [alice.user_id, fred.user_id],
});
assert.equal(buddy_list.first_key(), alice.user_id); assert.equal(buddy_list.first_key(), alice.user_id);
assert.equal(buddy_list.prev_key(alice.user_id), undefined); assert.equal(buddy_list.prev_key(alice.user_id), undefined);
assert.equal(buddy_list.prev_key(fred.user_id), alice.user_id); assert.equal(buddy_list.prev_key(fred.user_id), alice.user_id);
assert.equal(buddy_list.next_key(alice.user_id), fred.user_id); assert.equal(buddy_list.next_key(alice.user_id), fred.user_id);
assert.equal(buddy_list.next_key(fred.user_id), undefined); assert.equal(buddy_list.next_key(fred.user_id), undefined);
assert.ok(rendered_alice); // one other user
assert.ok(rendered_fred); clear_buddy_list(buddy_list);
buddy_list_add_other_user(fred.user_id, $fred_stub);
buddy_list.populate({
all_user_ids: [fred.user_id],
});
assert.equal(buddy_list.first_key(), fred.user_id);
assert.equal(buddy_list.prev_key(fred.user_id), undefined);
assert.equal(buddy_list.next_key(fred.user_id), undefined);
// two other users
clear_buddy_list(buddy_list);
buddy_list_add_other_user(alice.user_id, $alice_stub);
buddy_list_add_other_user(fred.user_id, $fred_stub);
buddy_list.populate({
all_user_ids: [alice.user_id, fred.user_id],
});
assert.equal(buddy_list.first_key(), alice.user_id);
assert.equal(buddy_list.prev_key(alice.user_id), undefined);
assert.equal(buddy_list.prev_key(fred.user_id), alice.user_id);
assert.equal(buddy_list.next_key(alice.user_id), fred.user_id);
assert.equal(buddy_list.next_key(fred.user_id), undefined);
// one user matching the view, and one other user
clear_buddy_list(buddy_list);
buddy_list_add_user_matching_view(alice.user_id, $alice_stub);
buddy_list_add_other_user(alice.user_id, $fred_stub);
buddy_list.populate({
all_user_ids: [alice.user_id, fred.user_id],
});
assert.equal(buddy_list.first_key(), alice.user_id);
assert.equal(buddy_list.prev_key(alice.user_id), undefined);
assert.equal(buddy_list.prev_key(fred.user_id), alice.user_id);
assert.equal(buddy_list.next_key(alice.user_id), fred.user_id);
assert.equal(buddy_list.next_key(fred.user_id), undefined);
}); });
test("render_empty_user_list_message", ({override, mock_template}) => { test("render_empty_user_list_message", ({override, mock_template}) => {
@ -481,15 +495,17 @@ test("render_empty_user_list_message", ({override, mock_template}) => {
test("insert_one_user_into_empty_list", ({override, mock_template}) => { test("insert_one_user_into_empty_list", ({override, mock_template}) => {
user_settings.user_list_style = 2; user_settings.user_list_style = 2;
override(padded_widget, "update_padding", noop);
mock_template("presence_row.hbs", true, (data, html) => { mock_template("presence_row.hbs", true, (data, html) => {
assert.deepEqual(data, { assert.deepEqual(data, {
faded: false,
href: "#narrow/dm/1-Alice-Smith", href: "#narrow/dm/1-Alice-Smith",
name: "Alice Smith", name: "Alice Smith",
user_id: 1, user_id: 1,
is_current_user: false, is_current_user: false,
num_unread: 0, num_unread: 0,
user_circle_class: "user_circle_green", user_circle_class: "user_circle_green",
faded: true,
status_emoji_info: undefined, status_emoji_info: undefined,
status_text: undefined, status_text: undefined,
user_list_style: { user_list_style: {
@ -503,51 +519,69 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => {
return html; return html;
}); });
override(padded_widget, "update_padding", noop); let users_matching_view_appended_html;
override(buddy_list.$users_matching_view_container, "append", (html) => {
let appended_html; users_matching_view_appended_html = html;
override(buddy_list.$container, "append", (html) => { });
appended_html = html; let other_users_appended_html;
override(buddy_list.$other_users_container, "append", (html) => {
other_users_appended_html = html;
}); });
add_sub_and_set_as_current_narrow(rome_sub);
buddy_list_add_user_matching_view(alice.user_id, $alice_stub);
peer_data.set_subscribers(rome_sub.stream_id, [alice.user_id]);
activity_ui.redraw_user(alice.user_id); activity_ui.redraw_user(alice.user_id);
assert.ok(appended_html.indexOf('data-user-id="1"') > 0); assert.ok(users_matching_view_appended_html.indexOf('data-user-id="1"') > 0);
assert.ok(appended_html.indexOf("user_circle_green") > 0); assert.ok(users_matching_view_appended_html.indexOf("user_circle_green") > 0);
clear_buddy_list(buddy_list);
buddy_list_add_other_user(alice.user_id, $alice_stub);
peer_data.set_subscribers(rome_sub.stream_id, []);
activity_ui.redraw_user(alice.user_id);
assert.ok(other_users_appended_html.indexOf('data-user-id="1"') > 0);
assert.ok(other_users_appended_html.indexOf("user_circle_green") > 0);
}); });
test("insert_alice_then_fred", ({override, mock_template}) => { test("insert_alice_then_fred", ({override, mock_template}) => {
mock_template("presence_row.hbs", true, (_data, html) => html); mock_template("presence_row.hbs", true, (_data, html) => html);
let appended_html; let other_users_appended_html;
override(buddy_list.$container, "append", (html) => { override(buddy_list.$other_users_container, "append", (html) => {
appended_html = html; other_users_appended_html = html;
}); });
override(padded_widget, "update_padding", noop); override(padded_widget, "update_padding", noop);
activity_ui.redraw_user(alice.user_id); activity_ui.redraw_user(alice.user_id);
assert.ok(appended_html.indexOf('data-user-id="1"') > 0); assert.ok(other_users_appended_html.indexOf('data-user-id="1"') > 0);
assert.ok(appended_html.indexOf("user_circle_green") > 0); assert.ok(other_users_appended_html.indexOf("user_circle_green") > 0);
activity_ui.redraw_user(fred.user_id); activity_ui.redraw_user(fred.user_id);
assert.ok(appended_html.indexOf('data-user-id="2"') > 0); assert.ok(other_users_appended_html.indexOf('data-user-id="2"') > 0);
assert.ok(appended_html.indexOf("user_circle_green") > 0); assert.ok(other_users_appended_html.indexOf("user_circle_green") > 0);
}); });
test("insert_fred_then_alice_then_rename", ({override, mock_template}) => { test("insert_fred_then_alice_then_rename, both as users matching view", ({
override,
mock_template,
}) => {
mock_template("presence_row.hbs", true, (_data, html) => html); mock_template("presence_row.hbs", true, (_data, html) => html);
let appended_html; add_sub_and_set_as_current_narrow(rome_sub);
override(buddy_list.$container, "append", (html) => { peer_data.set_subscribers(rome_sub.stream_id, [alice.user_id, fred.user_id]);
appended_html = html;
let users_matching_view_appended_html;
override(buddy_list.$users_matching_view_container, "append", (html) => {
users_matching_view_appended_html = html;
}); });
override(padded_widget, "update_padding", noop); override(padded_widget, "update_padding", noop);
buddy_list_add_user_matching_view(alice.user_id, $alice_stub);
buddy_list_add_user_matching_view(fred.user_id, $fred_stub);
activity_ui.redraw_user(fred.user_id); activity_ui.redraw_user(fred.user_id);
assert.ok(appended_html.indexOf('data-user-id="2"') > 0); assert.ok(users_matching_view_appended_html.indexOf('data-user-id="2"') > 0);
assert.ok(appended_html.indexOf("user_circle_green") > 0); assert.ok(users_matching_view_appended_html.indexOf("user_circle_green") > 0);
const $fred_stub = $.create("fred-first");
buddy_list_add(fred.user_id, $fred_stub);
let inserted_html; let inserted_html;
$fred_stub.before = (html) => { $fred_stub.before = (html) => {
@ -571,8 +605,58 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => {
}; };
people.add_active_user(fred_with_new_name); people.add_active_user(fred_with_new_name);
const $alice_stub = $.create("alice-first"); $alice_stub.before = (html) => {
buddy_list_add(alice.user_id, $alice_stub); inserted_html = html;
};
activity_ui.redraw_user(fred_with_new_name.user_id);
assert.ok(fred_removed);
assert.ok(users_matching_view_appended_html.indexOf('data-user-id="2"') > 0);
// restore old Fred data
people.add_active_user(fred);
});
test("insert_fred_then_alice_then_rename, both as other users", ({override, mock_template}) => {
mock_template("presence_row.hbs", true, (_data, html) => html);
add_sub_and_set_as_current_narrow(rome_sub);
peer_data.set_subscribers(rome_sub.stream_id, []);
let other_users_appended_html;
override(buddy_list.$other_users_container, "append", (html) => {
other_users_appended_html = html;
});
override(padded_widget, "update_padding", noop);
buddy_list_add_other_user(alice.user_id, $alice_stub);
buddy_list_add_other_user(fred.user_id, $fred_stub);
activity_ui.redraw_user(fred.user_id);
assert.ok(other_users_appended_html.indexOf('data-user-id="2"') > 0);
assert.ok(other_users_appended_html.indexOf("user_circle_green") > 0);
let inserted_html;
$fred_stub.before = (html) => {
inserted_html = html;
};
let fred_removed;
$fred_stub.remove = () => {
fred_removed = true;
};
activity_ui.redraw_user(alice.user_id);
assert.ok(inserted_html.indexOf('data-user-id="1"') > 0);
assert.ok(inserted_html.indexOf("user_circle_green") > 0);
// Next rename fred to Aaron.
const fred_with_new_name = {
email: fred.email,
user_id: fred.user_id,
full_name: "Aaron",
};
people.add_active_user(fred_with_new_name);
$alice_stub.before = (html) => { $alice_stub.before = (html) => {
inserted_html = html; inserted_html = html;
@ -580,7 +664,7 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => {
activity_ui.redraw_user(fred_with_new_name.user_id); activity_ui.redraw_user(fred_with_new_name.user_id);
assert.ok(fred_removed); assert.ok(fred_removed);
assert.ok(appended_html.indexOf('data-user-id="2"') > 0); assert.ok(other_users_appended_html.indexOf('data-user-id="2"') > 0);
// restore old Fred data // restore old Fred data
people.add_active_user(fred); people.add_active_user(fred);
@ -624,7 +708,7 @@ test("update_presence_info", ({override}) => {
}; };
const $alice_li = $.create("alice stub"); const $alice_li = $.create("alice stub");
buddy_list_add(alice.user_id, $alice_li); buddy_list_add_user_matching_view(alice.user_id, $alice_li);
let inserted; let inserted;
override(buddy_list, "insert_or_move", () => { override(buddy_list, "insert_or_move", () => {
@ -661,10 +745,9 @@ test("update_presence_info", ({override}) => {
}); });
test("initialize", ({override, mock_template}) => { test("initialize", ({override, mock_template}) => {
mock_template("presence_rows.hbs", false, noop);
override(padded_widget, "update_padding", noop);
override(pm_list, "update_private_messages", noop); override(pm_list, "update_private_messages", noop);
override(watchdog, "check_for_unsuspend", noop); override(watchdog, "check_for_unsuspend", noop);
override(buddy_list, "fill_screen_with_content", noop);
let payload; let payload;
override(channel, "post", (arg) => { override(channel, "post", (arg) => {
@ -677,9 +760,13 @@ test("initialize", ({override, mock_template}) => {
function clear() { function clear() {
$.clear_all_elements(); $.clear_all_elements();
buddy_list.$container = $("#buddy-list-users-matching-view"); buddy_list.$users_matching_view_container = $("#buddy-list-users-matching-view");
buddy_list.$container.append = noop; buddy_list.$users_matching_view_container.append = noop;
clear_buddy_list(); buddy_list.$other_users_container = $("#buddy-list-other-users");
buddy_list.$other_users_container.append = noop;
stub_buddy_list_elements();
mock_template("empty_list_widget_for_list.hbs", false, noop);
clear_buddy_list(buddy_list);
page_params.presences = {}; page_params.presences = {};
} }

View File

@ -13,6 +13,7 @@ const timerender = mock_esm("../src/timerender");
const compose_fade_helper = zrequire("compose_fade_helper"); const compose_fade_helper = zrequire("compose_fade_helper");
const muted_users = zrequire("muted_users"); const muted_users = zrequire("muted_users");
const narrow_state = zrequire("narrow_state");
const peer_data = zrequire("peer_data"); const peer_data = zrequire("peer_data");
const people = zrequire("people"); const people = zrequire("people");
const presence = zrequire("presence"); const presence = zrequire("presence");
@ -421,6 +422,12 @@ test("always show me", () => {
assert.deepEqual(buddy_data.get_filtered_and_sorted_user_ids(""), [me.user_id]); assert.deepEqual(buddy_data.get_filtered_and_sorted_user_ids(""), [me.user_id]);
}); });
test("always show pm users", ({override_rewire}) => {
people.add_active_user(selma);
override_rewire(narrow_state, "pm_ids_set", () => new Set([selma.user_id]));
assert.deepEqual(buddy_data.get_filtered_and_sorted_user_ids(""), [me.user_id, selma.user_id]);
});
test("level", () => { test("level", () => {
realm.server_presence_offline_threshold_seconds = 200; realm.server_presence_offline_threshold_seconds = 200;
@ -450,6 +457,58 @@ test("level", () => {
assert.equal(buddy_data.level(selma.user_id), 3); assert.equal(buddy_data.level(selma.user_id), 3);
}); });
test("compare_function", () => {
const first_user_shown_higher = -1;
const second_user_shown_higher = 1;
const stream_id = 1001;
const sub = {name: "Rome", subscribed: true, stream_id};
stream_data.add_sub(sub);
people.add_active_user(alice);
people.add_active_user(fred);
// Alice is higher because of alphabetical sorting.
peer_data.set_subscribers(stream_id, []);
assert.equal(
second_user_shown_higher,
buddy_data.compare_function(fred.user_id, alice.user_id, sub, new Set()),
);
// Fred is higher because they're in the narrow and Alice isn't.
peer_data.set_subscribers(stream_id, [fred.user_id]);
assert.equal(
first_user_shown_higher,
buddy_data.compare_function(fred.user_id, alice.user_id, sub, new Set()),
);
assert.equal(
second_user_shown_higher,
buddy_data.compare_function(alice.user_id, fred.user_id, sub, new Set()),
);
// Fred is higher because they're in the DM conversation and Alice isn't.
assert.equal(
first_user_shown_higher,
buddy_data.compare_function(
fred.user_id,
alice.user_id,
undefined,
new Set([fred.user_id]),
),
);
// Alice is higher because of alphabetical sorting.
assert.equal(
second_user_shown_higher,
buddy_data.compare_function(fred.user_id, alice.user_id, undefined, new Set()),
);
// The user is part of a DM conversation, though that's not explicitly in the DM list.
assert.equal(
first_user_shown_higher,
buddy_data.compare_function(me.user_id, alice.user_id, undefined, new Set([fred.user_id])),
);
});
test("user_last_seen_time_status", ({override}) => { test("user_last_seen_time_status", ({override}) => {
set_presence(selma.user_id, "active"); set_presence(selma.user_id, "active");
set_presence(me.user_id, "active"); set_presence(me.user_id, "active");

View File

@ -4,6 +4,13 @@ const {strict: assert} = require("assert");
const _ = require("lodash"); const _ = require("lodash");
const {
clear_buddy_list,
override_user_matches_narrow,
buddy_list_add_user_matching_view,
buddy_list_add_other_user,
stub_buddy_list_elements,
} = require("./lib/buddy_list");
const {mock_esm, zrequire} = require("./lib/namespace"); const {mock_esm, zrequire} = require("./lib/namespace");
const {run_test, noop} = require("./lib/test"); const {run_test, noop} = require("./lib/test");
const blueslip = require("./lib/zblueslip"); const blueslip = require("./lib/zblueslip");
@ -12,8 +19,9 @@ const $ = require("./lib/zjquery");
const padded_widget = mock_esm("../src/padded_widget"); const padded_widget = mock_esm("../src/padded_widget");
const message_viewport = mock_esm("../src/message_viewport"); const message_viewport = mock_esm("../src/message_viewport");
const people = zrequire("people"); const buddy_data = zrequire("buddy_data");
const {BuddyList} = zrequire("buddy_list"); const {BuddyList} = zrequire("buddy_list");
const people = zrequire("people");
function init_simulated_scrolling() { function init_simulated_scrolling() {
const elem = { const elem = {
@ -35,54 +43,43 @@ const alice = {
full_name: "Alice Smith", full_name: "Alice Smith",
}; };
people.add_active_user(alice); people.add_active_user(alice);
const bob = {
email: "bob@zulip.com",
user_id: 15,
full_name: "Bob Smith",
};
people.add_active_user(bob);
const chris = {
email: "chris@zulip.com",
user_id: 20,
full_name: "Chris Smith",
};
people.add_active_user(chris);
const $alice_li = $.create("alice-stub");
const $bob_li = $.create("bob-stub");
run_test("get_items", () => { run_test("basics", ({override, mock_template}) => {
const buddy_list = new BuddyList();
// We don't make $alice_li an actual jQuery stub,
// because our test only cares that it comes
// back from get_items.
const $alice_li = "alice stub";
const sel = "li.user_sidebar_entry";
const $container = $.create("get_items container", {
children: [{to_$: () => $alice_li}],
});
buddy_list.$container.set_find_results(sel, $container);
const items = buddy_list.get_items();
assert.deepEqual(items, [$alice_li]);
});
run_test("basics", ({override}) => {
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
init_simulated_scrolling(); init_simulated_scrolling();
override(buddy_list, "get_data_from_user_ids", (user_ids) => { override(buddy_list, "items_to_html", () => "html-stub");
assert.deepEqual(user_ids, [alice.user_id]);
return "data-stub";
});
override(buddy_list, "items_to_html", (opts) => {
const items = opts.items;
assert.equal(items, "data-stub");
return "html-stub";
});
override(message_viewport, "height", () => 550); override(message_viewport, "height", () => 550);
override(padded_widget, "update_padding", noop); override(padded_widget, "update_padding", noop);
stub_buddy_list_elements();
mock_template("buddy_list/view_all_users.hbs", false, noop);
let appended; let appended_to_users_matching_view;
$("#buddy-list-users-matching-view").append = (html) => { $("#buddy-list-users-matching-view").append = (html) => {
assert.equal(html, "html-stub"); assert.equal(html, "html-stub");
appended = true; appended_to_users_matching_view = true;
}; };
buddy_list.populate({ buddy_list.populate({
all_user_ids: [alice.user_id], all_user_ids: [alice.user_id],
}); });
assert.ok(appended); assert.ok(appended_to_users_matching_view);
const $alice_li = {length: 1}; const $alice_li = "alice-stub";
override(buddy_list, "get_li_from_user_id", (opts) => { override(buddy_list, "get_li_from_user_id", (opts) => {
const user_id = opts.user_id; const user_id = opts.user_id;
@ -97,14 +94,98 @@ run_test("basics", ({override}) => {
assert.equal($li, $alice_li); assert.equal($li, $alice_li);
}); });
run_test("big_list", ({override}) => { run_test("split list", ({override, override_rewire, mock_template}) => {
const buddy_list = new BuddyList();
init_simulated_scrolling();
stub_buddy_list_elements();
mock_template("buddy_list/section_header.hbs", false, noop);
mock_template("buddy_list/view_all_users.hbs", false, noop);
override_rewire(buddy_data, "user_matches_narrow", override_user_matches_narrow);
override(buddy_list, "items_to_html", (opts) => {
if (opts.items.length > 0) {
return "html-stub";
}
return "empty-list";
});
override(message_viewport, "height", () => 550);
override(padded_widget, "update_padding", noop);
let appended_to_users_matching_view = false;
$("#buddy-list-users-matching-view").append = (html) => {
if (html === "html-stub") {
appended_to_users_matching_view = true;
}
};
let appended_to_other_users = false;
$("#buddy-list-other-users").append = (html) => {
if (html === "html-stub") {
appended_to_other_users = true;
}
};
// one user matching the view
buddy_list_add_user_matching_view(alice.user_id, $alice_li);
buddy_list.populate({
all_user_ids: [alice.user_id],
});
assert.ok(appended_to_users_matching_view);
assert.ok(!appended_to_other_users);
appended_to_users_matching_view = false;
// one other user
clear_buddy_list(buddy_list);
buddy_list_add_other_user(alice.user_id, $alice_li);
buddy_list.populate({
all_user_ids: [alice.user_id],
});
assert.ok(!appended_to_users_matching_view);
assert.ok(appended_to_other_users);
appended_to_other_users = false;
// a user matching the view, and an other user
clear_buddy_list(buddy_list);
buddy_list_add_user_matching_view(alice.user_id, $alice_li);
buddy_list_add_other_user(bob.user_id, $bob_li);
buddy_list.populate({
all_user_ids: [alice.user_id, bob.user_id],
});
assert.ok(appended_to_users_matching_view);
assert.ok(appended_to_other_users);
});
run_test("find_li", ({override, mock_template}) => {
const buddy_list = new BuddyList();
override(buddy_list, "fill_screen_with_content", noop);
mock_template("buddy_list/view_all_users.hbs", false, noop);
stub_buddy_list_elements();
clear_buddy_list(buddy_list);
buddy_list_add_user_matching_view(alice.user_id, $alice_li);
buddy_list_add_other_user(bob.user_id, $bob_li);
let $li = buddy_list.find_li({
key: alice.user_id,
});
assert.equal($li, $alice_li);
$li = buddy_list.find_li({
key: bob.user_id,
});
assert.equal($li, $bob_li);
});
run_test("fill_screen_with_content early break on big list", ({override, mock_template}) => {
stub_buddy_list_elements();
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
const elem = init_simulated_scrolling(); const elem = init_simulated_scrolling();
stub_buddy_list_elements();
mock_template("buddy_list/view_all_users.hbs", false, noop);
// Don't actually render, but do simulate filling up
// the screen.
let chunks_inserted = 0; let chunks_inserted = 0;
override(buddy_list, "render_more", () => { override(buddy_list, "render_more", () => {
elem.scrollHeight += 100; elem.scrollHeight += 100;
chunks_inserted += 1; chunks_inserted += 1;
@ -112,7 +193,9 @@ run_test("big_list", ({override}) => {
override(message_viewport, "height", () => 550); override(message_viewport, "height", () => 550);
// We will have more than enough users, but still // We will have more than enough users, but still
// only do 6 chunks of data. // only do 6 chunks of data (20 users per chunk)
// because of exiting early from fill_screen_with_content
// because of not scrolling enough to fetch more users.
const num_users = 300; const num_users = 300;
const user_ids = []; const user_ids = [];
@ -130,9 +213,72 @@ run_test("big_list", ({override}) => {
all_user_ids: user_ids, all_user_ids: user_ids,
}); });
// Only 6 chunks, even though that's 120 users instead of the full 300.
assert.equal(chunks_inserted, 6); assert.equal(chunks_inserted, 6);
}); });
run_test("big_list", ({override, override_rewire, mock_template}) => {
const buddy_list = new BuddyList();
init_simulated_scrolling();
stub_buddy_list_elements();
override(padded_widget, "update_padding", noop);
override(message_viewport, "height", () => 550);
override_rewire(buddy_data, "user_matches_narrow", override_user_matches_narrow);
mock_template("empty_list_widget_for_list.hbs", false, noop);
mock_template("buddy_list/section_header.hbs", false, noop);
mock_template("buddy_list/view_all_users.hbs", false, noop);
let items_to_html_call_count = 0;
override(buddy_list, "items_to_html", () => {
items_to_html_call_count += 1;
return "html-stub";
});
const num_users = 300;
const user_ids = [];
// This isn't a great way of testing this, but this is here for
// the sake of code coverage. Essentially, for a very long list,
// these buddy list sections can collect empty messages in the middle
// of populating (i.e. once a chunk is rendered) which later might need
// to be removed to add users from future chunks.
//
// For example: chunk1 populates only users in the list of users matching,
// the view and the empty list says "None", but chunk2 adds users to the
// other list so the "None" message should be removed.
//
// Here we're just saying both lists are rendered as empty from start,
// which doesn't actually happen, since I don't know how to properly
// get it set in the middle of buddy_list.populate().
$("#buddy-list-users-matching-view .empty-list-message").length = 1;
$("#buddy-list-other-users .empty-list-message").length = 1;
_.times(num_users, (i) => {
const person = {
email: "foo" + i + "@zulip.com",
user_id: 100 + i,
full_name: "Somebody " + i,
};
people.add_active_user(person);
if (i < 100 || i % 2 === 0) {
buddy_list_add_user_matching_view(person.user_id, $.create("stub" + i));
} else {
buddy_list_add_other_user(person.user_id, $.create("stub" + i));
}
user_ids.push(person.user_id);
});
buddy_list.populate({
all_user_ids: user_ids,
});
// Chunks are default size 20, so there should be 300/20 = 15 chunks
const expected_chunks_inserted = 15;
// Two calls per chunk: one for users_matching_view and one for other_users.
assert.equal(items_to_html_call_count, 2 * expected_chunks_inserted);
});
run_test("force_render", ({override}) => { run_test("force_render", ({override}) => {
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
buddy_list.render_count = 50; buddy_list.render_count = 50;
@ -159,10 +305,10 @@ run_test("find_li w/force_render", ({override}) => {
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
// If we call find_li w/force_render set, and the // If we call find_li w/force_render set, and the
// key is not already rendered in DOM, then the // user_id is not already rendered in DOM, then the
// widget will call show_key to force-render it. // widget will force-render it.
const user_id = "999"; const user_id = "999";
const $stub_li = {length: 0}; const $stub_li = "stub-li";
override(buddy_list, "get_li_from_user_id", (opts) => { override(buddy_list, "get_li_from_user_id", (opts) => {
assert.equal(opts.user_id, user_id); assert.equal(opts.user_id, user_id);
@ -178,10 +324,10 @@ run_test("find_li w/force_render", ({override}) => {
shown = true; shown = true;
}); });
const $empty_li = buddy_list.find_li({ const $hidden_li = buddy_list.find_li({
key: user_id, key: user_id,
}); });
assert.equal($empty_li, $stub_li); assert.equal($hidden_li, $stub_li);
assert.ok(!shown); assert.ok(!shown);
const $li = buddy_list.find_li({ const $li = buddy_list.find_li({
@ -195,7 +341,7 @@ run_test("find_li w/force_render", ({override}) => {
run_test("find_li w/bad key", ({override}) => { run_test("find_li w/bad key", ({override}) => {
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
override(buddy_list, "get_li_from_user_id", () => ({length: 0})); override(buddy_list, "get_li_from_user_id", () => "stub-li");
const $undefined_li = buddy_list.find_li({ const $undefined_li = buddy_list.find_li({
key: "not-there", key: "not-there",
@ -205,23 +351,20 @@ run_test("find_li w/bad key", ({override}) => {
assert.deepEqual($undefined_li, []); assert.deepEqual($undefined_li, []);
}); });
run_test("scrolling", ({override}) => { run_test("scrolling", ({override, mock_template}) => {
const buddy_list = new BuddyList(); const buddy_list = new BuddyList();
init_simulated_scrolling();
override(message_viewport, "height", () => 550);
buddy_list.populate({
all_user_ids: [],
});
let tried_to_fill; let tried_to_fill;
override(buddy_list, "fill_screen_with_content", () => { override(buddy_list, "fill_screen_with_content", () => {
tried_to_fill = true; tried_to_fill = true;
}); });
mock_template("buddy_list/view_all_users.hbs", false, noop);
stub_buddy_list_elements();
init_simulated_scrolling();
stub_buddy_list_elements();
assert.ok(!tried_to_fill); clear_buddy_list(buddy_list);
assert.ok(tried_to_fill);
tried_to_fill = false;
buddy_list.start_scroll_handler(); buddy_list.start_scroll_handler();
$(buddy_list.scroll_container_selector).trigger("scroll"); $(buddy_list.scroll_container_selector).trigger("scroll");

View File

@ -0,0 +1,47 @@
"use strict";
const {noop} = require("./test");
const $ = require("./zjquery");
let users_matching_view = [];
exports.buddy_list_add_user_matching_view = (user_id, $stub) => {
if ($stub.attr) {
$stub.attr("data-user-id", user_id);
}
$stub.length = 1;
users_matching_view.push(user_id);
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
$("#buddy_list_wrapper").set_find_results(sel, $stub);
};
let other_users = [];
exports.buddy_list_add_other_user = (user_id, $stub) => {
if ($stub.attr) {
$stub.attr("data-user-id", user_id);
}
$stub.length = 1;
other_users.push(user_id);
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
$("#buddy_list_wrapper").set_find_results(sel, $stub);
};
exports.override_user_matches_narrow = (user_id) => users_matching_view.includes(user_id);
exports.clear_buddy_list = (buddy_list) => {
buddy_list.populate({
all_user_ids: [],
});
users_matching_view = [];
other_users = [];
};
exports.stub_buddy_list_elements = () => {
// Set to an empty list since we're not testing CSS.
$("#buddy-list-users-matching-view").children = () => [];
$("#buddy-list-other-users").children = () => [];
$("#buddy-list-users-matching-view .empty-list-message").length = 0;
$("#buddy-list-other-users .empty-list-message").length = 0;
$("#buddy-list-other-users-container .view-all-users-link").length = 0;
$("#buddy-list-users-matching-view-container .view-all-subscribers-link").remove = noop;
$("#buddy-list-other-users-container .view-all-users-link").remove = noop;
};

View File

@ -72,9 +72,12 @@ mock_esm("../src/user_topics", {
is_topic_muted: () => false, is_topic_muted: () => false,
}); });
const {buddy_list} = zrequire("buddy_list");
const activity_ui = zrequire("activity_ui");
const narrow_state = zrequire("narrow_state"); const narrow_state = zrequire("narrow_state");
const stream_data = zrequire("stream_data"); const stream_data = zrequire("stream_data");
const narrow = zrequire("narrow"); const narrow = zrequire("narrow");
const people = zrequire("people");
const denmark = { const denmark = {
subscribed: false, subscribed: false,
@ -156,6 +159,16 @@ function stub_message_list() {
run_test("basics", ({override}) => { run_test("basics", ({override}) => {
stub_message_list(); stub_message_list();
activity_ui.set_cursor_and_filter();
const me = {
email: "me@zulip.com",
user_id: 999,
full_name: "Me Myself",
};
people.add_active_user(me);
people.initialize_current_user(me.user_id);
override(buddy_list, "populate", noop);
const helper = test_helper({override}); const helper = test_helper({override});
const terms = [{operator: "stream", operand: "Denmark"}]; const terms = [{operator: "stream", operand: "Denmark"}];

View File

@ -189,6 +189,8 @@ dropdown_widget.DropdownWidget = function DropdownWidget() {
}; };
const {all_messages_data} = zrequire("all_messages_data"); const {all_messages_data} = zrequire("all_messages_data");
const {buddy_list} = zrequire("buddy_list");
const activity_ui = zrequire("activity_ui");
const people = zrequire("people"); const people = zrequire("people");
const rt = zrequire("recent_view_ui"); const rt = zrequire("recent_view_ui");
const recent_view_util = zrequire("recent_view_util"); const recent_view_util = zrequire("recent_view_util");
@ -444,7 +446,7 @@ function test(label, f) {
}); });
} }
test("test_recent_view_show", ({mock_template}) => { test("test_recent_view_show", ({override, mock_template}) => {
// Note: unread count and urls are fake, // Note: unread count and urls are fake,
// since they are generated in external libraries // since they are generated in external libraries
// and are not to be tested here. // and are not to be tested here.
@ -458,6 +460,8 @@ test("test_recent_view_show", ({mock_template}) => {
is_spectator: false, is_spectator: false,
}; };
activity_ui.set_cursor_and_filter();
mock_template("recent_view_table.hbs", false, (data) => { mock_template("recent_view_table.hbs", false, (data) => {
assert.deepEqual(data, expected); assert.deepEqual(data, expected);
return "<recent_view table stub>"; return "<recent_view table stub>";
@ -465,6 +469,11 @@ test("test_recent_view_show", ({mock_template}) => {
mock_template("recent_view_row.hbs", false, noop); mock_template("recent_view_row.hbs", false, noop);
let buddy_list_populated = false;
override(buddy_list, "populate", () => {
buddy_list_populated = true;
});
stub_out_filter_buttons(); stub_out_filter_buttons();
// We don't test the css calls; we just skip over them. // We don't test the css calls; we just skip over them.
$("#mark_read_on_scroll_state_banner").toggleClass = noop; $("#mark_read_on_scroll_state_banner").toggleClass = noop;
@ -474,6 +483,8 @@ test("test_recent_view_show", ({mock_template}) => {
rt.show(); rt.show();
assert.ok(buddy_list_populated);
// incorrect topic_key // incorrect topic_key
assert.equal(rt.inplace_rerender("stream_unknown:topic_unknown"), false); assert.equal(rt.inplace_rerender("stream_unknown:topic_unknown"), false);
}); });

View File

@ -9,7 +9,10 @@ const {realm} = require("./lib/zpage_params");
const fake_buddy_list = { const fake_buddy_list = {
scroll_container_selector: "#whatever", scroll_container_selector: "#whatever",
$container: { $users_matching_view_container: {
data() {},
},
$other_users_container: {
data() {}, data() {},
}, },
find_li() {}, find_li() {},
@ -58,7 +61,7 @@ const all_user_ids = [alice.user_id, fred.user_id, jill.user_id, me.user_id];
const ordered_user_ids = [me.user_id, alice.user_id, fred.user_id, jill.user_id]; const ordered_user_ids = [me.user_id, alice.user_id, fred.user_id, jill.user_id];
function test(label, f) { function test(label, f) {
run_test(label, ({override}) => { run_test(label, (opts) => {
people.init(); people.init();
people.add_active_user(alice); people.add_active_user(alice);
people.add_active_user(fred); people.add_active_user(fred);
@ -67,7 +70,7 @@ function test(label, f) {
people.initialize_current_user(me.user_id); people.initialize_current_user(me.user_id);
muted_users.set_muted_users([]); muted_users.set_muted_users([]);
activity_ui.set_cursor_and_filter(); activity_ui.set_cursor_and_filter();
f({override}); f(opts);
}); });
} }
@ -76,12 +79,19 @@ function set_input_val(val) {
$(".user-list-filter").trigger("input"); $(".user-list-filter").trigger("input");
} }
function stub_buddy_list_empty_list_message_lengths() {
$("#buddy-list-users-matching-view .empty-list-message").length = 0;
$("#buddy-list-other-users .empty-list-message").length = 0;
}
test("clear_search", ({override}) => { test("clear_search", ({override}) => {
override(presence, "get_status", () => "active"); override(presence, "get_status", () => "active");
override(presence, "get_user_ids", () => all_user_ids); override(presence, "get_user_ids", () => all_user_ids);
override(popovers, "hide_all", noop); override(popovers, "hide_all", noop);
override(resize, "resize_sidebars", noop); override(resize, "resize_sidebars", noop);
stub_buddy_list_empty_list_message_lengths();
// Empty because no users match this search string. // Empty because no users match this search string.
override(fake_buddy_list, "populate", (user_ids) => { override(fake_buddy_list, "populate", (user_ids) => {
assert.deepEqual(user_ids, {all_user_ids: []}); assert.deepEqual(user_ids, {all_user_ids: []});
@ -104,6 +114,7 @@ test("escape_search", ({override}) => {
override(resize, "resize_sidebars", noop); override(resize, "resize_sidebars", noop);
override(popovers, "hide_all", noop); override(popovers, "hide_all", noop);
stub_buddy_list_empty_list_message_lengths();
set_input_val("somevalue"); set_input_val("somevalue");
activity_ui.escape_search(); activity_ui.escape_search();