buddy_list: Show conversation participants in the right sidebar.

Fixes #31129.
This commit is contained in:
evykassirer 2024-09-12 11:32:55 -07:00 committed by Tim Abbott
parent 12033d6690
commit 38e5b4b8fc
7 changed files with 209 additions and 19 deletions

View File

@ -15,6 +15,7 @@ import type {BuddyUserInfo} from "./buddy_data";
import {media_breakpoints_num} from "./css_variables"; import {media_breakpoints_num} from "./css_variables";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as message_lists from "./message_lists";
import * as message_viewport from "./message_viewport"; import * as message_viewport from "./message_viewport";
import * as narrow_state from "./narrow_state"; import * as narrow_state from "./narrow_state";
import * as padded_widget from "./padded_widget"; import * as padded_widget from "./padded_widget";
@ -78,6 +79,7 @@ type BuddyListRenderData = {
other_users_count: number; other_users_count: number;
total_human_users: number; total_human_users: number;
hide_headers: boolean; hide_headers: boolean;
participant_ids_set: Set<number>;
}; };
function get_render_data(): BuddyListRenderData { function get_render_data(): BuddyListRenderData {
@ -88,6 +90,17 @@ function get_render_data(): BuddyListRenderData {
const total_human_users = people.get_active_human_count(); const total_human_users = people.get_active_human_count();
const other_users_count = total_human_users - total_human_subscribers_count; const other_users_count = total_human_users - total_human_subscribers_count;
const hide_headers = should_hide_headers(current_sub, pm_ids_set); const hide_headers = should_hide_headers(current_sub, pm_ids_set);
const participant_ids_set = new Set<number>();
if (current_sub && narrow_state.topic() && message_lists.current) {
for (const message of message_lists.current.all_messages()) {
if (
!people.is_valid_bot_user(message.sender_id) &&
people.is_person_active(message.sender_id)
) {
participant_ids_set.add(message.sender_id);
}
}
}
return { return {
current_sub, current_sub,
@ -96,10 +109,12 @@ function get_render_data(): BuddyListRenderData {
other_users_count, other_users_count,
total_human_users, total_human_users,
hide_headers, hide_headers,
participant_ids_set,
}; };
} }
class BuddyListConf { class BuddyListConf {
participants_list_selector = "#buddy-list-participants";
matching_view_list_selector = "#buddy-list-users-matching-view"; matching_view_list_selector = "#buddy-list-users-matching-view";
other_user_list_selector = "#buddy-list-other-users"; other_user_list_selector = "#buddy-list-other-users";
scroll_container_selector = "#buddy_list_wrapper"; scroll_container_selector = "#buddy_list_wrapper";
@ -149,14 +164,17 @@ class BuddyListConf {
export class BuddyList extends BuddyListConf { export class BuddyList extends BuddyListConf {
all_user_ids: number[] = []; all_user_ids: number[] = [];
participant_user_ids: number[] = [];
users_matching_view_ids: number[] = []; users_matching_view_ids: number[] = [];
other_user_ids: number[] = []; other_user_ids: number[] = [];
participants_is_collapsed = false;
users_matching_view_is_collapsed = false; users_matching_view_is_collapsed = false;
other_users_is_collapsed = true; other_users_is_collapsed = true;
render_count = 0; render_count = 0;
render_data = get_render_data(); render_data = get_render_data();
// 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.
$participants_list = $(this.participants_list_selector);
$users_matching_view_list = $(this.matching_view_list_selector); $users_matching_view_list = $(this.matching_view_list_selector);
$other_users_list = $(this.other_user_list_selector); $other_users_list = $(this.other_user_list_selector);
@ -198,9 +216,29 @@ export class BuddyList extends BuddyListConf {
current_sub, current_sub,
pm_ids_set, pm_ids_set,
); );
const participant_count = Number.parseInt(
$("#buddy-list-participants-section-heading").attr("data-user-count")!,
10,
);
const elem_id = $elem.attr("id"); const elem_id = $elem.attr("id");
if (elem_id === "buddy-list-users-matching-view-section-heading") { if (elem_id === "buddy-list-participants-section-heading") {
if (current_sub) { tooltip_text = $t(
{
defaultMessage:
"{N, plural, one {# participant} other {# participants}}",
},
{N: participant_count},
);
} else if (elem_id === "buddy-list-users-matching-view-section-heading") {
if (participant_count) {
tooltip_text = $t(
{
defaultMessage:
"{N, plural, one {# other subscriber} other {# other subscribers}}",
},
{N: total_human_subscribers_count - participant_count},
);
} else if (current_sub) {
tooltip_text = $t( tooltip_text = $t(
{ {
defaultMessage: defaultMessage:
@ -242,6 +280,8 @@ export class BuddyList extends BuddyListConf {
populate(opts: {all_user_ids: number[]}): void { populate(opts: {all_user_ids: number[]}): void {
this.render_count = 0; this.render_count = 0;
this.$participants_list.empty();
this.participant_user_ids = [];
this.$users_matching_view_list.empty(); this.$users_matching_view_list.empty();
this.users_matching_view_ids = []; this.users_matching_view_ids = [];
this.$other_users_list.empty(); this.$other_users_list.empty();
@ -332,11 +372,23 @@ export class BuddyList extends BuddyListConf {
other_users_count, other_users_count,
total_human_users, total_human_users,
hide_headers, hide_headers,
participant_ids_set,
} = this.render_data; } = this.render_data;
$("#buddy-list-users-matching-view-container .buddy-list-subsection-header").empty(); $(".buddy-list-subsection-header").empty();
$("#buddy-list-other-users-container .buddy-list-subsection-header").empty();
// If we're in the mode of hiding headers, that means we're only showing the "other users"
// section, so hide the other two sections.
$("#buddy-list-users-matching-view-container").toggleClass("no-display", hide_headers); $("#buddy-list-users-matching-view-container").toggleClass("no-display", hide_headers);
const show_participants_list = !hide_headers && participant_ids_set.size;
$("#buddy-list-participants-container").toggleClass("no-display", !show_participants_list);
// This is the case where every subscriber is in the participants list. In this case, we
// hide the "others in this channel" section.
if (
show_participants_list &&
total_human_subscribers_count === this.participant_user_ids.length
) {
$("#buddy-list-users-matching-view-container").toggleClass("no-display", true);
}
// Usually we show the user counts in the headers, but if we're hiding // 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. // those headers then we show the total user count in the main title.
@ -351,17 +403,35 @@ export class BuddyList extends BuddyListConf {
let header_text; let header_text;
if (current_sub) { if (current_sub) {
header_text = $t({defaultMessage: "In this channel"}); if (participant_ids_set.size) {
header_text = $t({defaultMessage: "Others in this channel"});
} else {
header_text = $t({defaultMessage: "In this channel"});
}
} else { } else {
header_text = $t({defaultMessage: "In this conversation"}); header_text = $t({defaultMessage: "In this conversation"});
} }
$("#buddy-list-participants-container .buddy-list-subsection-header").append(
$(
render_section_header({
id: "buddy-list-participants-section-heading",
header_text: $t({defaultMessage: "In this conversation"}),
user_count: get_formatted_sub_count(this.participant_user_ids.length),
toggle_class: "toggle-participants",
is_collapsed: this.participants_is_collapsed,
}),
),
);
$("#buddy-list-users-matching-view-container .buddy-list-subsection-header").append( $("#buddy-list-users-matching-view-container .buddy-list-subsection-header").append(
$( $(
render_section_header({ render_section_header({
id: "buddy-list-users-matching-view-section-heading", id: "buddy-list-users-matching-view-section-heading",
header_text, header_text,
user_count: get_formatted_sub_count(total_human_subscribers_count), user_count: get_formatted_sub_count(
total_human_subscribers_count - this.participant_user_ids.length,
),
toggle_class: "toggle-users-matching-view", toggle_class: "toggle-users-matching-view",
is_collapsed: this.users_matching_view_is_collapsed, is_collapsed: this.users_matching_view_is_collapsed,
}), }),
@ -381,6 +451,26 @@ export class BuddyList extends BuddyListConf {
); );
} }
toggle_participants_section(): void {
this.participants_is_collapsed = !this.participants_is_collapsed;
$("#buddy-list-participants-container").toggleClass(
"collapsed",
this.participants_is_collapsed,
);
$("#buddy-list-participants-container .toggle-participants").toggleClass(
"fa-caret-down",
!this.participants_is_collapsed,
);
$("#buddy-list-participants-container .toggle-participants").toggleClass(
"fa-caret-right",
this.participants_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_users_matching_view_section(): void { toggle_users_matching_view_section(): void {
this.users_matching_view_is_collapsed = !this.users_matching_view_is_collapsed; this.users_matching_view_is_collapsed = !this.users_matching_view_is_collapsed;
$("#buddy-list-users-matching-view-container").toggleClass( $("#buddy-list-users-matching-view-container").toggleClass(
@ -434,6 +524,7 @@ 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 participants = [];
const subscribed_users = []; const subscribed_users = [];
const other_users = []; const other_users = [];
const current_sub = this.render_data.current_sub; const current_sub = this.render_data.current_sub;
@ -441,14 +532,27 @@ export class BuddyList extends BuddyListConf {
for (const item of items) { for (const item of items) {
if (buddy_data.user_matches_narrow(item.user_id, pm_ids_set, current_sub?.stream_id)) { if (buddy_data.user_matches_narrow(item.user_id, pm_ids_set, current_sub?.stream_id)) {
subscribed_users.push(item); if (this.render_data.participant_ids_set.has(item.user_id)) {
this.users_matching_view_ids.push(item.user_id); participants.push(item);
this.participant_user_ids.push(item.user_id);
} else {
subscribed_users.push(item);
this.users_matching_view_ids.push(item.user_id);
}
} else { } else {
other_users.push(item); other_users.push(item);
this.other_user_ids.push(item.user_id); this.other_user_ids.push(item.user_id);
} }
} }
this.$participants_list = $(this.participants_list_selector);
if (participants.length) {
const participants_html = this.items_to_html({
items: participants,
});
this.$participants_list.append($(participants_html));
}
this.$users_matching_view_list = $(this.matching_view_list_selector); this.$users_matching_view_list = $(this.matching_view_list_selector);
if (subscribed_users.length) { if (subscribed_users.length) {
// Remove the empty list message before adding users // Remove the empty list message before adding users
@ -486,7 +590,8 @@ export class BuddyList extends BuddyListConf {
render_view_user_list_links(): void { render_view_user_list_links(): void {
const {current_sub, total_human_subscribers_count, other_users_count} = this.render_data; const {current_sub, total_human_subscribers_count, other_users_count} = this.render_data;
const has_inactive_users_matching_view = const has_inactive_users_matching_view =
total_human_subscribers_count > this.users_matching_view_ids.length; total_human_subscribers_count >
this.users_matching_view_ids.length + this.participant_user_ids.length;
const has_inactive_other_users = other_users_count > this.other_user_ids.length; const has_inactive_other_users = other_users_count > this.other_user_ids.length;
// For stream views, we show a link at the bottom of the list of subscribed users that // For stream views, we show a link at the bottom of the list of subscribed users that
@ -514,10 +619,16 @@ export class BuddyList extends BuddyListConf {
if (has_inactive_other_users) { if (has_inactive_other_users) {
$("#buddy-list-other-users-container").append($(render_view_all_users())); $("#buddy-list-other-users-container").append($(render_view_all_users()));
} }
// Note that we don't show a link for the participants list because we expect
// all participants to be shown (except bots or deactivated 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(): number | undefined { first_key(): number | undefined {
if (this.participant_user_ids.length) {
return this.participant_user_ids[0];
}
if (this.users_matching_view_ids.length) { if (this.users_matching_view_ids.length) {
return this.users_matching_view_ids[0]; return this.users_matching_view_ids[0];
} }
@ -529,14 +640,29 @@ export class BuddyList extends BuddyListConf {
// 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: number): number | undefined { prev_key(key: number): number | undefined {
let i = this.users_matching_view_ids.indexOf(key); let i = this.participant_user_ids.indexOf(key);
// This would be the middle of the list of participants,
// moving to a prev participant.
if (i > 0) {
return this.participant_user_ids[i - 1];
}
// If it's the first participant, we don't move the selection.
if (i === 0) {
return undefined;
}
i = this.users_matching_view_ids.indexOf(key);
// This would be the middle of the list of users matching view, // This would be the middle of the list of users matching view,
// moving to a prev user matching the view. // moving to a prev user matching the view.
if (i > 0) { if (i > 0) {
return this.users_matching_view_ids[i - 1]; return this.users_matching_view_ids[i - 1];
} }
// If it's the first user matching the view, we don't move the selection. // The key before the first user matching view is the last participant, if that exists,
// and if it doesn't then we don't move the selection.
if (i === 0) { if (i === 0) {
if (this.participant_user_ids.length > 0) {
return this.participant_user_ids.at(-1);
}
return undefined; return undefined;
} }
@ -551,12 +677,17 @@ export class BuddyList extends BuddyListConf {
if (this.users_matching_view_ids.length > 0) { if (this.users_matching_view_ids.length > 0) {
return this.users_matching_view_ids.at(-1); return this.users_matching_view_ids.at(-1);
} }
// If there are no matching users but there are participants, go there
if (this.participant_user_ids.length > 0) {
return this.participant_user_ids.at(-1);
}
return undefined; return undefined;
} }
// The only way we reach here is if the key isn't found in either list, // The only way we reach here is if the key isn't found in either list,
// which shouldn't happen. // which shouldn't happen.
blueslip.error("Couldn't find key in buddy list", { blueslip.error("Couldn't find key in buddy list", {
key, key,
participant_user_ids: this.participant_user_ids,
users_matching_view_ids: this.users_matching_view_ids, users_matching_view_ids: this.users_matching_view_ids,
other_user_ids: this.other_user_ids, other_user_ids: this.other_user_ids,
}); });
@ -565,7 +696,25 @@ export class BuddyList extends BuddyListConf {
// 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: number): number | undefined { next_key(key: number): number | undefined {
let i = this.users_matching_view_ids.indexOf(key); let i = this.participant_user_ids.indexOf(key);
// Moving from participants to the list of users matching view,
// if they exist, otherwise do nothing.
if (i >= 0 && i === this.participant_user_ids.length - 1) {
if (this.users_matching_view_ids.length > 0) {
return this.users_matching_view_ids[0];
}
// If there are no matching users but there are other users, go there
if (this.other_user_ids.length > 0) {
return this.other_user_ids[0];
}
return undefined;
}
// This is a regular move within the list of users matching the view.
if (i >= 0) {
return this.participant_user_ids[i + 1];
}
i = this.users_matching_view_ids.indexOf(key);
// Moving from users matching the view to the list of other users, // Moving from users matching the view to the list of other users,
// if they exist, otherwise do nothing. // if they exist, otherwise do nothing.
if (i >= 0 && i === this.users_matching_view_ids.length - 1) { if (i >= 0 && i === this.users_matching_view_ids.length - 1) {
@ -593,6 +742,7 @@ export class BuddyList extends BuddyListConf {
// which shouldn't happen. // which shouldn't happen.
blueslip.error("Couldn't find key in buddy list", { blueslip.error("Couldn't find key in buddy list", {
key, key,
participant_user_ids: this.participant_user_ids,
users_matching_view_ids: this.users_matching_view_ids, users_matching_view_ids: this.users_matching_view_ids,
other_user_ids: this.other_user_ids, other_user_ids: this.other_user_ids,
}); });
@ -601,7 +751,11 @@ export class BuddyList extends BuddyListConf {
maybe_remove_user_id(opts: {user_id: number}): void { maybe_remove_user_id(opts: {user_id: number}): void {
let was_removed = false; let was_removed = false;
for (const user_id_list of [this.users_matching_view_ids, this.other_user_ids]) { for (const user_id_list of [
this.participant_user_ids,
this.users_matching_view_ids,
this.other_user_ids,
]) {
const pos = user_id_list.indexOf(opts.user_id); const pos = user_id_list.indexOf(opts.user_id);
if (pos >= 0) { if (pos >= 0) {
user_id_list.splice(pos, 1); user_id_list.splice(pos, 1);
@ -699,14 +853,18 @@ export class BuddyList extends BuddyListConf {
new_user_id: number | undefined; new_user_id: number | undefined;
html: string; html: string;
is_subscribed_user: boolean; is_subscribed_user: boolean;
is_participant_user: boolean;
}): void { }): void {
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 is_subscribed_user = opts.is_subscribed_user; const is_subscribed_user = opts.is_subscribed_user;
const is_participant_user = opts.is_participant_user;
// This means we're inserting at the end // This means we're inserting at the end
if (user_id_following_insertion === undefined) { if (user_id_following_insertion === undefined) {
if (is_subscribed_user) { if (is_participant_user) {
this.$participants_list.append($(html));
} else if (is_subscribed_user) {
this.$users_matching_view_list.append($(html)); this.$users_matching_view_list.append($(html));
} else { } else {
this.$other_users_list.append($(html)); this.$other_users_list.append($(html));
@ -739,9 +897,14 @@ export class BuddyList extends BuddyListConf {
pm_ids_set, pm_ids_set,
current_sub?.stream_id, current_sub?.stream_id,
); );
const user_id_list = is_subscribed_user let user_id_list;
? this.users_matching_view_ids if (this.render_data.participant_ids_set.has(user_id)) {
: this.other_user_ids; user_id_list = this.participant_user_ids;
} else if (is_subscribed_user) {
user_id_list = this.users_matching_view_ids;
} else {
user_id_list = this.other_user_ids;
}
const new_pos_in_user_list = this.find_position({ const new_pos_in_user_list = this.find_position({
user_id, user_id,
user_id_list, user_id_list,
@ -760,6 +923,7 @@ export class BuddyList extends BuddyListConf {
html, html,
new_user_id, new_user_id,
is_subscribed_user, is_subscribed_user,
is_participant_user: this.render_data.participant_ids_set.has(user_id),
}); });
} }

View File

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

View File

@ -2,11 +2,13 @@ import autosize from "autosize";
import $ from "jquery"; import $ from "jquery";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import * as activity_ui from "./activity_ui";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as compose_tooltips from "./compose_tooltips"; import * as compose_tooltips from "./compose_tooltips";
import type {MessageListData} from "./message_list_data"; import type {MessageListData} from "./message_list_data";
import * as message_list_tooltips from "./message_list_tooltips"; import * as message_list_tooltips from "./message_list_tooltips";
import {MessageListView} from "./message_list_view"; import {MessageListView} from "./message_list_view";
import * as message_lists from "./message_lists";
import type {Message} from "./message_store"; import type {Message} from "./message_store";
import * as narrow_banner from "./narrow_banner"; import * as narrow_banner from "./narrow_banner";
import * as narrow_state from "./narrow_state"; import * as narrow_state from "./narrow_state";
@ -202,6 +204,11 @@ export class MessageList {
this.select_id(first_unread_message_id, {then_scroll: true, use_closest: true}); this.select_id(first_unread_message_id, {then_scroll: true, use_closest: true});
} }
// Rebuild message list, since we might need to shuffle around the participant users.
if (this === message_lists.current && narrow_state.stream_sub() && narrow_state.topic()) {
activity_ui.build_user_sidebar();
}
return render_info; return render_info;
} }
@ -459,6 +466,11 @@ export class MessageList {
remove_and_rerender(message_ids: number[]): void { remove_and_rerender(message_ids: number[]): void {
this.data.remove(message_ids); this.data.remove(message_ids);
this.rerender(); this.rerender();
// Rebuild message list if we're deleting messages from the current list,
// since we might need to remove a participant user.
if (this.is_current_message_list()) {
activity_ui.build_user_sidebar();
}
} }
show_edit_message($row: JQuery, $form: JQuery): void { show_edit_message($row: JQuery, $form: JQuery): void {

View File

@ -268,6 +268,11 @@ export function initialize_right_sidebar(): void {
}, },
); );
$("#buddy-list-participants-container").on("click", ".buddy-list-subsection-header", (e) => {
e.stopPropagation();
buddy_list.toggle_participants_section();
});
$("#buddy-list-other-users-container").on("click", ".buddy-list-subsection-header", (e) => { $("#buddy-list-other-users-container").on("click", ".buddy-list-subsection-header", (e) => {
e.stopPropagation(); e.stopPropagation();
buddy_list.toggle_other_users_section(); buddy_list.toggle_other_users_section();

View File

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

View File

@ -14,6 +14,10 @@
</button> </button>
</div> </div>
<div id="buddy_list_wrapper" class="scrolling_list" data-simplebar data-simplebar-tab-index="-1"> <div id="buddy_list_wrapper" class="scrolling_list" data-simplebar data-simplebar-tab-index="-1">
<div id="buddy-list-participants-container" class="buddy-list-section-container">
<div class="buddy-list-subsection-header"></div>
<ul id="buddy-list-participants" class="buddy-list-section filters" data-search-results-empty="{{t 'None.' }}"></ul>
</div>
<div id="buddy-list-users-matching-view-container" class="buddy-list-section-container"> <div id="buddy-list-users-matching-view-container" class="buddy-list-section-container">
<div class="buddy-list-subsection-header"></div> <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> <ul id="buddy-list-users-matching-view" class="buddy-list-section filters" data-search-results-empty="{{t 'None.' }}"></ul>

View File

@ -23,6 +23,7 @@ set_global("document", {
}, },
}); });
const activity_ui = mock_esm("../src/activity_ui");
const narrow_state = mock_esm("../src/narrow_state"); const narrow_state = mock_esm("../src/narrow_state");
const stream_data = mock_esm("../src/stream_data"); const stream_data = mock_esm("../src/stream_data");
@ -43,6 +44,7 @@ mock_esm("../src/message_list_view", {
const {Filter} = zrequire("filter"); const {Filter} = zrequire("filter");
run_test("basics", ({override}) => { run_test("basics", ({override}) => {
override(activity_ui, "build_user_sidebar", noop);
const filter = new Filter([]); const filter = new Filter([]);
const list = new MessageList({ const list = new MessageList({
@ -433,7 +435,9 @@ run_test("bookend", ({override}) => {
} }
}); });
run_test("add_remove_rerender", () => { run_test("add_remove_rerender", ({override}) => {
override(activity_ui, "build_user_sidebar", noop);
const filter = new Filter([]); const filter = new Filter([]);
const list = new MessageList({ const list = new MessageList({
data: new MessageListData({ data: new MessageListData({