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 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<MessageWithBooleans & {type: "private"}, "reactions">
| Omit<MessageWithBooleans & {type: "stream"}, "reactions">
) & {
// Replaced by `clean_reactions` in `reactions.set_clean_reactions`.
reactions?: RawReaction[];
// Added in `reactions.set_clean_reactions`.
clean_reactions: Map<string, MessageCleanReaction>;

View File

@ -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<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(",");
}
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<string, RawReaction>();
const user_map = new Map<string, number[]>();
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);
}
}

View File

@ -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);