From fb7d77545f6bf742b2929e2344ccc35441553bce Mon Sep 17 00:00:00 2001 From: evykassirer Date: Wed, 27 Dec 2023 18:08:51 -0800 Subject: [PATCH] reactions: Convert module to typescript. --- web/src/message_store.ts | 3 + web/src/{reactions.js => reactions.ts} | 206 +++++++++++++++++-------- web/tests/reactions.test.js | 3 +- 3 files changed, 148 insertions(+), 64 deletions(-) rename web/src/{reactions.js => reactions.ts} (78%) diff --git a/web/src/message_store.ts b/web/src/message_store.ts index 044d3cb5a2..ad371c4ada 100644 --- a/web/src/message_store.ts +++ b/web/src/message_store.ts @@ -1,5 +1,6 @@ import * as blueslip from "./blueslip"; import * as people from "./people"; +import type {RawReaction} from "./reactions"; import type {Submessage, TopicLink} from "./types"; const stored_messages = new Map(); @@ -106,6 +107,8 @@ export type Message = ( | Omit | Omit ) & { + // Replaced by `clean_reactions` in `reactions.set_clean_reactions`. + reactions?: RawReaction[]; // Added in `reactions.set_clean_reactions`. clean_reactions: Map; diff --git a/web/src/reactions.js b/web/src/reactions.ts similarity index 78% rename from web/src/reactions.js rename to web/src/reactions.ts index b80226f4af..1094f330bd 100644 --- a/web/src/reactions.js +++ b/web/src/reactions.ts @@ -1,33 +1,48 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; import render_message_reaction from "../templates/message_reaction.hbs"; import * as blueslip from "./blueslip"; import * as channel from "./channel"; import * as emoji from "./emoji"; +import type {EmojiRenderingDetails} from "./emoji"; import {$t} from "./i18n"; import * as message_lists from "./message_lists"; import * as message_store from "./message_store"; +import type {Message, MessageCleanReaction} from "./message_store"; import {page_params} from "./page_params"; import * as people from "./people"; import * as spectators from "./spectators"; import {current_user} from "./state_data"; import {user_settings} from "./user_settings"; -const waiting_for_server_request_ids = new Set(); +const waiting_for_server_request_ids = new Set(); -export function get_local_reaction_id(rendering_details) { +type ReactionEvent = { + message_id: number; + user_id: number; + local_id: string; + reaction_type: string; + emoji_name: string; + emoji_code: string; +}; + +export function get_local_reaction_id(rendering_details: EmojiRenderingDetails): string { return [rendering_details.reaction_type, rendering_details.emoji_code].join(","); } -export function current_user_has_reacted_to_emoji(message, local_id) { +export function current_user_has_reacted_to_emoji(message: Message, local_id: string): boolean { set_clean_reactions(message); const clean_reaction_object = message.clean_reactions.get(local_id); - return clean_reaction_object && clean_reaction_object.user_ids.includes(current_user.user_id); + return ( + clean_reaction_object !== undefined && + clean_reaction_object.user_ids.includes(current_user.user_id) + ); } -function get_message(message_id) { +function get_message(message_id: number): Message | undefined { const message = message_store.get(message_id); if (!message) { blueslip.error("reactions: Bad message id", {message_id}); @@ -38,7 +53,17 @@ function get_message(message_id) { return message; } -function create_reaction(message_id, rendering_details) { +export type RawReaction = { + emoji_name: string; + reaction_type: string; + emoji_code: string; + user_id: number; +}; + +function create_reaction( + message_id: number, + rendering_details: EmojiRenderingDetails, +): ReactionEvent { return { message_id, user_id: current_user.user_id, @@ -49,7 +74,10 @@ function create_reaction(message_id, rendering_details) { }; } -function update_ui_and_send_reaction_ajax(message_id, rendering_details) { +function update_ui_and_send_reaction_ajax( + message_id: number, + rendering_details: EmojiRenderingDetails, +): void { if (page_params.is_spectator) { // Spectators can't react, since they don't have accounts. We // stop here to avoid a confusing reaction local echo. @@ -58,6 +86,9 @@ function update_ui_and_send_reaction_ajax(message_id, rendering_details) { } const message = get_message(message_id); + if (message === undefined) { + return; + } const local_id = get_local_reaction_id(rendering_details); const has_reacted = current_user_has_reacted_to_emoji(message, local_id); const operation = has_reacted ? "remove" : "add"; @@ -83,7 +114,7 @@ function update_ui_and_send_reaction_ajax(message_id, rendering_details) { success() { waiting_for_server_request_ids.delete(reaction_request_id); }, - error(xhr) { + error(xhr: JQuery.jqXHR) { waiting_for_server_request_ids.delete(reaction_request_id); if (xhr.readyState !== 0) { if ( @@ -101,13 +132,13 @@ function update_ui_and_send_reaction_ajax(message_id, rendering_details) { waiting_for_server_request_ids.add(reaction_request_id); if (operation === "add") { - channel.post(args); + void channel.post(args); } else if (operation === "remove") { - channel.del(args); + void channel.del(args); } } -export function toggle_emoji_reaction(message_id, emoji_name) { +export function toggle_emoji_reaction(message_id: number, emoji_name: string): void { // This codepath doesn't support toggling a deactivated realm emoji. // Since a user can interact with a deactivated realm emoji only by // clicking on a reaction and that is handled by `process_reaction_click()` @@ -118,7 +149,7 @@ export function toggle_emoji_reaction(message_id, emoji_name) { update_ui_and_send_reaction_ajax(message_id, rendering_details); } -export function process_reaction_click(message_id, local_id) { +export function process_reaction_click(message_id: number, local_id: string): void { const message = get_message(message_id); if (!message) { @@ -142,26 +173,33 @@ export function process_reaction_click(message_id, local_id) { update_ui_and_send_reaction_ajax(message_id, rendering_details); } -function generate_title(emoji_name, user_ids) { +function generate_title(emoji_name: string, user_ids: number[]): string { const usernames = people.get_display_full_names( user_ids.filter((user_id) => user_id !== current_user.user_id), ); const current_user_reacted = user_ids.length !== usernames.length; - const context = { - emoji_name: ":" + emoji_name + ":", - }; + const colon_emoji_name = ":" + emoji_name + ":"; if (user_ids.length === 1) { if (current_user_reacted) { + const context = { + emoji_name: colon_emoji_name, + }; return $t({defaultMessage: "You (click to remove) reacted with {emoji_name}"}, context); } - context.username = usernames[0]; + const context = { + emoji_name: colon_emoji_name, + username: usernames[0], + }; return $t({defaultMessage: "{username} reacted with {emoji_name}"}, context); } if (user_ids.length === 2 && current_user_reacted) { - context.other_username = usernames[0]; + const context = { + emoji_name: colon_emoji_name, + other_username: usernames[0], + }; return $t( { defaultMessage: @@ -171,8 +209,11 @@ function generate_title(emoji_name, user_ids) { ); } - context.comma_separated_usernames = usernames.slice(0, -1).join(", "); - context.last_username = usernames.at(-1); + const context = { + emoji_name: colon_emoji_name, + comma_separated_usernames: usernames.slice(0, -1).join(", "), + last_username: usernames.at(-1), + }; if (current_user_reacted) { return $t( { @@ -192,10 +233,13 @@ function generate_title(emoji_name, user_ids) { } // Add a tooltip showing who reacted to a message. -export function get_reaction_title_data(message_id, local_id) { +export function get_reaction_title_data(message_id: number, local_id: string): string { const message = get_message(message_id); + assert(message !== undefined); const clean_reaction_object = message.clean_reactions.get(local_id); + assert(clean_reaction_object !== undefined); + const user_list = clean_reaction_object.user_ids; const emoji_name = clean_reaction_object.emoji_name; const title = generate_title(emoji_name, user_list); @@ -203,29 +247,29 @@ export function get_reaction_title_data(message_id, local_id) { return title; } -export function get_reaction_sections(message_id) { +export function get_reaction_sections(message_id: number): JQuery { const $rows = message_lists.all_rendered_row_for_message_id(message_id); return $rows.find(".message_reactions"); } -export function find_reaction(message_id, local_id) { +export function find_reaction(message_id: number, local_id: string): JQuery { const $reaction_section = get_reaction_sections(message_id); const $reaction = $reaction_section.find(`[data-reaction-id='${CSS.escape(local_id)}']`); return $reaction; } -export function get_add_reaction_button(message_id) { +export function get_add_reaction_button(message_id: number): JQuery { const $reaction_section = get_reaction_sections(message_id); const $add_button = $reaction_section.find(".reaction_button"); return $add_button; } -export function set_reaction_vote_text($reaction, vote_text) { +export function set_reaction_vote_text($reaction: JQuery, vote_text: string): void { const $count_element = $reaction.find(".message_reaction_count"); $count_element.text(vote_text); } -export function add_reaction(event) { +export function add_reaction(event: ReactionEvent): void { const message_id = event.message_id; const message = message_store.get(message_id); @@ -271,7 +315,11 @@ export function add_reaction(event) { } } -export function update_existing_reaction(clean_reaction_object, message, acting_user_id) { +export function update_existing_reaction( + clean_reaction_object: MessageCleanReaction, + message: Message, + acting_user_id: number, +): void { // Our caller ensures that this message already has a reaction // for this emoji and sets up our user_list. This function // simply updates the DOM. @@ -291,34 +339,38 @@ export function update_existing_reaction(clean_reaction_object, message, acting_ update_vote_text_on_message(message); } -export function insert_new_reaction(clean_reaction_object, message, user_id) { +export function insert_new_reaction( + clean_reaction_object: MessageCleanReaction, + message: Message, + user_id: number, +): void { // Our caller ensures we are the first user to react to this // message with this emoji. We then render the emoji/title/count // and insert it before the add button. - const context = { - message_id: message.id, - ...emoji.get_emoji_details_for_rendering(clean_reaction_object), - }; - + const emoji_details = emoji.get_emoji_details_for_rendering(clean_reaction_object); const new_label = generate_title( clean_reaction_object.emoji_name, clean_reaction_object.user_ids, ); - context.count = 1; - context.label = new_label; - context.local_id = get_local_reaction_id(clean_reaction_object); - context.emoji_alt_code = user_settings.emojiset === "text"; - context.is_realm_emoji = - context.reaction_type === "realm_emoji" || context.reaction_type === "zulip_extra_emoji"; - context.vote_text = ""; // Updated below + const is_realm_emoji = + emoji_details.reaction_type === "realm_emoji" || + emoji_details.reaction_type === "zulip_extra_emoji"; + const reaction_class = + user_id === current_user.user_id ? "message_reaction reacted" : "message_reaction"; - if (user_id === current_user.user_id) { - context.class = "message_reaction reacted"; - } else { - context.class = "message_reaction"; - } + const context = { + message_id: message.id, + ...emoji_details, + count: 1, + label: new_label, + local_id: get_local_reaction_id(clean_reaction_object), + emoji_alt_code: user_settings.emojiset === "text", + is_realm_emoji, + vote_text: "", // Updated below + class: reaction_class, + }; const $new_reaction = $(render_message_reaction(context)); @@ -329,7 +381,7 @@ export function insert_new_reaction(clean_reaction_object, message, user_id) { update_vote_text_on_message(message); } -export function remove_reaction(event) { +export function remove_reaction(event: ReactionEvent): void { const message_id = event.message_id; const user_id = event.user_id; const message = message_store.get(message_id); @@ -366,7 +418,11 @@ export function remove_reaction(event) { remove_reaction_from_view(clean_reaction_object, message, user_id); } -export function remove_reaction_from_view(clean_reaction_object, message, user_id) { +export function remove_reaction_from_view( + clean_reaction_object: MessageCleanReaction, + message: Message, + user_id: number, +): void { const local_id = get_local_reaction_id(clean_reaction_object); const $reaction = find_reaction(message.id, local_id); const reaction_count = clean_reaction_object.user_ids.length; @@ -394,9 +450,11 @@ export function remove_reaction_from_view(clean_reaction_object, message, user_i update_vote_text_on_message(message); } -export function get_emojis_used_by_user_for_message_id(message_id) { +export function get_emojis_used_by_user_for_message_id(message_id: number): string[] { const user_id = current_user.user_id; + assert(user_id !== undefined); const message = message_store.get(message_id); + assert(message !== undefined); set_clean_reactions(message); const names = []; @@ -409,12 +467,12 @@ export function get_emojis_used_by_user_for_message_id(message_id) { return names; } -export function get_message_reactions(message) { +export function get_message_reactions(message: Message): MessageCleanReaction[] { set_clean_reactions(message); return [...message.clean_reactions.values()]; } -export function set_clean_reactions(message) { +export function set_clean_reactions(message: Message): void { /* set_clean_reactions processes the raw message.reactions object, which will contain one object for each individual reaction, even @@ -441,8 +499,9 @@ export function set_clean_reactions(message) { // This first loop creates a temporary distinct_reactions data // structure, which will accumulate the set of users who have // reacted with each distinct reaction. - const distinct_reactions = new Map(); - const user_map = new Map(); + assert(message.reactions !== undefined); + const distinct_reactions = new Map(); + const user_map = new Map(); for (const reaction of message.reactions) { const local_id = get_local_reaction_id(reaction); const user_id = reaction.user_id; @@ -452,7 +511,7 @@ export function set_clean_reactions(message) { user_map.set(local_id, []); } - const user_ids = user_map.get(local_id); + const user_ids = user_map.get(local_id)!; if (user_ids.includes(user_id)) { blueslip.error("server sent duplicate reactions", {user_id, local_id}); @@ -471,6 +530,7 @@ export function set_clean_reactions(message) { const reaction_counts_and_user_ids = [...distinct_reactions.keys()].map((local_id) => { const user_ids = user_map.get(local_id); + assert(user_ids !== undefined); return { count: user_ids.length, user_ids, @@ -480,7 +540,9 @@ export function set_clean_reactions(message) { for (const local_id of distinct_reactions.keys()) { const reaction = distinct_reactions.get(local_id); + assert(reaction !== undefined); const user_ids = user_map.get(local_id); + assert(user_ids !== undefined); message.clean_reactions.set( local_id, @@ -502,7 +564,14 @@ function make_clean_reaction({ emoji_code, reaction_type, should_display_reactors, -}) { +}: { + local_id: string; + user_ids: number[]; + emoji_name: string; + emoji_code: string; + reaction_type: string; + should_display_reactors: boolean; +}): MessageCleanReaction { const emoji_details = emoji.get_emoji_details_for_rendering({ emoji_name, emoji_code, @@ -537,7 +606,10 @@ function make_clean_reaction({ }; } -export function update_user_fields(clean_reaction_object, should_display_reactors) { +export function update_user_fields( + clean_reaction_object: MessageCleanReaction, + should_display_reactors: boolean, +): void { // update_user_fields needs to be called whenever the set of users // who reacted on a message might have changed, including due to // upvote/downvotes on ANY reaction in the message, because those @@ -547,6 +619,7 @@ export function update_user_fields(clean_reaction_object, should_display_reactor clean_reaction_object.emoji_name, clean_reaction_object.user_ids, ); + if (clean_reaction_object.user_ids.includes(current_user.user_id)) { clean_reaction_object.class = "message_reaction reacted"; } else { @@ -567,33 +640,40 @@ export function update_user_fields(clean_reaction_object, should_display_reactor ); } -function get_reaction_counts_and_user_ids(message) { +type ReactionUserIdAndCount = { + count?: number; + user_ids: number[]; +}; + +function get_reaction_counts_and_user_ids(message: Message): ReactionUserIdAndCount[] { return [...message.clean_reactions.values()].map((reaction) => ({ count: reaction.count, user_ids: reaction.user_ids, })); } -export function get_vote_text(user_ids, should_display_reactors) { +export function get_vote_text(user_ids: number[], should_display_reactors: boolean): string { if (should_display_reactors) { return comma_separated_usernames(user_ids); } return `${user_ids.length}`; } -function check_should_display_reactors(reaction_counts_and_user_ids) { +function check_should_display_reactors( + reaction_counts_and_user_ids: ReactionUserIdAndCount[], +): boolean { if (!user_settings.display_emoji_reaction_users) { return false; } let total_reactions = 0; for (const {count, user_ids} of reaction_counts_and_user_ids) { - total_reactions += count || user_ids.length; + total_reactions += count ?? user_ids.length; } return total_reactions <= 3; } -function comma_separated_usernames(user_list) { +function comma_separated_usernames(user_list: number[]): string { const usernames = people.get_display_full_names(user_list); const current_user_has_reacted = user_list.includes(current_user.user_id); @@ -607,7 +687,7 @@ function comma_separated_usernames(user_list) { return comma_separated_usernames; } -export function update_vote_text_on_message(message) { +export function update_vote_text_on_message(message: Message): void { // Because whether we display a count or the names of reacting // users depends on total reactions on the message, we need to // recalculate this whenever adjusting reaction rendering on a @@ -618,7 +698,9 @@ export function update_vote_text_on_message(message) { for (const [reaction, clean_reaction] of message.clean_reactions.entries()) { const reaction_elem = find_reaction(message.id, clean_reaction.local_id); const vote_text = get_vote_text(clean_reaction.user_ids, should_display_reactors); - message.clean_reactions.get(reaction).vote_text = vote_text; + const message_clean_reaction = message.clean_reactions.get(reaction); + assert(message_clean_reaction !== undefined); + message_clean_reaction.vote_text = vote_text; set_reaction_vote_text(reaction_elem, vote_text); } } diff --git a/web/tests/reactions.test.js b/web/tests/reactions.test.js index cb4e5c18c5..f549bbc0b2 100644 --- a/web/tests/reactions.test.js +++ b/web/tests/reactions.test.js @@ -1181,7 +1181,7 @@ test("remove_reaction_from_view (last person)", () => { assert.ok(removed); }); -test("error_handling", ({override, override_rewire}) => { +test("error_handling", ({override}) => { override(message_store, "get", noop); blueslip.expect("error", "reactions: Bad message id"); @@ -1193,7 +1193,6 @@ test("error_handling", ({override, override_rewire}) => { emoji_code: "991", user_id: 99, }; - override_rewire(reactions, "current_user_has_reacted_to_emoji", () => true); reactions.toggle_emoji_reaction(55, bogus_event.emoji_name); reactions.add_reaction(bogus_event);