reactions: Convert module to typescript.

This commit is contained in:
evykassirer 2023-12-27 18:08:51 -08:00 committed by Tim Abbott
parent 07671997ca
commit fb7d77545f
3 changed files with 148 additions and 64 deletions

View File

@ -1,5 +1,6 @@
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as people from "./people"; import * as people from "./people";
import type {RawReaction} from "./reactions";
import type {Submessage, TopicLink} from "./types"; import type {Submessage, TopicLink} from "./types";
const stored_messages = new Map(); const stored_messages = new Map();
@ -106,6 +107,8 @@ export type Message = (
| Omit<MessageWithBooleans & {type: "private"}, "reactions"> | Omit<MessageWithBooleans & {type: "private"}, "reactions">
| Omit<MessageWithBooleans & {type: "stream"}, "reactions"> | Omit<MessageWithBooleans & {type: "stream"}, "reactions">
) & { ) & {
// Replaced by `clean_reactions` in `reactions.set_clean_reactions`.
reactions?: RawReaction[];
// Added in `reactions.set_clean_reactions`. // Added in `reactions.set_clean_reactions`.
clean_reactions: Map<string, MessageCleanReaction>; clean_reactions: Map<string, MessageCleanReaction>;

View File

@ -1,33 +1,48 @@
import $ from "jquery"; import $ from "jquery";
import assert from "minimalistic-assert";
import render_message_reaction from "../templates/message_reaction.hbs"; import render_message_reaction from "../templates/message_reaction.hbs";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as channel from "./channel"; import * as channel from "./channel";
import * as emoji from "./emoji"; import * as emoji from "./emoji";
import type {EmojiRenderingDetails} from "./emoji";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as message_store from "./message_store"; import * as message_store from "./message_store";
import type {Message, MessageCleanReaction} from "./message_store";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as people from "./people"; import * as people from "./people";
import * as spectators from "./spectators"; import * as spectators from "./spectators";
import {current_user} from "./state_data"; import {current_user} from "./state_data";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
const waiting_for_server_request_ids = new Set(); const waiting_for_server_request_ids = new Set<string>();
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(","); 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); set_clean_reactions(message);
const clean_reaction_object = message.clean_reactions.get(local_id); 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); const message = message_store.get(message_id);
if (!message) { if (!message) {
blueslip.error("reactions: Bad message id", {message_id}); blueslip.error("reactions: Bad message id", {message_id});
@ -38,7 +53,17 @@ function get_message(message_id) {
return message; 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 { return {
message_id, message_id,
user_id: current_user.user_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) { if (page_params.is_spectator) {
// Spectators can't react, since they don't have accounts. We // Spectators can't react, since they don't have accounts. We
// stop here to avoid a confusing reaction local echo. // 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); const message = get_message(message_id);
if (message === undefined) {
return;
}
const local_id = get_local_reaction_id(rendering_details); const local_id = get_local_reaction_id(rendering_details);
const has_reacted = current_user_has_reacted_to_emoji(message, local_id); const has_reacted = current_user_has_reacted_to_emoji(message, local_id);
const operation = has_reacted ? "remove" : "add"; const operation = has_reacted ? "remove" : "add";
@ -83,7 +114,7 @@ function update_ui_and_send_reaction_ajax(message_id, rendering_details) {
success() { success() {
waiting_for_server_request_ids.delete(reaction_request_id); waiting_for_server_request_ids.delete(reaction_request_id);
}, },
error(xhr) { error(xhr: JQuery.jqXHR) {
waiting_for_server_request_ids.delete(reaction_request_id); waiting_for_server_request_ids.delete(reaction_request_id);
if (xhr.readyState !== 0) { if (xhr.readyState !== 0) {
if ( 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); waiting_for_server_request_ids.add(reaction_request_id);
if (operation === "add") { if (operation === "add") {
channel.post(args); void channel.post(args);
} else if (operation === "remove") { } 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. // This codepath doesn't support toggling a deactivated realm emoji.
// Since a user can interact with a deactivated realm emoji only by // Since a user can interact with a deactivated realm emoji only by
// clicking on a reaction and that is handled by `process_reaction_click()` // 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); 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); const message = get_message(message_id);
if (!message) { 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); 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( const usernames = people.get_display_full_names(
user_ids.filter((user_id) => user_id !== current_user.user_id), user_ids.filter((user_id) => user_id !== current_user.user_id),
); );
const current_user_reacted = user_ids.length !== usernames.length; const current_user_reacted = user_ids.length !== usernames.length;
const context = { const colon_emoji_name = ":" + emoji_name + ":";
emoji_name: ":" + emoji_name + ":",
};
if (user_ids.length === 1) { if (user_ids.length === 1) {
if (current_user_reacted) { if (current_user_reacted) {
const context = {
emoji_name: colon_emoji_name,
};
return $t({defaultMessage: "You (click to remove) reacted with {emoji_name}"}, context); 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); return $t({defaultMessage: "{username} reacted with {emoji_name}"}, context);
} }
if (user_ids.length === 2 && current_user_reacted) { 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( return $t(
{ {
defaultMessage: defaultMessage:
@ -171,8 +209,11 @@ function generate_title(emoji_name, user_ids) {
); );
} }
context.comma_separated_usernames = usernames.slice(0, -1).join(", "); const context = {
context.last_username = usernames.at(-1); emoji_name: colon_emoji_name,
comma_separated_usernames: usernames.slice(0, -1).join(", "),
last_username: usernames.at(-1),
};
if (current_user_reacted) { if (current_user_reacted) {
return $t( return $t(
{ {
@ -192,10 +233,13 @@ function generate_title(emoji_name, user_ids) {
} }
// Add a tooltip showing who reacted to a message. // 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); const message = get_message(message_id);
assert(message !== undefined);
const clean_reaction_object = message.clean_reactions.get(local_id); const clean_reaction_object = message.clean_reactions.get(local_id);
assert(clean_reaction_object !== undefined);
const user_list = clean_reaction_object.user_ids; const user_list = clean_reaction_object.user_ids;
const emoji_name = clean_reaction_object.emoji_name; const emoji_name = clean_reaction_object.emoji_name;
const title = generate_title(emoji_name, user_list); const title = generate_title(emoji_name, user_list);
@ -203,29 +247,29 @@ export function get_reaction_title_data(message_id, local_id) {
return title; 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); const $rows = message_lists.all_rendered_row_for_message_id(message_id);
return $rows.find(".message_reactions"); 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_section = get_reaction_sections(message_id);
const $reaction = $reaction_section.find(`[data-reaction-id='${CSS.escape(local_id)}']`); const $reaction = $reaction_section.find(`[data-reaction-id='${CSS.escape(local_id)}']`);
return $reaction; 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 $reaction_section = get_reaction_sections(message_id);
const $add_button = $reaction_section.find(".reaction_button"); const $add_button = $reaction_section.find(".reaction_button");
return $add_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"); const $count_element = $reaction.find(".message_reaction_count");
$count_element.text(vote_text); $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_id = event.message_id;
const message = message_store.get(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 // Our caller ensures that this message already has a reaction
// for this emoji and sets up our user_list. This function // for this emoji and sets up our user_list. This function
// simply updates the DOM. // simply updates the DOM.
@ -291,34 +339,38 @@ export function update_existing_reaction(clean_reaction_object, message, acting_
update_vote_text_on_message(message); 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 // Our caller ensures we are the first user to react to this
// message with this emoji. We then render the emoji/title/count // message with this emoji. We then render the emoji/title/count
// and insert it before the add button. // and insert it before the add button.
const context = { const emoji_details = emoji.get_emoji_details_for_rendering(clean_reaction_object);
message_id: message.id,
...emoji.get_emoji_details_for_rendering(clean_reaction_object),
};
const new_label = generate_title( const new_label = generate_title(
clean_reaction_object.emoji_name, clean_reaction_object.emoji_name,
clean_reaction_object.user_ids, clean_reaction_object.user_ids,
); );
context.count = 1; const is_realm_emoji =
context.label = new_label; emoji_details.reaction_type === "realm_emoji" ||
context.local_id = get_local_reaction_id(clean_reaction_object); emoji_details.reaction_type === "zulip_extra_emoji";
context.emoji_alt_code = user_settings.emojiset === "text"; const reaction_class =
context.is_realm_emoji = user_id === current_user.user_id ? "message_reaction reacted" : "message_reaction";
context.reaction_type === "realm_emoji" || context.reaction_type === "zulip_extra_emoji";
context.vote_text = ""; // Updated below
if (user_id === current_user.user_id) { const context = {
context.class = "message_reaction reacted"; message_id: message.id,
} else { ...emoji_details,
context.class = "message_reaction"; 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)); 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); 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 message_id = event.message_id;
const user_id = event.user_id; const user_id = event.user_id;
const message = message_store.get(message_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); 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 local_id = get_local_reaction_id(clean_reaction_object);
const $reaction = find_reaction(message.id, local_id); const $reaction = find_reaction(message.id, local_id);
const reaction_count = clean_reaction_object.user_ids.length; 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); 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; const user_id = current_user.user_id;
assert(user_id !== undefined);
const message = message_store.get(message_id); const message = message_store.get(message_id);
assert(message !== undefined);
set_clean_reactions(message); set_clean_reactions(message);
const names = []; const names = [];
@ -409,12 +467,12 @@ export function get_emojis_used_by_user_for_message_id(message_id) {
return names; return names;
} }
export function get_message_reactions(message) { export function get_message_reactions(message: Message): MessageCleanReaction[] {
set_clean_reactions(message); set_clean_reactions(message);
return [...message.clean_reactions.values()]; 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, set_clean_reactions processes the raw message.reactions object,
which will contain one object for each individual reaction, even 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 // This first loop creates a temporary distinct_reactions data
// structure, which will accumulate the set of users who have // structure, which will accumulate the set of users who have
// reacted with each distinct reaction. // reacted with each distinct reaction.
const distinct_reactions = new Map(); assert(message.reactions !== undefined);
const user_map = new Map(); const distinct_reactions = new Map<string, RawReaction>();
const user_map = new Map<string, number[]>();
for (const reaction of message.reactions) { for (const reaction of message.reactions) {
const local_id = get_local_reaction_id(reaction); const local_id = get_local_reaction_id(reaction);
const user_id = reaction.user_id; const user_id = reaction.user_id;
@ -452,7 +511,7 @@ export function set_clean_reactions(message) {
user_map.set(local_id, []); 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)) { if (user_ids.includes(user_id)) {
blueslip.error("server sent duplicate reactions", {user_id, local_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 reaction_counts_and_user_ids = [...distinct_reactions.keys()].map((local_id) => {
const user_ids = user_map.get(local_id); const user_ids = user_map.get(local_id);
assert(user_ids !== undefined);
return { return {
count: user_ids.length, count: user_ids.length,
user_ids, user_ids,
@ -480,7 +540,9 @@ export function set_clean_reactions(message) {
for (const local_id of distinct_reactions.keys()) { for (const local_id of distinct_reactions.keys()) {
const reaction = distinct_reactions.get(local_id); const reaction = distinct_reactions.get(local_id);
assert(reaction !== undefined);
const user_ids = user_map.get(local_id); const user_ids = user_map.get(local_id);
assert(user_ids !== undefined);
message.clean_reactions.set( message.clean_reactions.set(
local_id, local_id,
@ -502,7 +564,14 @@ function make_clean_reaction({
emoji_code, emoji_code,
reaction_type, reaction_type,
should_display_reactors, 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({ const emoji_details = emoji.get_emoji_details_for_rendering({
emoji_name, emoji_name,
emoji_code, 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 // update_user_fields needs to be called whenever the set of users
// who reacted on a message might have changed, including due to // who reacted on a message might have changed, including due to
// upvote/downvotes on ANY reaction in the message, because those // 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.emoji_name,
clean_reaction_object.user_ids, clean_reaction_object.user_ids,
); );
if (clean_reaction_object.user_ids.includes(current_user.user_id)) { if (clean_reaction_object.user_ids.includes(current_user.user_id)) {
clean_reaction_object.class = "message_reaction reacted"; clean_reaction_object.class = "message_reaction reacted";
} else { } 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) => ({ return [...message.clean_reactions.values()].map((reaction) => ({
count: reaction.count, count: reaction.count,
user_ids: reaction.user_ids, 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) { if (should_display_reactors) {
return comma_separated_usernames(user_ids); return comma_separated_usernames(user_ids);
} }
return `${user_ids.length}`; 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) { if (!user_settings.display_emoji_reaction_users) {
return false; return false;
} }
let total_reactions = 0; let total_reactions = 0;
for (const {count, user_ids} of reaction_counts_and_user_ids) { 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; 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 usernames = people.get_display_full_names(user_list);
const current_user_has_reacted = user_list.includes(current_user.user_id); 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; 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 // Because whether we display a count or the names of reacting
// users depends on total reactions on the message, we need to // users depends on total reactions on the message, we need to
// recalculate this whenever adjusting reaction rendering on a // 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()) { for (const [reaction, clean_reaction] of message.clean_reactions.entries()) {
const reaction_elem = find_reaction(message.id, clean_reaction.local_id); const reaction_elem = find_reaction(message.id, clean_reaction.local_id);
const vote_text = get_vote_text(clean_reaction.user_ids, should_display_reactors); 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); set_reaction_vote_text(reaction_elem, vote_text);
} }
} }

View File

@ -1181,7 +1181,7 @@ test("remove_reaction_from_view (last person)", () => {
assert.ok(removed); assert.ok(removed);
}); });
test("error_handling", ({override, override_rewire}) => { test("error_handling", ({override}) => {
override(message_store, "get", noop); override(message_store, "get", noop);
blueslip.expect("error", "reactions: Bad message id"); blueslip.expect("error", "reactions: Bad message id");
@ -1193,7 +1193,6 @@ test("error_handling", ({override, override_rewire}) => {
emoji_code: "991", emoji_code: "991",
user_id: 99, user_id: 99,
}; };
override_rewire(reactions, "current_user_has_reacted_to_emoji", () => true);
reactions.toggle_emoji_reaction(55, bogus_event.emoji_name); reactions.toggle_emoji_reaction(55, bogus_event.emoji_name);
reactions.add_reaction(bogus_event); reactions.add_reaction(bogus_event);