zulip/web/src/user_group_popover.ts
sujal shah 771d3b1434 invites: Enable adding users to user groups during invitations.
This commit allows users to be assigned to custom groups when
inviting them to join Zulip, similar to how channels are handled.
The implementation follows a similar pattern for adding pills,
ensuring consistency, as user groups and channels are parallel
in nature.

Fixes #24365.
2024-11-26 11:26:34 -08:00

209 lines
7.4 KiB
TypeScript

import $ from "jquery";
import assert from "minimalistic-assert";
import type * as tippy from "tippy.js";
import render_user_group_info_popover from "../templates/popovers/user_group_info_popover.hbs";
import * as blueslip from "./blueslip.ts";
import * as buddy_data from "./buddy_data.ts";
import * as hash_util from "./hash_util.ts";
import * as message_lists from "./message_lists.ts";
import * as people from "./people.ts";
import type {User} from "./people.ts";
import * as popover_menus from "./popover_menus.ts";
import * as rows from "./rows.ts";
import {current_user} from "./state_data.ts";
import * as ui_util from "./ui_util.ts";
import * as user_group_components from "./user_group_components.ts";
import * as user_groups from "./user_groups.ts";
import * as util from "./util.ts";
let user_group_popover_instance: tippy.Instance | undefined;
type PopoverGroupMember = User & {user_circle_class: string; user_last_seen_time_status: string};
export function hide(): void {
if (user_group_popover_instance !== undefined) {
user_group_popover_instance.destroy();
user_group_popover_instance = undefined;
}
}
export function is_open(): boolean {
return Boolean(user_group_popover_instance);
}
function get_user_group_popover_items(): JQuery | undefined {
if (user_group_popover_instance === undefined) {
blueslip.error("Trying to get menu items when user group popover is closed.");
return undefined;
}
const $popover = $(user_group_popover_instance.popper);
if (!$popover) {
blueslip.error("Cannot find user group popover data");
return undefined;
}
return $("li:not(.divider):visible a", $popover);
}
export function handle_keyboard(key: string): void {
const $items = get_user_group_popover_items();
popover_menus.popover_items_handle_keyboard(key, $items);
}
// element is the target element to pop off of;
// the element could be user group pill or mentions in a message;
// in case of message, message_id is the message id containing it;
// in case of user group pill, message_id is not used;
export function toggle_user_group_info_popover(
element: tippy.ReferenceElement,
message_id: number | undefined,
): void {
if (is_open()) {
hide();
return;
}
const $elt = $(element);
const user_group_id_str = $elt.attr("data-user-group-id");
assert(user_group_id_str !== undefined);
const user_group_id = Number.parseInt(user_group_id_str, 10);
const group = user_groups.get_user_group_from_id(user_group_id);
popover_menus.toggle_popover_menu(
element,
{
theme: "popover-menu",
placement: "right",
popperOptions: {
modifiers: [
{
name: "flip",
options: {
fallbackPlacements: ["left", "top", "bottom"],
},
},
],
},
onCreate(instance) {
if (message_id) {
assert(message_lists.current !== undefined);
message_lists.current.select_id(message_id);
}
user_group_popover_instance = instance;
const subgroups = user_groups.convert_name_to_display_name_for_groups(
user_groups
.get_direct_subgroups_of_group(group)
.sort(user_group_components.sort_group_member_name),
);
const args = {
group_name: user_groups.get_display_group_name(group.name),
group_description: group.description,
members: sort_group_members(fetch_group_members([...group.members])),
subgroups,
group_edit_url: hash_util.group_edit_url(group, "general"),
is_guest: current_user.is_guest,
is_system_group: group.is_system_group,
deactivated: group.deactivated,
members_count: user_groups.get_recursive_group_members(group).size,
};
instance.setContent(ui_util.parse_html(render_user_group_info_popover(args)));
},
onHidden() {
hide();
},
},
{
show_as_overlay_on_mobile: true,
show_as_overlay_always: false,
},
);
}
export function register_click_handlers(): void {
$("#main_div").on("click", ".user-group-mention", function (this: HTMLElement, e) {
e.stopPropagation();
const $elt = $(this);
const $row = $elt.closest(".message_row");
const message_id = rows.id($row);
assert(message_lists.current !== undefined);
const message = message_lists.current.get(message_id);
assert(message !== undefined);
try {
toggle_user_group_info_popover(this, message.id);
} catch {
// This user group has likely been deleted.
blueslip.info("Unable to find user group in message" + message.sender_id);
}
});
// Show the user_group_popover when pill clicked in subscriber settings.
$("body").on(
"click",
".person_picker .pill[data-user-group-id]",
function (this: HTMLElement, e) {
e.stopPropagation();
toggle_user_group_info_popover(this, undefined);
},
);
// Show the user_group_popover in user invite section.
$("body").on(
"click",
"#invite-user-group-container .pill-container .pill",
function (this: HTMLElement, e) {
e.stopPropagation();
toggle_user_group_info_popover(this, undefined);
},
);
// Note: Message feeds and drafts have their own direct event listeners
// that run before this one and call stopPropagation.
$("body").on("click", ".messagebox .user-group-mention", function (this: HTMLElement, e) {
e.stopPropagation();
toggle_user_group_info_popover(this, undefined);
});
$("body").on("click", ".view_user_group", function (this: HTMLElement, e) {
e.stopPropagation();
toggle_user_group_info_popover(this, undefined);
});
}
function fetch_group_members(member_ids: number[]): PopoverGroupMember[] {
return (
member_ids
.map((m: number) => people.get_user_by_id_assert_valid(m))
// We need to include inaccessible users here separately, since
// we do not include them in active_user_dict, but we want to
// show them in the popover as "Unknown user".
.filter(
(m: User) => people.is_active_user_for_popover(m.user_id) || m.is_inaccessible_user,
)
.map((p: User) => ({
...p,
user_circle_class: buddy_data.get_user_circle_class(p.user_id),
user_last_seen_time_status: buddy_data.user_last_seen_time_status(p.user_id),
}))
);
}
function sort_group_members(members: PopoverGroupMember[]): PopoverGroupMember[] {
return members.sort((a: PopoverGroupMember, b: PopoverGroupMember) =>
util.strcmp(a.full_name, b.full_name),
);
}
// exporting these functions for testing purposes
export const _test_fetch_group_members = fetch_group_members;
export const _test_sort_group_members = sort_group_members;
export function initialize(): void {
register_click_handlers();
}