mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1755 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1755 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import md5 from "blueimp-md5";
 | 
						|
import assert from "minimalistic-assert";
 | 
						|
 | 
						|
import * as typeahead from "../shared/src/typeahead";
 | 
						|
 | 
						|
import * as blueslip from "./blueslip";
 | 
						|
import {FoldDict} from "./fold_dict";
 | 
						|
import {$t} from "./i18n";
 | 
						|
import type {DisplayRecipientUser, Message, MessageWithBooleans} from "./message_store";
 | 
						|
import * as message_user_ids from "./message_user_ids";
 | 
						|
import * as muted_users from "./muted_users";
 | 
						|
import {page_params} from "./page_params";
 | 
						|
import * as reload_state from "./reload_state";
 | 
						|
import * as settings_config from "./settings_config";
 | 
						|
import * as settings_data from "./settings_data";
 | 
						|
import {current_user, realm} from "./state_data";
 | 
						|
import * as timerender from "./timerender";
 | 
						|
import {user_settings} from "./user_settings";
 | 
						|
import * as util from "./util";
 | 
						|
 | 
						|
export type ProfileData = {
 | 
						|
    value: string;
 | 
						|
    rendered_value?: string;
 | 
						|
};
 | 
						|
 | 
						|
export type User = {
 | 
						|
    user_id: number;
 | 
						|
    delivery_email: string | null;
 | 
						|
    email: string;
 | 
						|
    full_name: string;
 | 
						|
    date_joined: string;
 | 
						|
    is_active: boolean;
 | 
						|
    is_owner: boolean;
 | 
						|
    is_admin: boolean;
 | 
						|
    is_guest: boolean;
 | 
						|
    is_moderator: boolean;
 | 
						|
    is_billing_admin: boolean;
 | 
						|
    role: number;
 | 
						|
    timezone: string;
 | 
						|
    avatar_url?: string | null;
 | 
						|
    avatar_version: number;
 | 
						|
    profile_data: Record<number, ProfileData>;
 | 
						|
    // used for fake user objects.
 | 
						|
    is_missing_server_data?: boolean;
 | 
						|
    // used for inaccessible user objects.
 | 
						|
    is_inaccessible_user?: boolean;
 | 
						|
} & (
 | 
						|
    | {
 | 
						|
          is_bot: false;
 | 
						|
          bot_type: null;
 | 
						|
      }
 | 
						|
    | {
 | 
						|
          is_bot: true;
 | 
						|
          bot_type: number;
 | 
						|
          bot_owner_id: number | null;
 | 
						|
      }
 | 
						|
);
 | 
						|
 | 
						|
export type SenderInfo = User & {
 | 
						|
    avatar_url_small: string;
 | 
						|
    is_muted: boolean;
 | 
						|
};
 | 
						|
 | 
						|
// This type is generated by the `compose_typeahead.broadcast_mentions` function.
 | 
						|
export type PseudoMentionUser = {
 | 
						|
    special_item_text: string;
 | 
						|
    email: string;
 | 
						|
    pm_recipient_count: number;
 | 
						|
    full_name: string;
 | 
						|
    is_broadcast: true;
 | 
						|
    idx: number;
 | 
						|
};
 | 
						|
 | 
						|
export type CrossRealmBot = User & {
 | 
						|
    is_system_bot: boolean;
 | 
						|
};
 | 
						|
 | 
						|
export type PeopleParams = {
 | 
						|
    realm_users: User[];
 | 
						|
    realm_non_active_users: User[];
 | 
						|
    cross_realm_bots: CrossRealmBot[];
 | 
						|
};
 | 
						|
 | 
						|
let people_dict: FoldDict<User>;
 | 
						|
let people_by_name_dict: FoldDict<User>;
 | 
						|
let people_by_user_id_dict: Map<number, User>;
 | 
						|
let active_user_dict: Map<number, User>;
 | 
						|
let non_active_user_dict: Map<number, User>;
 | 
						|
let cross_realm_dict: Map<number, CrossRealmBot>;
 | 
						|
let pm_recipient_count_dict: Map<number, number>;
 | 
						|
let duplicate_full_name_data: FoldDict<Set<number>>;
 | 
						|
let my_user_id: number;
 | 
						|
 | 
						|
export let INACCESSIBLE_USER_NAME: string;
 | 
						|
 | 
						|
// We have an init() function so that our automated tests
 | 
						|
// can easily clear data.
 | 
						|
export function init(): void {
 | 
						|
    // The following three dicts point to the same objects
 | 
						|
    // (all people we've seen), but people_dict can have duplicate
 | 
						|
    // keys related to email changes.  We want to deprecate
 | 
						|
    // people_dict over time and always do lookups by user_id.
 | 
						|
    people_dict = new FoldDict();
 | 
						|
    people_by_name_dict = new FoldDict();
 | 
						|
    people_by_user_id_dict = new Map();
 | 
						|
 | 
						|
    // The next dictionary includes all active users (human/user)
 | 
						|
    // in our realm, but it excludes non-active users and
 | 
						|
    // cross-realm bots.
 | 
						|
    active_user_dict = new Map();
 | 
						|
    non_active_user_dict = new Map();
 | 
						|
    cross_realm_dict = new Map(); // keyed by user_id
 | 
						|
    pm_recipient_count_dict = new Map();
 | 
						|
 | 
						|
    // This maintains a set of ids of people with same full names.
 | 
						|
    duplicate_full_name_data = new FoldDict();
 | 
						|
 | 
						|
    INACCESSIBLE_USER_NAME = $t({defaultMessage: "Unknown user"});
 | 
						|
}
 | 
						|
 | 
						|
// WE INITIALIZE DATA STRUCTURES HERE!
 | 
						|
init();
 | 
						|
 | 
						|
export function split_to_ints(lst: string): number[] {
 | 
						|
    return lst.split(",").map((s) => Number.parseInt(s, 10));
 | 
						|
}
 | 
						|
 | 
						|
export function get_users_from_ids(user_ids: number[]): User[] {
 | 
						|
    return user_ids.map((user_id) => get_by_user_id(user_id));
 | 
						|
}
 | 
						|
 | 
						|
// Use this function only when you are sure that user_id is valid.
 | 
						|
export function get_by_user_id(user_id: number): User {
 | 
						|
    const person = people_by_user_id_dict.get(user_id);
 | 
						|
    assert(person, `Unknown user_id in get_by_user_id: ${user_id}`);
 | 
						|
    return person;
 | 
						|
}
 | 
						|
 | 
						|
// This is type unsafe version of get_by_user_id for the callers that expects undefined values.
 | 
						|
export function maybe_get_user_by_id(user_id: number, ignore_missing = false): User | undefined {
 | 
						|
    if (!people_by_user_id_dict.has(user_id) && !ignore_missing) {
 | 
						|
        blueslip.error("Unknown user_id in maybe_get_user_by_id", {user_id});
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
    return people_by_user_id_dict.get(user_id);
 | 
						|
}
 | 
						|
 | 
						|
export function validate_user_ids(user_ids: number[]): number[] {
 | 
						|
    const good_ids = [];
 | 
						|
    const bad_ids = [];
 | 
						|
 | 
						|
    for (const user_id of user_ids) {
 | 
						|
        if (people_by_user_id_dict.has(user_id)) {
 | 
						|
            good_ids.push(user_id);
 | 
						|
        } else {
 | 
						|
            bad_ids.push(user_id);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (bad_ids.length > 0) {
 | 
						|
        blueslip.warn("We have untracked user_ids", {bad_ids});
 | 
						|
    }
 | 
						|
 | 
						|
    return good_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function get_by_email(email: string): User | undefined {
 | 
						|
    const person = people_dict.get(email);
 | 
						|
 | 
						|
    if (!person) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    if (person.email.toLowerCase() !== email.toLowerCase()) {
 | 
						|
        blueslip.warn(
 | 
						|
            "Obsolete email passed to get_by_email: " + email + " new email = " + person.email,
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    return person;
 | 
						|
}
 | 
						|
 | 
						|
export function get_bot_owner_user(user: User & {is_bot: true}): User | undefined {
 | 
						|
    const owner_id = user.bot_owner_id;
 | 
						|
 | 
						|
    if (owner_id === undefined || owner_id === null) {
 | 
						|
        // This is probably a cross-realm bot.
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return get_user_by_id_assert_valid(owner_id);
 | 
						|
}
 | 
						|
 | 
						|
export function can_admin_user(user: User): boolean {
 | 
						|
    return (
 | 
						|
        (user.is_bot && user.bot_owner_id !== null && user.bot_owner_id === current_user.user_id) ||
 | 
						|
        is_my_user_id(user.user_id)
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
export function id_matches_email_operand(user_id: number, email: string): boolean {
 | 
						|
    const person = get_by_email(email);
 | 
						|
 | 
						|
    if (!person) {
 | 
						|
        // The user may type bad data into the search bar, so
 | 
						|
        // we don't complain too loud here.
 | 
						|
        blueslip.debug("User email operand unknown: " + email);
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return person.user_id === user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function update_email(user_id: number, new_email: string): void {
 | 
						|
    const person = get_by_user_id(user_id);
 | 
						|
    person.email = new_email;
 | 
						|
    people_dict.set(new_email, person);
 | 
						|
 | 
						|
    // For legacy reasons we don't delete the old email
 | 
						|
    // keys in our dictionaries, so that reverse lookups
 | 
						|
    // still work correctly.
 | 
						|
}
 | 
						|
 | 
						|
export function get_visible_email(user: User): string {
 | 
						|
    if (user.delivery_email) {
 | 
						|
        return user.delivery_email;
 | 
						|
    }
 | 
						|
    return user.email;
 | 
						|
}
 | 
						|
 | 
						|
export function get_user_id(email: string): number | undefined {
 | 
						|
    const person = get_by_email(email);
 | 
						|
    if (person === undefined) {
 | 
						|
        blueslip.error("Unknown email for get_user_id", {email});
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
    const user_id = person.user_id;
 | 
						|
    if (!user_id) {
 | 
						|
        blueslip.error("No user_id found for email", {email});
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function is_known_user_id(user_id: number): boolean {
 | 
						|
    /*
 | 
						|
    We may get a user_id from mention syntax that we don't
 | 
						|
    know about if a user includes some random number in
 | 
						|
    the mention syntax by manually typing it instead of
 | 
						|
    selecting some user from typeahead.
 | 
						|
    */
 | 
						|
 | 
						|
    /*
 | 
						|
    This function also returns false for inaccessible users
 | 
						|
    even though we have the user_id for them as we do not
 | 
						|
    want to show the mention pill for them.
 | 
						|
    */
 | 
						|
    const person = maybe_get_user_by_id(user_id, true);
 | 
						|
    if (person === undefined || person.is_inaccessible_user) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
export function is_known_user(user: User): boolean {
 | 
						|
    return user && is_known_user_id(user.user_id);
 | 
						|
}
 | 
						|
 | 
						|
function sort_numerically(user_ids: number[]): number[] {
 | 
						|
    user_ids.sort((a, b) => a - b);
 | 
						|
 | 
						|
    return user_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function huddle_string(message: Message): string | undefined {
 | 
						|
    if (message.type !== "private") {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    assert(
 | 
						|
        typeof message.display_recipient !== "string",
 | 
						|
        "Private messages should have list of recipients",
 | 
						|
    );
 | 
						|
    let user_ids = message.display_recipient.map((recip) => recip.id);
 | 
						|
 | 
						|
    user_ids = user_ids.filter(
 | 
						|
        (user_id) => user_id && people_by_user_id_dict.has(user_id) && !is_my_user_id(user_id),
 | 
						|
    );
 | 
						|
 | 
						|
    if (user_ids.length <= 1) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function user_ids_string_to_emails_string(user_ids_string: string): string | undefined {
 | 
						|
    const user_ids = split_to_ints(user_ids_string);
 | 
						|
 | 
						|
    let emails = util.try_parse_as_truthy(
 | 
						|
        user_ids.map((user_id) => {
 | 
						|
            const person = people_by_user_id_dict.get(user_id);
 | 
						|
            return person?.email;
 | 
						|
        }),
 | 
						|
    );
 | 
						|
 | 
						|
    if (emails === undefined) {
 | 
						|
        blueslip.warn("Unknown user ids: " + user_ids_string);
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    emails = emails.map((email) => email.toLowerCase());
 | 
						|
 | 
						|
    emails.sort();
 | 
						|
 | 
						|
    return emails.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function user_ids_string_to_ids_array(user_ids_string: string): number[] {
 | 
						|
    const user_ids = user_ids_string.length === 0 ? [] : user_ids_string.split(",");
 | 
						|
    const ids = user_ids.map(Number);
 | 
						|
    return ids;
 | 
						|
}
 | 
						|
 | 
						|
export function get_participants_from_user_ids_string(user_ids_string: string): Set<number> {
 | 
						|
    // Convert to set to ensure there are no duplicate ids.
 | 
						|
    const user_ids = new Set(user_ids_string_to_ids_array(user_ids_string));
 | 
						|
    // For group or 1:1 direct messages, the user_ids_string contains
 | 
						|
    // just the other user, so we need to add ourselves if not already
 | 
						|
    // present. For a direct message to oneself, the current user is
 | 
						|
    // already present, in user_ids_string, so we don't need to add it
 | 
						|
    // which is take care of by user_ids being a `Set`.
 | 
						|
    user_ids.add(my_user_id);
 | 
						|
    return user_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function emails_strings_to_user_ids_array(emails_string: string): number[] | undefined {
 | 
						|
    const user_ids_string = emails_strings_to_user_ids_string(emails_string);
 | 
						|
    if (user_ids_string === undefined) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    const user_ids_array = user_ids_string_to_ids_array(user_ids_string);
 | 
						|
    return user_ids_array;
 | 
						|
}
 | 
						|
 | 
						|
export function reply_to_to_user_ids_string(emails_string: string): string | undefined {
 | 
						|
    // This is basically emails_strings_to_user_ids_string
 | 
						|
    // without blueslip warnings, since it can be called with
 | 
						|
    // invalid data.
 | 
						|
    const emails = emails_string.split(",");
 | 
						|
 | 
						|
    let user_ids = util.try_parse_as_truthy(
 | 
						|
        emails.map((email) => {
 | 
						|
            const person = get_by_email(email);
 | 
						|
            return person?.user_id;
 | 
						|
        }),
 | 
						|
    );
 | 
						|
 | 
						|
    if (user_ids === undefined) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function emails_to_full_names_string(emails: string[]): string {
 | 
						|
    return emails
 | 
						|
        .map((email) => {
 | 
						|
            email = email.trim();
 | 
						|
            const person = get_by_email(email);
 | 
						|
            if (person !== undefined) {
 | 
						|
                return person.full_name;
 | 
						|
            }
 | 
						|
            return INACCESSIBLE_USER_NAME;
 | 
						|
        })
 | 
						|
        .join(", ");
 | 
						|
}
 | 
						|
 | 
						|
export function get_user_time(user_id: number): string | undefined {
 | 
						|
    const user_timezone = get_by_user_id(user_id).timezone;
 | 
						|
    if (user_timezone) {
 | 
						|
        try {
 | 
						|
            return new Date().toLocaleTimeString(user_settings.default_language, {
 | 
						|
                ...timerender.get_format_options_for_type(
 | 
						|
                    "time",
 | 
						|
                    user_settings.twenty_four_hour_time,
 | 
						|
                ),
 | 
						|
                timeZone: user_timezone,
 | 
						|
            });
 | 
						|
        } catch (error) {
 | 
						|
            blueslip.warn(`Error formatting time in ${user_timezone}: ${String(error)}`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return undefined;
 | 
						|
}
 | 
						|
 | 
						|
export function get_user_type(user_id: number): string | undefined {
 | 
						|
    const user_profile = get_by_user_id(user_id);
 | 
						|
    return settings_config.user_role_map.get(user_profile.role);
 | 
						|
}
 | 
						|
 | 
						|
export function emails_strings_to_user_ids_string(emails_string: string): string | undefined {
 | 
						|
    const emails = emails_string.split(",");
 | 
						|
    return email_list_to_user_ids_string(emails);
 | 
						|
}
 | 
						|
 | 
						|
export function email_list_to_user_ids_string(emails: string[]): string | undefined {
 | 
						|
    let user_ids = util.try_parse_as_truthy(
 | 
						|
        emails.map((email) => {
 | 
						|
            const person = get_by_email(email);
 | 
						|
            return person?.user_id;
 | 
						|
        }),
 | 
						|
    );
 | 
						|
 | 
						|
    if (user_ids === undefined) {
 | 
						|
        blueslip.warn("Unknown emails", {emails});
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function get_full_names_for_poll_option(user_ids: number[]): string {
 | 
						|
    return get_display_full_names(user_ids).join(", ");
 | 
						|
}
 | 
						|
 | 
						|
export function get_display_full_name(user_id: number): string {
 | 
						|
    const person = get_user_by_id_assert_valid(user_id);
 | 
						|
 | 
						|
    if (muted_users.is_user_muted(user_id)) {
 | 
						|
        if (should_add_guest_user_indicator(user_id)) {
 | 
						|
            return $t({defaultMessage: "Muted user (guest)"});
 | 
						|
        }
 | 
						|
 | 
						|
        return $t({defaultMessage: "Muted user"});
 | 
						|
    }
 | 
						|
 | 
						|
    if (should_add_guest_user_indicator(user_id)) {
 | 
						|
        return $t({defaultMessage: "{name} (guest)"}, {name: person.full_name});
 | 
						|
    }
 | 
						|
 | 
						|
    return person.full_name;
 | 
						|
}
 | 
						|
 | 
						|
export function get_display_full_names(user_ids: number[]): string[] {
 | 
						|
    return user_ids.map((user_id) => get_display_full_name(user_id));
 | 
						|
}
 | 
						|
 | 
						|
export function get_full_name(user_id: number): string {
 | 
						|
    const person = get_by_user_id(user_id);
 | 
						|
    return person.full_name;
 | 
						|
}
 | 
						|
 | 
						|
function _calc_user_and_other_ids(user_ids_string: string): {
 | 
						|
    user_ids: number[];
 | 
						|
    other_ids: number[];
 | 
						|
} {
 | 
						|
    const user_ids = split_to_ints(user_ids_string);
 | 
						|
    const other_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
 | 
						|
    return {user_ids, other_ids};
 | 
						|
}
 | 
						|
 | 
						|
export function get_recipients(user_ids_string: string): string {
 | 
						|
    // See message_store.get_pm_full_names() for a similar function.
 | 
						|
 | 
						|
    const {other_ids} = _calc_user_and_other_ids(user_ids_string);
 | 
						|
 | 
						|
    if (other_ids.length === 0) {
 | 
						|
        // direct message with oneself
 | 
						|
        return my_full_name();
 | 
						|
    }
 | 
						|
 | 
						|
    const names = get_display_full_names(other_ids).sort();
 | 
						|
    return names.join(", ");
 | 
						|
}
 | 
						|
 | 
						|
export function pm_reply_user_string(message: Message): string | undefined {
 | 
						|
    const user_ids = pm_with_user_ids(message);
 | 
						|
 | 
						|
    if (!user_ids) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function pm_reply_to(message: Message): string | undefined {
 | 
						|
    const user_ids = pm_with_user_ids(message);
 | 
						|
 | 
						|
    if (!user_ids) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    const emails = user_ids.map((user_id) => {
 | 
						|
        const person = people_by_user_id_dict.get(user_id);
 | 
						|
        if (!person) {
 | 
						|
            blueslip.error("Unknown user id in message", {user_id});
 | 
						|
            return "?";
 | 
						|
        }
 | 
						|
        return person.email;
 | 
						|
    });
 | 
						|
 | 
						|
    emails.sort();
 | 
						|
 | 
						|
    const reply_to = emails.join(",");
 | 
						|
 | 
						|
    return reply_to;
 | 
						|
}
 | 
						|
 | 
						|
function sorted_other_user_ids(user_ids: number[]): number[] {
 | 
						|
    // This excludes your own user id unless you're the only user
 | 
						|
    // (i.e. you sent a message to yourself).
 | 
						|
 | 
						|
    const other_user_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
 | 
						|
 | 
						|
    if (other_user_ids.length >= 1) {
 | 
						|
        user_ids = other_user_ids;
 | 
						|
    } else {
 | 
						|
        user_ids = [my_user_id];
 | 
						|
    }
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
 | 
						|
    return user_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function concat_huddle(user_ids: number[], user_id: number): string {
 | 
						|
    /*
 | 
						|
        We assume user_ids and user_id have already
 | 
						|
        been validated by the caller.
 | 
						|
 | 
						|
        The only logic we're encapsulating here is
 | 
						|
        how to encode huddles.
 | 
						|
    */
 | 
						|
    const sorted_ids = sort_numerically([...user_ids, user_id]);
 | 
						|
    return sorted_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function pm_lookup_key_from_user_ids(user_ids: number[]): string {
 | 
						|
    /*
 | 
						|
        The server will sometimes include our own user id
 | 
						|
        in keys for direct messages, but we only want our
 | 
						|
        user id if we sent a direct message to ourself.
 | 
						|
    */
 | 
						|
    user_ids = sorted_other_user_ids(user_ids);
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function pm_lookup_key(user_ids_string: string): string {
 | 
						|
    const user_ids = split_to_ints(user_ids_string);
 | 
						|
    return pm_lookup_key_from_user_ids(user_ids);
 | 
						|
}
 | 
						|
 | 
						|
export function all_user_ids_in_pm(message: Message): number[] | undefined {
 | 
						|
    if (message.type !== "private") {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    assert(
 | 
						|
        typeof message.display_recipient !== "string",
 | 
						|
        "Private messages should have list of recipients",
 | 
						|
    );
 | 
						|
 | 
						|
    if (message.display_recipient.length === 0) {
 | 
						|
        blueslip.error("Empty recipient list in message");
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    let user_ids = message.display_recipient.map((recip) => recip.id);
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
    return user_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function pm_with_user_ids(
 | 
						|
    message: Message & {reply_to?: string; url?: string},
 | 
						|
): number[] | undefined {
 | 
						|
    if (message.type !== "private") {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    assert(
 | 
						|
        typeof message.display_recipient !== "string",
 | 
						|
        "Private messages should have list of recipients",
 | 
						|
    );
 | 
						|
 | 
						|
    if (message.display_recipient.length === 0) {
 | 
						|
        blueslip.error("Empty recipient list in message");
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    const user_ids = message.display_recipient.map((recip) => recip.id);
 | 
						|
 | 
						|
    return sorted_other_user_ids(user_ids);
 | 
						|
}
 | 
						|
 | 
						|
export function pm_perma_link(message: Message): string | undefined {
 | 
						|
    const user_ids = all_user_ids_in_pm(message);
 | 
						|
 | 
						|
    if (!user_ids) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    let suffix;
 | 
						|
 | 
						|
    if (user_ids.length >= 3) {
 | 
						|
        suffix = "group";
 | 
						|
    } else {
 | 
						|
        suffix = "dm";
 | 
						|
    }
 | 
						|
 | 
						|
    const slug = user_ids.join(",") + "-" + suffix;
 | 
						|
    const url = "#narrow/dm/" + slug;
 | 
						|
    return url;
 | 
						|
}
 | 
						|
 | 
						|
export function pm_with_url(message: Message): string | undefined {
 | 
						|
    const user_ids = pm_with_user_ids(message);
 | 
						|
 | 
						|
    if (!user_ids) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    let suffix;
 | 
						|
 | 
						|
    if (user_ids.length > 1) {
 | 
						|
        suffix = "group";
 | 
						|
    } else {
 | 
						|
        const person = maybe_get_user_by_id(user_ids[0]);
 | 
						|
        if (person?.full_name) {
 | 
						|
            suffix = person.full_name.replaceAll(/[ "%/<>`\p{C}]+/gu, "-");
 | 
						|
        } else {
 | 
						|
            blueslip.error("Unknown people in message");
 | 
						|
            suffix = "unk";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const slug = user_ids.join(",") + "-" + suffix;
 | 
						|
    const url = "#narrow/dm/" + slug;
 | 
						|
    return url;
 | 
						|
}
 | 
						|
 | 
						|
export function update_email_in_reply_to(
 | 
						|
    reply_to: string,
 | 
						|
    user_id: number,
 | 
						|
    new_email: string,
 | 
						|
): string {
 | 
						|
    // We try to replace an old email with a new email in a reply_to,
 | 
						|
    // but we try to avoid changing the reply_to if we don't have to,
 | 
						|
    // and we don't warn on any errors.
 | 
						|
    let emails = reply_to.split(",");
 | 
						|
 | 
						|
    const persons = util.try_parse_as_truthy(emails.map((email) => people_dict.get(email.trim())));
 | 
						|
 | 
						|
    if (persons === undefined) {
 | 
						|
        return reply_to;
 | 
						|
    }
 | 
						|
 | 
						|
    const needs_patch = persons.some((person) => person.user_id === user_id);
 | 
						|
 | 
						|
    if (!needs_patch) {
 | 
						|
        return reply_to;
 | 
						|
    }
 | 
						|
 | 
						|
    emails = persons.map((person) => {
 | 
						|
        if (person.user_id === user_id) {
 | 
						|
            return new_email;
 | 
						|
        }
 | 
						|
        return person.email;
 | 
						|
    });
 | 
						|
 | 
						|
    return emails.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function pm_with_operand_ids(operand: string): number[] | undefined {
 | 
						|
    let emails = operand.split(",");
 | 
						|
    emails = emails.map((email) => email.trim());
 | 
						|
    let persons = util.try_parse_as_truthy(emails.map((email) => people_dict.get(email)));
 | 
						|
 | 
						|
    if (persons === undefined) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    // If your email is included in a group direct message with other people,
 | 
						|
    // then ignore it.
 | 
						|
    if (persons.length > 1) {
 | 
						|
        const my_user = people_by_user_id_dict.get(my_user_id);
 | 
						|
        persons = persons.filter((person) => person !== my_user);
 | 
						|
    }
 | 
						|
 | 
						|
    let user_ids = persons.map((person) => person.user_id);
 | 
						|
 | 
						|
    user_ids = sort_numerically(user_ids);
 | 
						|
 | 
						|
    return user_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function emails_to_slug(emails_string: string): string | undefined {
 | 
						|
    let slug = reply_to_to_user_ids_string(emails_string);
 | 
						|
 | 
						|
    if (!slug) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    slug += "-";
 | 
						|
 | 
						|
    const emails = emails_string.split(",");
 | 
						|
 | 
						|
    if (emails.length === 1) {
 | 
						|
        const person = get_by_email(emails[0]);
 | 
						|
        assert(person !== undefined, "Unknown person in emails_to_slug");
 | 
						|
        const name = person.full_name;
 | 
						|
        slug += name.replaceAll(/[ "%/<>`\p{C}]+/gu, "-");
 | 
						|
    } else {
 | 
						|
        slug += "group";
 | 
						|
    }
 | 
						|
 | 
						|
    return slug;
 | 
						|
}
 | 
						|
 | 
						|
export function slug_to_emails(slug: string): string | undefined {
 | 
						|
    /*
 | 
						|
        It's not super important to be flexible about
 | 
						|
        direct message related slugs, since you would
 | 
						|
        rarely post them to the web, but we do want
 | 
						|
        to support reasonable variations:
 | 
						|
 | 
						|
            99-alice@example.com
 | 
						|
            99
 | 
						|
 | 
						|
        Our canonical version is 99-alice@example.com,
 | 
						|
        and we only care about the "99" prefix.
 | 
						|
    */
 | 
						|
    const m = /^([\d,]+)(-.*)?/.exec(slug);
 | 
						|
    if (m) {
 | 
						|
        let user_ids_string = m[1];
 | 
						|
        user_ids_string = exclude_me_from_string(user_ids_string);
 | 
						|
        return user_ids_string_to_emails_string(user_ids_string);
 | 
						|
    }
 | 
						|
    /* istanbul ignore next */
 | 
						|
    return undefined;
 | 
						|
}
 | 
						|
 | 
						|
export function exclude_me_from_string(user_ids_string: string): string {
 | 
						|
    // Exclude me from a user_ids_string UNLESS I'm the
 | 
						|
    // only one in it.
 | 
						|
    let user_ids = split_to_ints(user_ids_string);
 | 
						|
 | 
						|
    if (user_ids.length <= 1) {
 | 
						|
        // We either have a message to ourself, an empty
 | 
						|
        // slug, or a message to somebody else where we weren't
 | 
						|
        // part of the slug.
 | 
						|
        return user_ids.join(",");
 | 
						|
    }
 | 
						|
 | 
						|
    user_ids = user_ids.filter((user_id) => !is_my_user_id(user_id));
 | 
						|
 | 
						|
    return user_ids.join(",");
 | 
						|
}
 | 
						|
 | 
						|
export function format_small_avatar_url(raw_url: string): string {
 | 
						|
    const url = new URL(raw_url, location.origin);
 | 
						|
    url.search += (url.search ? "&" : "") + "s=50";
 | 
						|
    return url.href;
 | 
						|
}
 | 
						|
 | 
						|
export function sender_is_bot(message: Message): boolean {
 | 
						|
    if (message.sender_id) {
 | 
						|
        const person = get_by_user_id(message.sender_id);
 | 
						|
        return person.is_bot;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
export function sender_is_guest(message: Message): boolean {
 | 
						|
    if (message.sender_id) {
 | 
						|
        const person = get_by_user_id(message.sender_id);
 | 
						|
        return person.is_guest;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
export function is_valid_bot_user(user_id: number): boolean {
 | 
						|
    const user = maybe_get_user_by_id(user_id, true);
 | 
						|
    return user !== undefined && user.is_bot;
 | 
						|
}
 | 
						|
 | 
						|
export function should_add_guest_user_indicator(user_id: number): boolean {
 | 
						|
    if (!realm.realm_enable_guest_user_indicator) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const user = get_by_user_id(user_id);
 | 
						|
    return user.is_guest;
 | 
						|
}
 | 
						|
 | 
						|
export function user_can_direct_message(recipient_ids_string: string): boolean {
 | 
						|
    // Common function for checking if a user can send a direct
 | 
						|
    // message to the target user (or group of users) represented by a
 | 
						|
    // user ids string.
 | 
						|
 | 
						|
    // Regardless of policy, we allow sending direct messages to bots and to self.
 | 
						|
    const recipient_ids = user_ids_string_to_ids_array(recipient_ids_string);
 | 
						|
    if (
 | 
						|
        recipient_ids.length === 1 &&
 | 
						|
        (is_valid_bot_user(recipient_ids[0]) || is_my_user_id(recipient_ids[0]))
 | 
						|
    ) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
        realm.realm_private_message_policy ===
 | 
						|
        settings_config.private_message_policy_values.disabled.code
 | 
						|
    ) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
function gravatar_url_for_email(email: string): string {
 | 
						|
    const hash = md5(email.toLowerCase());
 | 
						|
    const avatar_url = "https://secure.gravatar.com/avatar/" + hash + "?d=identicon";
 | 
						|
    const small_avatar_url = format_small_avatar_url(avatar_url);
 | 
						|
    return small_avatar_url;
 | 
						|
}
 | 
						|
 | 
						|
export function small_avatar_url_for_person(person: User): string {
 | 
						|
    if (person.avatar_url) {
 | 
						|
        return format_small_avatar_url(person.avatar_url);
 | 
						|
    }
 | 
						|
 | 
						|
    if (person.avatar_url === null) {
 | 
						|
        return gravatar_url_for_email(person.email);
 | 
						|
    }
 | 
						|
 | 
						|
    return format_small_avatar_url(`/avatar/${person.user_id}`);
 | 
						|
}
 | 
						|
 | 
						|
function medium_gravatar_url_for_email(email: string): string {
 | 
						|
    const hash = md5(email.toLowerCase());
 | 
						|
    const avatar_url = "https://secure.gravatar.com/avatar/" + hash + "?d=identicon";
 | 
						|
    const url = new URL(avatar_url, location.origin);
 | 
						|
    url.search += (url.search ? "&" : "") + "s=500";
 | 
						|
    return url.href;
 | 
						|
}
 | 
						|
 | 
						|
export function medium_avatar_url_for_person(person: User): string {
 | 
						|
    /* Unlike the small avatar URL case, we don't generally have a
 | 
						|
     * medium avatar URL included in person objects. So only have the
 | 
						|
     * gravatar and server endpoints here. */
 | 
						|
 | 
						|
    if (person.avatar_url === null) {
 | 
						|
        return medium_gravatar_url_for_email(person.email);
 | 
						|
    }
 | 
						|
 | 
						|
    // We need to attach a version to the URL as a cache-breaker so that the browser
 | 
						|
    // will update the image in real time when user uploads a new avatar.
 | 
						|
    //
 | 
						|
    // TODO: Newly created users sometimes are first learned about via
 | 
						|
    // the report_late_add code path; these are missing the avatar_version
 | 
						|
    // metadata. Long term, we should clean up that possibility, but
 | 
						|
    // until it is, we fallback to using a version number of 0.
 | 
						|
    return `/avatar/${person.user_id}/medium?version=${person.avatar_version ?? 0}`;
 | 
						|
}
 | 
						|
 | 
						|
export function sender_info_for_recent_view_row(sender_ids: number[]): SenderInfo[] {
 | 
						|
    const senders_info = [];
 | 
						|
    for (const id of sender_ids) {
 | 
						|
        // TODO: Better handling for optional values w/o the assertion.
 | 
						|
        const person = get_by_user_id(id);
 | 
						|
        const sender: SenderInfo = {
 | 
						|
            ...person,
 | 
						|
            avatar_url_small: small_avatar_url_for_person(person),
 | 
						|
            is_muted: muted_users.is_user_muted(id),
 | 
						|
        };
 | 
						|
        senders_info.push(sender);
 | 
						|
    }
 | 
						|
    return senders_info;
 | 
						|
}
 | 
						|
 | 
						|
export function small_avatar_url(message: Message): string {
 | 
						|
    // Try to call this function in all places where we need 25px
 | 
						|
    // avatar images, so that the browser can help
 | 
						|
    // us avoid unnecessary network trips.  (For user-uploaded avatars,
 | 
						|
    // the s=25 parameter is essentially ignored, but it's harmless.)
 | 
						|
    //
 | 
						|
    // We actually request these at s=50, so that we look better
 | 
						|
    // on retina displays.
 | 
						|
 | 
						|
    let person;
 | 
						|
    if (message.sender_id) {
 | 
						|
        // We should always have message.sender_id, except for in the
 | 
						|
        // tutorial, where it's ok to fall back to the URL in the fake
 | 
						|
        // messages.
 | 
						|
        person = maybe_get_user_by_id(message.sender_id);
 | 
						|
    }
 | 
						|
 | 
						|
    // The first time we encounter a sender in a message, we may
 | 
						|
    // not have person.avatar_url set, but if we do, then use that.
 | 
						|
    if (person?.avatar_url) {
 | 
						|
        return small_avatar_url_for_person(person);
 | 
						|
    }
 | 
						|
 | 
						|
    // Try to get info from the message if we didn't have a `person` object
 | 
						|
    // or if the avatar was missing. We do this verbosely to avoid false
 | 
						|
    // positives on line coverage (we don't do branch checking).
 | 
						|
    if (message.avatar_url) {
 | 
						|
        return format_small_avatar_url(message.avatar_url);
 | 
						|
    }
 | 
						|
 | 
						|
    if (person && person.avatar_url === undefined) {
 | 
						|
        // If we don't have an avatar_url at all, we use `GET
 | 
						|
        // /avatar/{user_id}` endpoint to obtain avatar url.  This is
 | 
						|
        // required to take advantage of the user_avatar_url_field_optional
 | 
						|
        // optimization, which saves a huge amount of network traffic on
 | 
						|
        // servers with 10,000s of user accounts.
 | 
						|
        return format_small_avatar_url(`/avatar/${person.user_id}`);
 | 
						|
    }
 | 
						|
 | 
						|
    // For computing the user's email, we first trust the person
 | 
						|
    // object since that is updated via our real-time sync system, but
 | 
						|
    // if unavailable, we use the sender email.
 | 
						|
    let email;
 | 
						|
    if (person) {
 | 
						|
        email = person.email;
 | 
						|
    } else {
 | 
						|
        email = message.sender_email;
 | 
						|
    }
 | 
						|
 | 
						|
    return gravatar_url_for_email(email);
 | 
						|
}
 | 
						|
 | 
						|
export function is_valid_email_for_compose(email: string): boolean {
 | 
						|
    if (is_cross_realm_email(email)) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const person = get_by_email(email);
 | 
						|
    if (!person || person.is_inaccessible_user) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // we allow deactivated users in compose so that
 | 
						|
    // one can attempt to reply to threads that contained them.
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
export function is_valid_bulk_emails_for_compose(emails: string[]): boolean {
 | 
						|
    // Returns false if at least one of the emails is invalid.
 | 
						|
    return emails.every((email) => {
 | 
						|
        if (!is_valid_email_for_compose(email)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function is_active_user_for_popover(user_id: number): boolean {
 | 
						|
    // For popover menus, we include cross-realm bots as active
 | 
						|
    // users.
 | 
						|
 | 
						|
    if (cross_realm_dict.get(user_id)) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
    if (active_user_dict.has(user_id)) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // TODO: We can report errors here once we start loading
 | 
						|
    //       deactivated users at page-load time. For now just warn.
 | 
						|
    if (!people_by_user_id_dict.has(user_id)) {
 | 
						|
        blueslip.warn("Unexpectedly invalid user_id in user popover query", {user_id});
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
export function is_current_user_only_owner(): boolean {
 | 
						|
    if (!current_user.is_owner) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    for (const person of active_user_dict.values()) {
 | 
						|
        if (person.is_owner && !person.is_bot && person.user_id !== my_user_id) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
export function filter_all_persons(pred: (person: User) => boolean): User[] {
 | 
						|
    const ret = [];
 | 
						|
    for (const person of people_by_user_id_dict.values()) {
 | 
						|
        if (person.is_inaccessible_user) {
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        if (pred(person)) {
 | 
						|
            ret.push(person);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return ret;
 | 
						|
}
 | 
						|
 | 
						|
export function filter_all_users(pred: (person: User) => boolean): User[] {
 | 
						|
    const ret = [];
 | 
						|
    for (const person of active_user_dict.values()) {
 | 
						|
        if (pred(person)) {
 | 
						|
            ret.push(person);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return ret;
 | 
						|
}
 | 
						|
 | 
						|
export function get_realm_users(): User[] {
 | 
						|
    // includes humans and bots from your realm
 | 
						|
    return [...active_user_dict.values()];
 | 
						|
}
 | 
						|
 | 
						|
export function get_realm_active_human_users(): User[] {
 | 
						|
    // includes ONLY humans from your realm
 | 
						|
    const humans = [];
 | 
						|
 | 
						|
    for (const user of active_user_dict.values()) {
 | 
						|
        if (!user.is_bot) {
 | 
						|
            humans.push(user);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return humans;
 | 
						|
}
 | 
						|
 | 
						|
export function get_realm_active_human_user_ids(): number[] {
 | 
						|
    const human_ids = [];
 | 
						|
 | 
						|
    for (const user of active_user_dict.values()) {
 | 
						|
        if (!user.is_bot) {
 | 
						|
            human_ids.push(user.user_id);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return human_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function get_non_active_human_ids(): number[] {
 | 
						|
    const human_ids = [];
 | 
						|
 | 
						|
    for (const user of non_active_user_dict.values()) {
 | 
						|
        if (!user.is_bot) {
 | 
						|
            human_ids.push(user.user_id);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return human_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function get_bot_ids(): number[] {
 | 
						|
    const bot_ids = [];
 | 
						|
 | 
						|
    for (const user of people_by_user_id_dict.values()) {
 | 
						|
        if (user.is_bot) {
 | 
						|
            bot_ids.push(user.user_id);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return bot_ids;
 | 
						|
}
 | 
						|
 | 
						|
export function get_active_human_count(): number {
 | 
						|
    let count = 0;
 | 
						|
    for (const person of active_user_dict.values()) {
 | 
						|
        if (!person.is_bot) {
 | 
						|
            count += 1;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return count;
 | 
						|
}
 | 
						|
 | 
						|
export function get_active_user_ids(): number[] {
 | 
						|
    // This includes active users and active bots.
 | 
						|
    return [...active_user_dict.keys()];
 | 
						|
}
 | 
						|
 | 
						|
export function get_non_active_realm_users(): User[] {
 | 
						|
    return [...non_active_user_dict.values()];
 | 
						|
}
 | 
						|
 | 
						|
export function is_cross_realm_email(email: string): boolean {
 | 
						|
    const person = get_by_email(email);
 | 
						|
    if (!person) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    return cross_realm_dict.has(person.user_id);
 | 
						|
}
 | 
						|
 | 
						|
export function get_recipient_count(person: User | PseudoMentionUser): number {
 | 
						|
    // We can have fake person objects like the "all"
 | 
						|
    // pseudo-person in at-mentions.  They will have
 | 
						|
    // the pm_recipient_count on the object itself.
 | 
						|
    if ("pm_recipient_count" in person) {
 | 
						|
        return person.pm_recipient_count;
 | 
						|
    }
 | 
						|
 | 
						|
    /*
 | 
						|
        For searching in the search bar, we will
 | 
						|
        have true `person` objects with `user_id`.
 | 
						|
 | 
						|
        Likewise, we'll have user_id if we are
 | 
						|
        tab-completing a user to send a direct message
 | 
						|
        to (but we only get called if we're not
 | 
						|
        currently in a stream view).
 | 
						|
 | 
						|
        Finally, we'll have user_id if we are adding
 | 
						|
        people to a stream (w/typeahead).
 | 
						|
 | 
						|
    */
 | 
						|
    const count = pm_recipient_count_dict.get(person.user_id);
 | 
						|
 | 
						|
    return count ?? 0;
 | 
						|
}
 | 
						|
 | 
						|
export function incr_recipient_count(user_id: number): void {
 | 
						|
    const old_count = pm_recipient_count_dict.get(user_id) ?? 0;
 | 
						|
    pm_recipient_count_dict.set(user_id, old_count + 1);
 | 
						|
}
 | 
						|
 | 
						|
export function clear_recipient_counts_for_testing(): void {
 | 
						|
    pm_recipient_count_dict.clear();
 | 
						|
}
 | 
						|
 | 
						|
export function set_recipient_count_for_testing(user_id: number, count: number): void {
 | 
						|
    pm_recipient_count_dict.set(user_id, count);
 | 
						|
}
 | 
						|
 | 
						|
export function get_message_people(): User[] {
 | 
						|
    /*
 | 
						|
        message_people are roughly the people who have
 | 
						|
        actually sent messages that are currently
 | 
						|
        showing up on your feed. These people
 | 
						|
        are important--we give them preference
 | 
						|
        over other users in certain search
 | 
						|
        suggestions, since non-message-people are
 | 
						|
        presumably either not very active or
 | 
						|
        possibly subscribed to streams you don't
 | 
						|
        care about.
 | 
						|
 | 
						|
        message_people also includes people whom
 | 
						|
        you have sent direct messages, but look at
 | 
						|
        the message_store code to see the precise
 | 
						|
        semantics
 | 
						|
    */
 | 
						|
    const message_people = util.try_parse_as_truthy(
 | 
						|
        message_user_ids
 | 
						|
            .user_ids()
 | 
						|
            .map((user_id) => people_by_user_id_dict.get(user_id))
 | 
						|
            .filter(Boolean),
 | 
						|
    );
 | 
						|
 | 
						|
    return message_people ?? [];
 | 
						|
}
 | 
						|
 | 
						|
export function get_active_message_people(): User[] {
 | 
						|
    const message_people = get_message_people();
 | 
						|
    const active_message_people = message_people.filter((item) =>
 | 
						|
        active_user_dict.has(item.user_id),
 | 
						|
    );
 | 
						|
    return active_message_people;
 | 
						|
}
 | 
						|
 | 
						|
export function get_people_for_search_bar(query: string): User[] {
 | 
						|
    const pred = build_person_matcher(query);
 | 
						|
 | 
						|
    const message_people = get_message_people().filter((user) => !user.is_inaccessible_user);
 | 
						|
 | 
						|
    const small_results = message_people.filter((item) => pred(item));
 | 
						|
 | 
						|
    if (small_results.length >= 5) {
 | 
						|
        return small_results;
 | 
						|
    }
 | 
						|
 | 
						|
    return filter_all_persons(pred);
 | 
						|
}
 | 
						|
 | 
						|
export function build_termlet_matcher(termlet: string): (user: User) => boolean {
 | 
						|
    termlet = termlet.trim();
 | 
						|
 | 
						|
    const is_ascii = /^[a-z]+$/.test(termlet);
 | 
						|
 | 
						|
    return function (user: User): boolean {
 | 
						|
        let full_name = user.full_name;
 | 
						|
        if (is_ascii) {
 | 
						|
            // Only ignore diacritics if the query is plain ascii
 | 
						|
            full_name = typeahead.remove_diacritics(full_name);
 | 
						|
        }
 | 
						|
        const names = full_name.toLowerCase().split(" ");
 | 
						|
 | 
						|
        return names.some((name) => name.startsWith(termlet));
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
export function build_person_matcher(query: string): (user: User) => boolean {
 | 
						|
    query = query.trim();
 | 
						|
 | 
						|
    const termlets = query.toLowerCase().split(/\s+/);
 | 
						|
    const termlet_matchers = termlets.map((termlet) => build_termlet_matcher(termlet));
 | 
						|
 | 
						|
    return function (user: User): boolean {
 | 
						|
        const email = user.email.toLowerCase();
 | 
						|
 | 
						|
        if (email.startsWith(query)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        return termlet_matchers.every((matcher) => matcher(user));
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
export function filter_people_by_search_terms(
 | 
						|
    users: User[],
 | 
						|
    search_terms: string[],
 | 
						|
): Map<number, User> {
 | 
						|
    const filtered_users = new Map();
 | 
						|
 | 
						|
    // Build our matchers outside the loop to avoid some
 | 
						|
    // search overhead that is not user-specific.
 | 
						|
    const matchers = search_terms.map((search_term) => build_person_matcher(search_term));
 | 
						|
 | 
						|
    // Loop through users and populate filtered_users only
 | 
						|
    // if they include search_terms
 | 
						|
    for (const user of users) {
 | 
						|
        // Return user emails that include search terms
 | 
						|
        const match = matchers.some((matcher) => matcher(user));
 | 
						|
 | 
						|
        if (match) {
 | 
						|
            filtered_users.set(user.user_id, true);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return filtered_users;
 | 
						|
}
 | 
						|
 | 
						|
export const is_valid_full_name_and_user_id = (full_name: string, user_id: number): boolean => {
 | 
						|
    /*
 | 
						|
        This function is currently only used for checking
 | 
						|
        the mention syntax during markdown parsing. Since
 | 
						|
        we do not want to parse inaccessible users as
 | 
						|
        mention pill, this function returns false for
 | 
						|
        inaccessible users. We would need to update this
 | 
						|
        if we would want to use this function for other
 | 
						|
        cases where we might want to display inaccessible
 | 
						|
        users as "Unknown user".
 | 
						|
    */
 | 
						|
    const person = people_by_user_id_dict.get(user_id);
 | 
						|
 | 
						|
    if (!person || person.is_inaccessible_user) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return person.full_name === full_name;
 | 
						|
};
 | 
						|
 | 
						|
export const get_actual_name_from_user_id = (user_id: number): string | undefined => {
 | 
						|
    /*
 | 
						|
        If you are dealing with user-entered data, you
 | 
						|
        should validate the user_id BEFORE calling
 | 
						|
        this function.
 | 
						|
    */
 | 
						|
    const person = people_by_user_id_dict.get(user_id);
 | 
						|
 | 
						|
    if (!person) {
 | 
						|
        blueslip.error("Unknown user_id", {user_id});
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return person.full_name;
 | 
						|
};
 | 
						|
 | 
						|
export function get_user_id_from_name(full_name: string): number | undefined {
 | 
						|
    // get_user_id_from_name('Alice Smith') === 42
 | 
						|
 | 
						|
    /*
 | 
						|
        This function is intended to be called
 | 
						|
        with a full name that is user-entered, such
 | 
						|
        a full name from a user mention.
 | 
						|
 | 
						|
        We will only return a **unique** user_id
 | 
						|
        here.  For duplicate names, our UI should
 | 
						|
        force users to disambiguate names with a
 | 
						|
        user_id and then call is_valid_full_name_and_user_id
 | 
						|
        to make sure the combo is valid.  This is
 | 
						|
        exactly what we do with mentions.
 | 
						|
    */
 | 
						|
 | 
						|
    /*
 | 
						|
        Since we do not want to parse inaccessible users as
 | 
						|
        mention pill, this function returns false for
 | 
						|
        inaccessible users. We would need to update this if
 | 
						|
        we would want to use this function for other cases
 | 
						|
        where we might want to display inaccessible users
 | 
						|
        as "Unknown user".
 | 
						|
    */
 | 
						|
 | 
						|
    const person = people_by_name_dict.get(full_name);
 | 
						|
 | 
						|
    if (!person || person.is_inaccessible_user) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    if (is_duplicate_full_name(full_name)) {
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return person.user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function track_duplicate_full_name(
 | 
						|
    full_name: string,
 | 
						|
    user_id: number,
 | 
						|
    to_remove?: boolean,
 | 
						|
): void {
 | 
						|
    let ids: Set<number>;
 | 
						|
    if (duplicate_full_name_data.has(full_name)) {
 | 
						|
        // TODO: Better handling for optional values w/o the assertion.
 | 
						|
        ids = duplicate_full_name_data.get(full_name)!;
 | 
						|
    } else {
 | 
						|
        ids = new Set();
 | 
						|
    }
 | 
						|
    if (!to_remove && user_id) {
 | 
						|
        ids.add(user_id);
 | 
						|
    }
 | 
						|
    if (to_remove && user_id) {
 | 
						|
        ids.delete(user_id);
 | 
						|
    }
 | 
						|
    duplicate_full_name_data.set(full_name, ids);
 | 
						|
}
 | 
						|
 | 
						|
export function is_duplicate_full_name(full_name: string): boolean {
 | 
						|
    const ids = duplicate_full_name_data.get(full_name);
 | 
						|
 | 
						|
    return ids !== undefined && ids.size > 1;
 | 
						|
}
 | 
						|
 | 
						|
export function get_mention_syntax(full_name: string, user_id: number, silent: boolean): string {
 | 
						|
    let mention = "";
 | 
						|
    if (silent) {
 | 
						|
        mention += "@_**";
 | 
						|
    } else {
 | 
						|
        mention += "@**";
 | 
						|
    }
 | 
						|
    mention += full_name;
 | 
						|
    if (!user_id) {
 | 
						|
        blueslip.warn("get_mention_syntax called without user_id.");
 | 
						|
    }
 | 
						|
    if (
 | 
						|
        (is_duplicate_full_name(full_name) || full_name_matches_wildcard_mention(full_name)) &&
 | 
						|
        user_id
 | 
						|
    ) {
 | 
						|
        mention += `|${user_id}`;
 | 
						|
    }
 | 
						|
    mention += "**";
 | 
						|
    return mention;
 | 
						|
}
 | 
						|
 | 
						|
function full_name_matches_wildcard_mention(full_name: string): boolean {
 | 
						|
    return ["all", "everyone", "stream"].includes(full_name);
 | 
						|
}
 | 
						|
 | 
						|
export function _add_user(person: User): void {
 | 
						|
    /*
 | 
						|
        This is common code to add any user, even
 | 
						|
        users who may be deactivated or outside
 | 
						|
        our realm (like cross-realm bots).
 | 
						|
    */
 | 
						|
    person.is_moderator = false;
 | 
						|
    if (person.role === settings_config.user_role_values.moderator.code) {
 | 
						|
        person.is_moderator = true;
 | 
						|
    }
 | 
						|
    if (person.user_id) {
 | 
						|
        people_by_user_id_dict.set(person.user_id, person);
 | 
						|
    } else {
 | 
						|
        // We eventually want to lock this down completely
 | 
						|
        // and report an error and not update other the data
 | 
						|
        // structures here, but we have a lot of edge cases
 | 
						|
        // with cross-realm bots, zephyr users, etc., deactivated
 | 
						|
        // users, where we are probably fine for now not to
 | 
						|
        // find them via user_id lookups.
 | 
						|
        blueslip.warn("No user_id provided", {email: person.email});
 | 
						|
    }
 | 
						|
 | 
						|
    track_duplicate_full_name(person.full_name, person.user_id);
 | 
						|
    people_dict.set(person.email, person);
 | 
						|
    people_by_name_dict.set(person.full_name, person);
 | 
						|
}
 | 
						|
 | 
						|
export function add_active_user(person: User): void {
 | 
						|
    active_user_dict.set(person.user_id, person);
 | 
						|
    _add_user(person);
 | 
						|
    non_active_user_dict.delete(person.user_id);
 | 
						|
}
 | 
						|
 | 
						|
export const is_person_active = (user_id: number): boolean => {
 | 
						|
    if (!people_by_user_id_dict.has(user_id)) {
 | 
						|
        blueslip.error("No user found", {user_id});
 | 
						|
    }
 | 
						|
 | 
						|
    if (cross_realm_dict.has(user_id)) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return active_user_dict.has(user_id);
 | 
						|
};
 | 
						|
 | 
						|
export function add_cross_realm_user(person: CrossRealmBot): void {
 | 
						|
    if (!people_dict.has(person.email)) {
 | 
						|
        _add_user(person);
 | 
						|
    }
 | 
						|
    cross_realm_dict.set(person.user_id, person);
 | 
						|
}
 | 
						|
 | 
						|
export function deactivate(person: User): void {
 | 
						|
    // We don't fully remove a person from all of our data
 | 
						|
    // structures, because deactivated users can be part
 | 
						|
    // of somebody's direct message list.
 | 
						|
    active_user_dict.delete(person.user_id);
 | 
						|
    non_active_user_dict.set(person.user_id, person);
 | 
						|
}
 | 
						|
 | 
						|
export function remove_inaccessible_user(user_id: number): void {
 | 
						|
    // We do not track inaccessible users in active_user_dict.
 | 
						|
    active_user_dict.delete(user_id);
 | 
						|
 | 
						|
    // Create unknown user object for the inaccessible user.
 | 
						|
    const email = "user" + user_id + "@" + realm.realm_bot_domain;
 | 
						|
    const unknown_user = make_user(user_id, email, INACCESSIBLE_USER_NAME);
 | 
						|
    _add_user(unknown_user);
 | 
						|
}
 | 
						|
 | 
						|
export function report_late_add(user_id: number, email: string): void {
 | 
						|
    // If the events system is not running, then it is expected that
 | 
						|
    // we will fetch messages from the server that were sent by users
 | 
						|
    // who don't exist in our users data set. This can happen because
 | 
						|
    // we're in the middle of a reload (and thus stopped our event
 | 
						|
    // queue polling) or because we are a spectator and never had an
 | 
						|
    // event queue in the first place.
 | 
						|
    if (reload_state.is_in_progress() || page_params.is_spectator) {
 | 
						|
        blueslip.log("Added user late", {user_id, email});
 | 
						|
    } else if (!settings_data.user_can_access_all_other_users()) {
 | 
						|
        blueslip.log("Message was sent by an inaccessible user", {user_id});
 | 
						|
    } else {
 | 
						|
        blueslip.error("Added user late", {user_id, email});
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function make_user(user_id: number, email: string, full_name: string): User {
 | 
						|
    // Used to create fake user objects for users who we see via some
 | 
						|
    // API call, such as fetching a message sent by the user, before
 | 
						|
    // we receive a full user object for the user via the events
 | 
						|
    // system.
 | 
						|
    //
 | 
						|
    // This function is an ugly hack in that it makes up a lot of
 | 
						|
    // values, but usually thesefake user objects only persist for
 | 
						|
    // less than a second before being replaced by a real user when
 | 
						|
    // the events system receives the user-created event for the new
 | 
						|
    // or newly visible user.
 | 
						|
    return {
 | 
						|
        user_id,
 | 
						|
        email,
 | 
						|
        full_name,
 | 
						|
        role: settings_config.user_role_values.member.code,
 | 
						|
        is_active: true,
 | 
						|
        is_admin: false,
 | 
						|
        is_owner: false,
 | 
						|
        is_guest: false,
 | 
						|
        is_bot: false,
 | 
						|
        is_moderator: false,
 | 
						|
        is_billing_admin: false,
 | 
						|
        // We explicitly don't set `avatar_url` for fake person objects so that fallback code
 | 
						|
        // will ask the server or compute a gravatar URL only once we need the avatar URL,
 | 
						|
        // it's important for performance that we not hash every user's email to get gravatar URLs.
 | 
						|
        avatar_url: undefined,
 | 
						|
        avatar_version: 0,
 | 
						|
        timezone: "",
 | 
						|
        date_joined: "",
 | 
						|
        delivery_email: null,
 | 
						|
        profile_data: {},
 | 
						|
        bot_type: null,
 | 
						|
        // This may lead to cases where this field is set to
 | 
						|
        // true for an accessible user also and such user would
 | 
						|
        // not be shown in the right sidebar for some time till
 | 
						|
        // the user's correct data is received from the server.
 | 
						|
        is_inaccessible_user: !settings_data.user_can_access_all_other_users(),
 | 
						|
 | 
						|
        // This property allows us to distinguish actual server person
 | 
						|
        // objects from fake person objects generated by this function.
 | 
						|
        is_missing_server_data: true,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
export function add_inaccessible_user(user_id: number): User {
 | 
						|
    const email = "user" + user_id + "@" + realm.realm_bot_domain;
 | 
						|
    const unknown_user = make_user(user_id, email, INACCESSIBLE_USER_NAME);
 | 
						|
    _add_user(unknown_user);
 | 
						|
    return unknown_user;
 | 
						|
}
 | 
						|
 | 
						|
export function get_user_by_id_assert_valid(
 | 
						|
    user_id: number,
 | 
						|
    allow_missing_user = !settings_data.user_can_access_all_other_users(),
 | 
						|
): User {
 | 
						|
    if (!allow_missing_user) {
 | 
						|
        return get_by_user_id(user_id);
 | 
						|
    }
 | 
						|
 | 
						|
    let person = maybe_get_user_by_id(user_id, true);
 | 
						|
    if (person === undefined) {
 | 
						|
        person = add_inaccessible_user(user_id);
 | 
						|
    }
 | 
						|
    return person;
 | 
						|
}
 | 
						|
 | 
						|
function get_involved_people(message: MessageWithBooleans): DisplayRecipientUser[] {
 | 
						|
    let involved_people: DisplayRecipientUser[];
 | 
						|
 | 
						|
    switch (message.type) {
 | 
						|
        case "stream":
 | 
						|
            involved_people = [
 | 
						|
                {
 | 
						|
                    full_name: message.sender_full_name,
 | 
						|
                    id: message.sender_id,
 | 
						|
                    email: message.sender_email,
 | 
						|
                    is_mirror_dummy: false,
 | 
						|
                },
 | 
						|
            ];
 | 
						|
            break;
 | 
						|
 | 
						|
        case "private":
 | 
						|
            assert(
 | 
						|
                typeof message.display_recipient !== "string",
 | 
						|
                "Private messages should have list of recipients",
 | 
						|
            );
 | 
						|
            involved_people = message.display_recipient;
 | 
						|
            break;
 | 
						|
 | 
						|
        default:
 | 
						|
            involved_people = [];
 | 
						|
    }
 | 
						|
 | 
						|
    return involved_people;
 | 
						|
}
 | 
						|
 | 
						|
export function extract_people_from_message(message: MessageWithBooleans): void {
 | 
						|
    const involved_people = get_involved_people(message);
 | 
						|
 | 
						|
    // Add new people involved in this message to the people list
 | 
						|
    for (const person of involved_people) {
 | 
						|
        if (person.unknown_local_echo_user) {
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        const user_id = person.id;
 | 
						|
 | 
						|
        if (people_by_user_id_dict.has(user_id)) {
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        report_late_add(user_id, person.email);
 | 
						|
 | 
						|
        _add_user(make_user(user_id, person.email, person.full_name));
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function safe_lower(s?: string | null): string {
 | 
						|
    return (s ?? "").toLowerCase();
 | 
						|
}
 | 
						|
 | 
						|
export function matches_user_settings_search(person: User, value: string): boolean {
 | 
						|
    const email = person.delivery_email;
 | 
						|
 | 
						|
    return safe_lower(person.full_name).includes(value) || safe_lower(email).includes(value);
 | 
						|
}
 | 
						|
 | 
						|
export function filter_for_user_settings_search(persons: User[], query: string): User[] {
 | 
						|
    /*
 | 
						|
        TODO: For large realms, we can optimize this a couple
 | 
						|
              different ways.  For realms that don't show
 | 
						|
              emails, we can make a simpler filter predicate
 | 
						|
              that works solely with full names.  And we can
 | 
						|
              also consider two-pass filters that try more
 | 
						|
              stingy criteria first, such as exact prefix
 | 
						|
              matches, before widening the search.
 | 
						|
 | 
						|
              See #13554 for more context.
 | 
						|
    */
 | 
						|
    return persons.filter((person) => matches_user_settings_search(person, query));
 | 
						|
}
 | 
						|
 | 
						|
export function maybe_incr_recipient_count(
 | 
						|
    message: MessageWithBooleans & {sent_by_me: boolean},
 | 
						|
): void {
 | 
						|
    if (message.type !== "private") {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    assert(
 | 
						|
        typeof message.display_recipient !== "string",
 | 
						|
        "Private messages should have list of recipients",
 | 
						|
    );
 | 
						|
 | 
						|
    if (!message.sent_by_me) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Track the number of direct messages we've sent to this person
 | 
						|
    // to improve autocomplete
 | 
						|
    for (const recip of message.display_recipient) {
 | 
						|
        if (recip.unknown_local_echo_user) {
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        const user_id = recip.id;
 | 
						|
        incr_recipient_count(user_id);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function set_full_name(person_obj: User, new_full_name: string): void {
 | 
						|
    if (people_by_name_dict.has(person_obj.full_name)) {
 | 
						|
        people_by_name_dict.delete(person_obj.full_name);
 | 
						|
    }
 | 
						|
    // Remove previous and add new full name to the duplicate full name tracker.
 | 
						|
    track_duplicate_full_name(person_obj.full_name, person_obj.user_id, true);
 | 
						|
    track_duplicate_full_name(new_full_name, person_obj.user_id);
 | 
						|
    people_by_name_dict.set(new_full_name, person_obj);
 | 
						|
    person_obj.full_name = new_full_name;
 | 
						|
}
 | 
						|
 | 
						|
export function set_custom_profile_field_data(
 | 
						|
    user_id: number,
 | 
						|
    field: {id: number} & ProfileData,
 | 
						|
): void {
 | 
						|
    if (field.id === undefined) {
 | 
						|
        blueslip.error("Trying to set undefined field id");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    const person = get_by_user_id(user_id);
 | 
						|
    person.profile_data[field.id] = {
 | 
						|
        value: field.value,
 | 
						|
        rendered_value: field.rendered_value,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
export function is_current_user(email?: string | null): boolean {
 | 
						|
    if (email === null || email === undefined || page_params.is_spectator) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return email.toLowerCase() === my_current_email().toLowerCase();
 | 
						|
}
 | 
						|
 | 
						|
export function initialize_current_user(user_id: number): void {
 | 
						|
    my_user_id = user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function my_full_name(): string {
 | 
						|
    const person = get_by_user_id(my_user_id);
 | 
						|
    return person.full_name;
 | 
						|
}
 | 
						|
 | 
						|
export function my_current_email(): string {
 | 
						|
    const person = get_by_user_id(my_user_id);
 | 
						|
    return person.email;
 | 
						|
}
 | 
						|
 | 
						|
export function my_current_user_id(): number {
 | 
						|
    return my_user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function my_custom_profile_data(field_id: number): ProfileData | null | undefined {
 | 
						|
    if (field_id === undefined) {
 | 
						|
        blueslip.error("Undefined field id");
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
    return get_custom_profile_data(my_user_id, field_id);
 | 
						|
}
 | 
						|
 | 
						|
export function get_custom_profile_data(user_id: number, field_id: number): ProfileData | null {
 | 
						|
    const person = get_by_user_id(user_id);
 | 
						|
    const profile_data = person.profile_data;
 | 
						|
    if (profile_data === undefined) {
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
    return profile_data[field_id];
 | 
						|
}
 | 
						|
 | 
						|
export function get_custom_fields_by_type(
 | 
						|
    user_id: number,
 | 
						|
    field_type: number,
 | 
						|
): ProfileData[] | null {
 | 
						|
    const person = get_by_user_id(user_id);
 | 
						|
    const profile_data = person.profile_data;
 | 
						|
    if (profile_data === undefined) {
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
    const filteredProfileData: ProfileData[] = [];
 | 
						|
    for (const field of realm.custom_profile_fields) {
 | 
						|
        if (field.type === field_type) {
 | 
						|
            filteredProfileData.push(profile_data[field.id]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return filteredProfileData;
 | 
						|
}
 | 
						|
 | 
						|
export function is_my_user_id(user_id: number): boolean {
 | 
						|
    return user_id === my_user_id;
 | 
						|
}
 | 
						|
 | 
						|
export function compare_by_name(a: User, b: User): number {
 | 
						|
    return util.strcmp(a.full_name, b.full_name);
 | 
						|
}
 | 
						|
 | 
						|
export function sort_but_pin_current_user_on_top(users: User[]): void {
 | 
						|
    const my_user = get_by_user_id(my_user_id);
 | 
						|
    if (users.includes(my_user)) {
 | 
						|
        users.splice(users.indexOf(my_user), 1);
 | 
						|
        users.sort(compare_by_name);
 | 
						|
        users.unshift(my_user);
 | 
						|
    } else {
 | 
						|
        users.sort(compare_by_name);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function initialize(my_user_id: number, params: PeopleParams): void {
 | 
						|
    for (const person of params.realm_users) {
 | 
						|
        add_active_user(person);
 | 
						|
    }
 | 
						|
 | 
						|
    for (const person of params.realm_non_active_users) {
 | 
						|
        non_active_user_dict.set(person.user_id, person);
 | 
						|
        _add_user(person);
 | 
						|
    }
 | 
						|
 | 
						|
    for (const person of params.cross_realm_bots) {
 | 
						|
        add_cross_realm_user(person);
 | 
						|
    }
 | 
						|
 | 
						|
    initialize_current_user(my_user_id);
 | 
						|
}
 |