From 59e78f96baa913a85068d804ca09aa829d7eb3f3 Mon Sep 17 00:00:00 2001 From: Lalit Date: Wed, 12 Jul 2023 10:52:30 +0530 Subject: [PATCH] ts: Migrate people.js to TypeScript. We use `get_by_user_id` instead of directly accessing the global dict, because when accessing person objects directly from one of the global dicts we need callers to check for undefined values, this can be fixed by using `get_by_user_id` method to get person objects because that functions makes sure to assert that we indeed have a valid user id, so it will never return undefined values. Co-authored-by: Zixuan James Li --- web/src/page_params.ts | 2 + web/src/{people.js => people.ts} | 441 +++++++++++++++++++------------ web/src/types.ts | 57 +++- web/tests/dispatch_subs.test.js | 4 +- web/tests/narrow.test.js | 5 +- web/tests/people.test.js | 4 +- web/tests/people_errors.test.js | 2 +- 7 files changed, 341 insertions(+), 174 deletions(-) rename web/src/{people.js => people.ts} (73%) diff --git a/web/src/page_params.ts b/web/src/page_params.ts index 4aabe185df..481bacd470 100644 --- a/web/src/page_params.ts +++ b/web/src/page_params.ts @@ -12,6 +12,7 @@ export const page_params: { }[]; development_environment: boolean; is_admin: boolean; + is_bot: boolean; is_guest: boolean; is_moderator: boolean; is_owner: boolean; @@ -35,6 +36,7 @@ export const page_params: { realm_name: string; realm_org_type: number; realm_plan_type: number; + realm_private_message_policy: number; realm_push_notifications_enabled: boolean; realm_sentry_key: string | undefined; realm_uri: string; diff --git a/web/src/people.js b/web/src/people.ts similarity index 73% rename from web/src/people.js rename to web/src/people.ts index cc582de421..de6428f184 100644 --- a/web/src/people.js +++ b/web/src/people.ts @@ -13,21 +13,75 @@ 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 type {DisplayRecipientUser, Message, MessageWithBooleans} from "./types"; import * as util from "./util"; -let people_dict; -let people_by_name_dict; -let people_by_user_id_dict; -let active_user_dict; -let non_active_user_dict; -let cross_realm_dict; -let pm_recipient_count_dict; -let duplicate_full_name_data; -let my_user_id; +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; + is_bot: boolean; + bot_type?: number | null; + bot_owner_id?: number | null; + role: number; + timezone: string; + avatar_url?: string | null; + avatar_version: number; + profile_data: Record; + is_missing_server_data?: boolean; // used for fake user objects. +}; + +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; +let people_by_name_dict: FoldDict; +let people_by_user_id_dict: Map; +let active_user_dict: Map; +let non_active_user_dict: Map; +let cross_realm_dict: Map; +let pm_recipient_count_dict: Map; +let duplicate_full_name_data: FoldDict>; +let my_user_id: number; // We have an init() function so that our automated tests // can easily clear data. -export function init() { +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 @@ -51,23 +105,23 @@ export function init() { // WE INITIALIZE DATA STRUCTURES HERE! init(); -export function split_to_ints(lst) { +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) { +export function get_users_from_ids(user_ids: number[]): (User | undefined)[] { 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) { +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) { +export function maybe_get_user_by_id(user_id: number): User | undefined { if (!people_by_user_id_dict.has(user_id)) { blueslip.error("Unknown user_id in maybe_get_user_by_id", {user_id}); return undefined; @@ -75,7 +129,7 @@ export function maybe_get_user_by_id(user_id) { return people_by_user_id_dict.get(user_id); } -export function validate_user_ids(user_ids) { +export function validate_user_ids(user_ids: number[]): number[] { const good_ids = []; const bad_ids = []; @@ -88,13 +142,13 @@ export function validate_user_ids(user_ids) { } if (bad_ids.length > 0) { - blueslip.warn(`We have untracked user_ids: ${bad_ids}`); + blueslip.warn("We have untracked user_ids", {bad_ids}); } return good_ids; } -export function get_by_email(email) { +export function get_by_email(email: string): User | undefined { const person = people_dict.get(email); if (!person) { @@ -110,7 +164,7 @@ export function get_by_email(email) { return person; } -export function get_bot_owner_user(user) { +export function get_bot_owner_user(user: User): User | undefined { const owner_id = user.bot_owner_id; if (owner_id === undefined || owner_id === null) { @@ -121,14 +175,14 @@ export function get_bot_owner_user(user) { return get_by_user_id(owner_id); } -export function can_admin_user(user) { +export function can_admin_user(user: User): boolean { return ( (user.is_bot && user.bot_owner_id && user.bot_owner_id === page_params.user_id) || is_my_user_id(user.user_id) ); } -export function id_matches_email_operand(user_id, email) { +export function id_matches_email_operand(user_id: number, email: string): boolean { const person = get_by_email(email); if (!person) { @@ -141,8 +195,8 @@ export function id_matches_email_operand(user_id, email) { return person.user_id === user_id; } -export function update_email(user_id, new_email) { - const person = people_by_user_id_dict.get(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); @@ -151,14 +205,14 @@ export function update_email(user_id, new_email) { // still work correctly. } -export function get_visible_email(user) { +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) { +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}); @@ -173,7 +227,7 @@ export function get_user_id(email) { return user_id; } -export function is_known_user_id(user_id) { +export function is_known_user_id(user_id: number): boolean { /* For certain low-stakes operations, such as emoji reactions, we may get a user_id that we don't know about, because the @@ -184,21 +238,25 @@ export function is_known_user_id(user_id) { return people_by_user_id_dict.has(user_id); } -export function is_known_user(user) { +export function is_known_user(user: User): boolean { return user && is_known_user_id(user.user_id); } -function sort_numerically(user_ids) { +function sort_numerically(user_ids: number[]): number[] { user_ids.sort((a, b) => a - b); return user_ids; } -export function huddle_string(message) { +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( @@ -214,13 +272,13 @@ export function huddle_string(message) { return user_ids.join(","); } -export function user_ids_string_to_emails_string(user_ids_string) { +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 && person.email; + return person?.email; }), ); @@ -236,16 +294,15 @@ export function user_ids_string_to_emails_string(user_ids_string) { return emails.join(","); } -export function user_ids_string_to_ids_array(user_ids_string) { +export function user_ids_string_to_ids_array(user_ids_string: string): number[] { const user_ids = user_ids_string.split(","); const ids = user_ids.map(Number); return ids; } -export function get_participants_from_user_ids_string(user_ids_string) { - let user_ids = user_ids_string_to_ids_array(user_ids_string); +export function get_participants_from_user_ids_string(user_ids_string: string): Set { // Convert to set to ensure there are no duplicate ids. - user_ids = new Set(user_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 @@ -255,7 +312,7 @@ export function get_participants_from_user_ids_string(user_ids_string) { return user_ids; } -export function emails_strings_to_user_ids_array(emails_string) { +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; @@ -265,7 +322,7 @@ export function emails_strings_to_user_ids_array(emails_string) { return user_ids_array; } -export function reply_to_to_user_ids_string(emails_string) { +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. @@ -274,7 +331,7 @@ export function reply_to_to_user_ids_string(emails_string) { let user_ids = util.try_parse_as_truthy( emails.map((email) => { const person = get_by_email(email); - return person && person.user_id; + return person?.user_id; }), ); @@ -287,7 +344,7 @@ export function reply_to_to_user_ids_string(emails_string) { return user_ids.join(","); } -export function emails_to_full_names_string(emails) { +export function emails_to_full_names_string(emails: string[]): string { return emails .map((email) => { email = email.trim(); @@ -300,15 +357,17 @@ export function emails_to_full_names_string(emails) { .join(", "); } -export function get_user_time_preferences(user_id) { - const user_timezone = get_by_user_id(user_id).timezone; +export function get_user_time_preferences( + user_id: number, +): settings_data.TimePreferences | undefined { + const user_timezone = get_by_user_id(user_id)!.timezone; if (user_timezone) { return settings_data.get_time_preferences(user_timezone); } return undefined; } -export function get_user_time(user_id) { +export function get_user_time(user_id: number): string | undefined { const user_pref = get_user_time_preferences(user_id); if (user_pref) { const current_date = utcToZonedTime(new Date(), user_pref.timezone); @@ -322,27 +381,26 @@ export function get_user_time(user_id) { return undefined; } -export function get_user_type(user_id) { +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) { +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) { +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 && person.user_id; + return person?.user_id; }), ); if (user_ids === undefined) { - blueslip.warn("Unknown emails: " + emails); + blueslip.warn("Unknown emails", {emails}); return undefined; } @@ -351,11 +409,11 @@ export function email_list_to_user_ids_string(emails) { return user_ids.join(","); } -export function get_full_names_for_poll_option(user_ids) { +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) { +export function get_display_full_name(user_id: number): string { const person = maybe_get_user_by_id(user_id); if (!person) { blueslip.error("Unknown user id", {user_id}); @@ -369,21 +427,25 @@ export function get_display_full_name(user_id) { return person.full_name; } -export function get_display_full_names(user_ids) { +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) { - return people_by_user_id_dict.get(user_id).full_name; +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) { +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) { +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); @@ -397,7 +459,7 @@ export function get_recipients(user_ids_string) { return names.join(", "); } -export function pm_reply_user_string(message) { +export function pm_reply_user_string(message: Message): string | undefined { const user_ids = pm_with_user_ids(message); if (!user_ids) { @@ -407,7 +469,7 @@ export function pm_reply_user_string(message) { return user_ids.join(","); } -export function pm_reply_to(message) { +export function pm_reply_to(message: Message): string | undefined { const user_ids = pm_with_user_ids(message); if (!user_ids) { @@ -430,7 +492,7 @@ export function pm_reply_to(message) { return reply_to; } -function sorted_other_user_ids(user_ids) { +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). @@ -447,7 +509,7 @@ function sorted_other_user_ids(user_ids) { return user_ids; } -export function concat_huddle(user_ids, user_id) { +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. @@ -459,7 +521,7 @@ export function concat_huddle(user_ids, user_id) { return sorted_ids.join(","); } -export function pm_lookup_key_from_user_ids(user_ids) { +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 @@ -469,16 +531,21 @@ export function pm_lookup_key_from_user_ids(user_ids) { return user_ids.join(","); } -export function pm_lookup_key(user_ids_string) { +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) { +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; @@ -490,11 +557,18 @@ export function all_user_ids_in_pm(message) { return user_ids; } -export function pm_with_user_ids(message) { +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; @@ -505,7 +579,7 @@ export function pm_with_user_ids(message) { return sorted_other_user_ids(user_ids); } -export function pm_perma_link(message) { +export function pm_perma_link(message: Message): string | undefined { const user_ids = all_user_ids_in_pm(message); if (!user_ids) { @@ -525,7 +599,7 @@ export function pm_perma_link(message) { return url; } -export function pm_with_url(message) { +export function pm_with_url(message: Message): string | undefined { const user_ids = pm_with_user_ids(message); if (!user_ids) { @@ -538,7 +612,7 @@ export function pm_with_url(message) { suffix = "group"; } else { const person = maybe_get_user_by_id(user_ids[0]); - if (person && person.full_name) { + if (person?.full_name) { suffix = person.full_name.replaceAll(/[ "%/<>`\p{C}]+/gu, "-"); } else { blueslip.error("Unknown people in message"); @@ -551,7 +625,11 @@ export function pm_with_url(message) { return url; } -export function update_email_in_reply_to(reply_to, user_id, new_email) { +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. @@ -579,7 +657,7 @@ export function update_email_in_reply_to(reply_to, user_id, new_email) { return emails.join(","); } -export function pm_with_operand_ids(operand) { +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))); @@ -602,7 +680,7 @@ export function pm_with_operand_ids(operand) { return user_ids; } -export function emails_to_slug(emails_string) { +export function emails_to_slug(emails_string: string): string | undefined { let slug = reply_to_to_user_ids_string(emails_string); if (!slug) { @@ -614,7 +692,9 @@ export function emails_to_slug(emails_string) { const emails = emails_string.split(","); if (emails.length === 1) { - const name = get_by_email(emails[0]).full_name; + 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"; @@ -623,7 +703,7 @@ export function emails_to_slug(emails_string) { return slug; } -export function slug_to_emails(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 @@ -646,7 +726,7 @@ export function slug_to_emails(slug) { return undefined; } -export function exclude_me_from_string(user_ids_string) { +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); @@ -663,13 +743,13 @@ export function exclude_me_from_string(user_ids_string) { return user_ids.join(","); } -export function format_small_avatar_url(raw_url) { +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) { +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; @@ -677,7 +757,7 @@ export function sender_is_bot(message) { return false; } -export function sender_is_guest(message) { +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; @@ -685,12 +765,12 @@ export function sender_is_guest(message) { return false; } -export function user_is_bot(user_id) { +export function user_is_bot(user_id: number): boolean { const user = get_by_user_id(user_id); return user.is_bot; } -export function user_can_direct_message(recipient_ids_string) { +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. @@ -710,14 +790,14 @@ export function user_can_direct_message(recipient_ids_string) { return true; } -function gravatar_url_for_email(email) { +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) { +export function small_avatar_url_for_person(person: User): string { if (person.avatar_url) { return format_small_avatar_url(person.avatar_url); } @@ -726,10 +806,10 @@ export function small_avatar_url_for_person(person) { return gravatar_url_for_email(person.email); } - return format_small_avatar_url("/avatar/" + person.user_id); + return format_small_avatar_url(`/avatar/${person.user_id}`); } -function medium_gravatar_url_for_email(email) { +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); @@ -737,7 +817,7 @@ function medium_gravatar_url_for_email(email) { return url.href; } -export function medium_avatar_url_for_person(person) { +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. */ @@ -756,18 +836,22 @@ export function medium_avatar_url_for_person(person) { return `/avatar/${person.user_id}/medium?version=${person.avatar_version ?? 0}`; } -export function sender_info_for_recent_topics_row(sender_ids) { +export function sender_info_for_recent_topics_row(sender_ids: number[]): SenderInfo[] { const senders_info = []; for (const id of sender_ids) { - const sender = {...get_by_user_id(id)}; - sender.avatar_url_small = small_avatar_url_for_person(sender); - sender.is_muted = muted_users.is_user_muted(id); + // 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) { +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, @@ -786,7 +870,7 @@ export function small_avatar_url(message) { // 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 && person.avatar_url) { + if (person?.avatar_url) { return small_avatar_url_for_person(person); } @@ -803,7 +887,7 @@ export function small_avatar_url(message) { // 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); + return format_small_avatar_url(`/avatar/${person.user_id}`); } // For computing the user's email, we first trust the person @@ -819,7 +903,7 @@ export function small_avatar_url(message) { return gravatar_url_for_email(email); } -export function is_valid_email_for_compose(email) { +export function is_valid_email_for_compose(email: string): boolean { if (is_cross_realm_email(email)) { return true; } @@ -834,7 +918,7 @@ export function is_valid_email_for_compose(email) { return true; } -export function is_valid_bulk_emails_for_compose(emails) { +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)) { @@ -844,7 +928,7 @@ export function is_valid_bulk_emails_for_compose(emails) { }); } -export function is_active_user_for_popover(user_id) { +export function is_active_user_for_popover(user_id: number): boolean { // For popover menus, we include cross-realm bots as active // users. @@ -858,13 +942,13 @@ export function is_active_user_for_popover(user_id) { // 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); + blueslip.warn("Unexpectedly invalid user_id in user popover query", {user_id}); } return false; } -export function is_current_user_only_owner() { +export function is_current_user_only_owner(): boolean { if (!page_params.is_owner || page_params.is_bot) { return false; } @@ -881,7 +965,7 @@ export function is_current_user_only_owner() { return true; } -export function filter_all_persons(pred) { +export function filter_all_persons(pred: (person: User) => boolean): User[] { const ret = []; for (const person of people_by_user_id_dict.values()) { if (pred(person)) { @@ -891,7 +975,7 @@ export function filter_all_persons(pred) { return ret; } -export function filter_all_users(pred) { +export function filter_all_users(pred: (person: User) => boolean): User[] { const ret = []; for (const person of active_user_dict.values()) { if (pred(person)) { @@ -901,12 +985,12 @@ export function filter_all_users(pred) { return ret; } -export function get_realm_users() { +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() { +export function get_realm_active_human_users(): User[] { // includes ONLY humans from your realm const humans = []; @@ -919,7 +1003,7 @@ export function get_realm_active_human_users() { return humans; } -export function get_realm_active_human_user_ids() { +export function get_realm_active_human_user_ids(): number[] { const human_ids = []; for (const user of active_user_dict.values()) { @@ -931,7 +1015,7 @@ export function get_realm_active_human_user_ids() { return human_ids; } -export function get_non_active_human_ids() { +export function get_non_active_human_ids(): number[] { const human_ids = []; for (const user of non_active_user_dict.values()) { @@ -943,7 +1027,7 @@ export function get_non_active_human_ids() { return human_ids; } -export function get_bot_ids() { +export function get_bot_ids(): number[] { const bot_ids = []; for (const user of people_by_user_id_dict.values()) { @@ -955,7 +1039,7 @@ export function get_bot_ids() { return bot_ids; } -export function get_active_human_count() { +export function get_active_human_count(): number { let count = 0; for (const person of active_user_dict.values()) { if (!person.is_bot) { @@ -965,16 +1049,16 @@ export function get_active_human_count() { return count; } -export function get_active_user_ids() { +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() { +export function get_non_active_realm_users(): User[] { return [...non_active_user_dict.values()]; } -export function is_cross_realm_email(email) { +export function is_cross_realm_email(email: string): boolean { const person = get_by_email(email); if (!person) { return false; @@ -982,11 +1066,11 @@ export function is_cross_realm_email(email) { return cross_realm_dict.has(person.user_id); } -export function get_recipient_count(person) { +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 (person.pm_recipient_count) { + if ("pm_recipient_count" in person) { return person.pm_recipient_count; } @@ -1005,23 +1089,23 @@ export function get_recipient_count(person) { */ const count = pm_recipient_count_dict.get(person.user_id); - return count || 0; + return count ?? 0; } -export function incr_recipient_count(user_id) { - const old_count = pm_recipient_count_dict.get(user_id) || 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() { +export function clear_recipient_counts_for_testing(): void { pm_recipient_count_dict.clear(); } -export function set_recipient_count_for_testing(user_id, count) { +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() { +export function get_message_people(): User[] { /* message_people are roughly the people who have actually sent messages that are currently @@ -1045,10 +1129,10 @@ export function get_message_people() { .filter(Boolean), ); - return message_people; + return message_people ?? []; } -export function get_active_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), @@ -1056,7 +1140,7 @@ export function get_active_message_people() { return active_message_people; } -export function get_people_for_search_bar(query) { +export function get_people_for_search_bar(query: string): User[] { const pred = build_person_matcher(query); const message_people = get_message_people(); @@ -1070,12 +1154,12 @@ export function get_people_for_search_bar(query) { return filter_all_persons(pred); } -export function build_termlet_matcher(termlet) { +export function build_termlet_matcher(termlet: string): (user: User) => boolean { termlet = termlet.trim(); const is_ascii = /^[a-z]+$/.test(termlet); - return function (user) { + return function (user: User): boolean { let full_name = user.full_name; if (is_ascii) { // Only ignore diacritics if the query is plain ascii @@ -1087,13 +1171,13 @@ export function build_termlet_matcher(termlet) { }; } -export function build_person_matcher(query) { +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) { + return function (user: User): boolean { const email = user.email.toLowerCase(); if (email.startsWith(query)) { @@ -1104,7 +1188,10 @@ export function build_person_matcher(query) { }; } -export function filter_people_by_search_terms(users, search_terms) { +export function filter_people_by_search_terms( + users: User[], + search_terms: string[], +): Map { const filtered_users = new Map(); // Build our matchers outside the loop to avoid some @@ -1116,7 +1203,7 @@ export function filter_people_by_search_terms(users, search_terms) { for (const user of users) { const person = get_by_email(user.email); // Get person object (and ignore errors) - if (!person || !person.full_name) { + if (!person?.full_name) { continue; } @@ -1131,7 +1218,7 @@ export function filter_people_by_search_terms(users, search_terms) { return filtered_users; } -export const is_valid_full_name_and_user_id = (full_name, user_id) => { +export const is_valid_full_name_and_user_id = (full_name: string, user_id: number): boolean => { const person = people_by_user_id_dict.get(user_id); if (!person) { @@ -1141,7 +1228,7 @@ export const is_valid_full_name_and_user_id = (full_name, user_id) => { return person.full_name === full_name; }; -export const get_actual_name_from_user_id = (user_id) => { +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 @@ -1157,7 +1244,7 @@ export const get_actual_name_from_user_id = (user_id) => { return person.full_name; }; -export function get_user_id_from_name(full_name) { +export function get_user_id_from_name(full_name: string): number | undefined { // get_user_id_from_name('Alice Smith') === 42 /* @@ -1186,10 +1273,15 @@ export function get_user_id_from_name(full_name) { return person.user_id; } -export function track_duplicate_full_name(full_name, user_id, to_remove) { - let ids; +export function track_duplicate_full_name( + full_name: string, + user_id: number, + to_remove?: boolean, +): void { + let ids: Set; if (duplicate_full_name_data.has(full_name)) { - ids = duplicate_full_name_data.get(full_name); + // TODO: Better handling for optional values w/o the assertion. + ids = duplicate_full_name_data.get(full_name)!; } else { ids = new Set(); } @@ -1202,13 +1294,13 @@ export function track_duplicate_full_name(full_name, user_id, to_remove) { duplicate_full_name_data.set(full_name, ids); } -export function is_duplicate_full_name(full_name) { +export function is_duplicate_full_name(full_name: string): boolean { const ids = duplicate_full_name_data.get(full_name); - return ids && ids.size > 1; + return ids !== undefined && ids.size > 1; } -export function get_mention_syntax(full_name, user_id, silent) { +export function get_mention_syntax(full_name: string, user_id: number, silent: boolean): string { let mention = ""; if (silent) { mention += "@_**"; @@ -1223,17 +1315,17 @@ export function get_mention_syntax(full_name, user_id, silent) { (is_duplicate_full_name(full_name) || full_name_matches_wildcard_mention(full_name)) && user_id ) { - mention += "|" + user_id; + mention += `|${user_id}`; } mention += "**"; return mention; } -function full_name_matches_wildcard_mention(full_name) { +function full_name_matches_wildcard_mention(full_name: string): boolean { return ["all", "everyone", "stream"].includes(full_name); } -export function _add_user(person) { +export function _add_user(person: User): void { /* This is common code to add any user, even users who may be deactivated or outside @@ -1260,13 +1352,13 @@ export function _add_user(person) { people_by_name_dict.set(person.full_name, person); } -export function add_active_user(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) => { +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}); } @@ -1278,14 +1370,14 @@ export const is_person_active = (user_id) => { return active_user_dict.has(user_id); }; -export function add_cross_realm_user(person) { +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) { +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. @@ -1293,7 +1385,7 @@ export function deactivate(person) { non_active_user_dict.set(person.user_id, person); } -export function report_late_add(user_id, email) { +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 @@ -1307,7 +1399,7 @@ export function report_late_add(user_id, email) { } } -function make_user(user_id, email, full_name) { +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 @@ -1348,8 +1440,8 @@ function make_user(user_id, email, full_name) { }; } -function get_involved_people(message) { - let involved_people; +function get_involved_people(message: MessageWithBooleans): DisplayRecipientUser[] { + let involved_people: DisplayRecipientUser[]; switch (message.type) { case "stream": @@ -1358,11 +1450,16 @@ function get_involved_people(message) { 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; @@ -1373,7 +1470,7 @@ function get_involved_people(message) { return involved_people; } -export function extract_people_from_message(message) { +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 @@ -1394,17 +1491,17 @@ export function extract_people_from_message(message) { } } -function safe_lower(s) { - return (s || "").toLowerCase(); +function safe_lower(s?: string | null): string { + return (s ?? "").toLowerCase(); } -export function matches_user_settings_search(person, value) { +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, query) { +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 @@ -1419,11 +1516,18 @@ export function filter_for_user_settings_search(persons, query) { return persons.filter((person) => matches_user_settings_search(person, query)); } -export function maybe_incr_recipient_count(message) { +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; } @@ -1440,7 +1544,7 @@ export function maybe_incr_recipient_count(message) { } } -export function set_full_name(person_obj, new_full_name) { +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); } @@ -1451,18 +1555,22 @@ export function set_full_name(person_obj, new_full_name) { person_obj.full_name = new_full_name; } -export function set_custom_profile_field_data(user_id, field) { +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; } - people_by_user_id_dict.get(user_id).profile_data[field.id] = { + 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) { +export function is_current_user(email?: string | null): boolean { if (email === null || email === undefined || page_params.is_spectator) { return false; } @@ -1470,23 +1578,25 @@ export function is_current_user(email) { return email.toLowerCase() === my_current_email().toLowerCase(); } -export function initialize_current_user(user_id) { +export function initialize_current_user(user_id: number): void { my_user_id = user_id; } -export function my_full_name() { - return people_by_user_id_dict.get(my_user_id).full_name; +export function my_full_name(): string { + const person = get_by_user_id(my_user_id); + return person.full_name; } -export function my_current_email() { - return people_by_user_id_dict.get(my_user_id).email; +export function my_current_email(): string { + const person = get_by_user_id(my_user_id); + return person.email; } -export function my_current_user_id() { +export function my_current_user_id(): number { return my_user_id; } -export function my_custom_profile_data(field_id) { +export function my_custom_profile_data(field_id: number): ProfileData | null | undefined { if (field_id === undefined) { blueslip.error("Undefined field id"); return undefined; @@ -1494,15 +1604,16 @@ export function my_custom_profile_data(field_id) { return get_custom_profile_data(my_user_id, field_id); } -export function get_custom_profile_data(user_id, field_id) { - const profile_data = people_by_user_id_dict.get(user_id).profile_data; +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 is_my_user_id(user_id) { +export function is_my_user_id(user_id: number | string): boolean { if (!user_id) { return false; } @@ -1515,12 +1626,12 @@ export function is_my_user_id(user_id) { return user_id === my_user_id; } -export function compare_by_name(a, b) { +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) { - const my_user = people_by_user_id_dict.get(my_user_id); +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); @@ -1530,7 +1641,7 @@ export function sort_but_pin_current_user_on_top(users) { } } -export function initialize(my_user_id, params) { +export function initialize(my_user_id: number, params: PeopleParams): void { for (const person of params.realm_users) { add_active_user(person); } diff --git a/web/src/types.ts b/web/src/types.ts index e424eed89b..a9f85daaf6 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -15,6 +15,7 @@ export type DisplayRecipientUser = { full_name: string; id: number; is_mirror_dummy: boolean; + unknown_local_echo_user?: boolean; }; export type DisplayRecipient = string | DisplayRecipientUser[]; @@ -38,6 +39,15 @@ export type MessageReaction = { user_id: number; }; +// TODO/typescript: Move this to submessage.js +export type Submessage = { + id: number; + sender_id: number; + message_id: number; + content: string; + msg_type: string; +}; + // TODO/typescript: Move this to server_events export type TopicLink = { text: string; @@ -63,17 +73,60 @@ export type RawMessage = { sender_realm_str: string; stream_id?: number; subject: string; - submessages: string[]; + submessages: Submessage[]; timestamp: number; topic_links: TopicLink[]; type: MessageType; flags: string[]; } & MatchedMessage; +// We add these boolean properties to Raw message in `message_store.set_message_booleans` method. +export type MessageWithBooleans = Omit & { + unread: boolean; + historical: boolean; + starred: boolean; + mentioned: boolean; + mentioned_me_directly: boolean; + wildcard_mentioned: boolean; + collapsed: boolean; + alerted: boolean; +}; + // TODO/typescript: Move this to message_store -export type Message = RawMessage & { +export type MessageCleanReaction = { + class: string; + count: number; + emoji_alt_code: boolean; + emoji_code: string; + emoji_name: string; + is_realm_emoji: boolean; + label: string; + local_id: string; + reaction_type: string; + user_ids: number[]; + vote_text: string; +}; + +// TODO/typescript: Move this to message_store +export type Message = Omit & { + // Added in `reactions.set_clean_reactions`. + clean_reactions: Map; + + // Added in `message_helper.process_new_message`. + sent_by_me: boolean; + is_private?: boolean; + is_stream?: boolean; + stream?: string; + reply_to: string; + display_reply_to?: string; + pm_with_url?: string; to_user_ids?: string; topic: string; + + // These properties are used in `message_list_view.js`. + starred_status: string; + message_reactions: MessageCleanReaction[]; + url: string; }; // TODO/typescript: Move this to server_events_dispatch diff --git a/web/tests/dispatch_subs.test.js b/web/tests/dispatch_subs.test.js index de733b3188..d05b50768f 100644 --- a/web/tests/dispatch_subs.test.js +++ b/web/tests/dispatch_subs.test.js @@ -153,7 +153,7 @@ test("peer event error handling (bad stream_ids/user_ids)", ({override}) => { }; blueslip.expect("warn", "We have untracked stream_ids: 8888,9999"); - blueslip.expect("warn", "We have untracked user_ids: 3333,4444"); + blueslip.expect("warn", "We have untracked user_ids"); dispatch(add_event); blueslip.reset(); @@ -165,7 +165,7 @@ test("peer event error handling (bad stream_ids/user_ids)", ({override}) => { }; blueslip.expect("warn", "We have untracked stream_ids: 8888,9999"); - blueslip.expect("warn", "We have untracked user_ids: 3333,4444"); + blueslip.expect("warn", "We have untracked user_ids"); dispatch(remove_event); }); diff --git a/web/tests/narrow.test.js b/web/tests/narrow.test.js index 2118a6f155..ea9bf811d1 100644 --- a/web/tests/narrow.test.js +++ b/web/tests/narrow.test.js @@ -819,10 +819,11 @@ run_test("narrow_compute_title", ({override}) => { assert.equal(narrow.compute_narrow_title(filter), "joe"); filter = new Filter([{operator: "dm", operand: "joe@example.com,sally@doesnotexist.com"}]); - blueslip.expect("warn", "Unknown emails: joe@example.com,sally@doesnotexist.com"); + blueslip.expect("warn", "Unknown emails"); assert.equal(narrow.compute_narrow_title(filter), "translated: Invalid users"); + blueslip.reset(); filter = new Filter([{operator: "dm", operand: "sally@doesnotexist.com"}]); - blueslip.expect("warn", "Unknown emails: sally@doesnotexist.com"); + blueslip.expect("warn", "Unknown emails"); assert.equal(narrow.compute_narrow_title(filter), "translated: Invalid user"); }); diff --git a/web/tests/people.test.js b/web/tests/people.test.js index ad65c5ad0b..c3d284e55a 100644 --- a/web/tests/people.test.js +++ b/web/tests/people.test.js @@ -340,7 +340,7 @@ test_people("basics", () => { assert.equal(people.get_active_human_count(), 1); // Invalid user ID returns false and warns. - blueslip.expect("warn", "Unexpectedly invalid user_id in user popover query: 123412"); + blueslip.expect("warn", "Unexpectedly invalid user_id in user popover query"); assert.equal(people.is_active_user_for_popover(123412), false); // We can still get their info for non-realm needs. @@ -1203,7 +1203,7 @@ test_people("emails_strings_to_user_ids_array", () => { let user_ids = people.emails_strings_to_user_ids_array(`${steven.email},${maria.email}`); assert.deepEqual(user_ids, [steven.user_id, maria.user_id]); - blueslip.expect("warn", "Unknown emails: dummyuser@example.com"); + blueslip.expect("warn", "Unknown emails"); user_ids = people.emails_strings_to_user_ids_array("dummyuser@example.com"); assert.equal(user_ids, undefined); }); diff --git a/web/tests/people_errors.test.js b/web/tests/people_errors.test.js index 597528cab2..8f68eaf18f 100644 --- a/web/tests/people_errors.test.js +++ b/web/tests/people_errors.test.js @@ -65,7 +65,7 @@ run_test("blueslip", () => { blueslip.expect("warn", "Unknown user ids: 1,2"); people.user_ids_string_to_emails_string("1,2"); - blueslip.expect("warn", "Unknown emails: " + unknown_email); + blueslip.expect("warn", "Unknown emails"); people.email_list_to_user_ids_string([unknown_email]); let message = {