zulip/static/js/reactions.js

584 lines
21 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
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 * as emoji_picker from "./emoji_picker";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";
import * as message_store from "./message_store";
import {page_params} from "./page_params";
import * as people from "./people";
import * as spectators from "./spectators";
import {user_settings} from "./user_settings";
export const view = {}; // function namespace
2020-08-20 21:24:06 +02:00
export function get_local_reaction_id(reaction_info) {
return [reaction_info.reaction_type, reaction_info.emoji_code].join(",");
}
export function open_reactions_popover() {
const message = message_lists.current.selected_message();
let target;
// Use verbose style to ensure we test both sides of the condition.
if (message.sent_by_me) {
target = $(message_lists.current.selected_row()).find(".actions_hover")[0];
} else {
target = $(message_lists.current.selected_row()).find(".reaction_button")[0];
}
emoji_picker.toggle_emoji_popover(target, message_lists.current.selected_id());
return true;
}
export function current_user_has_reacted_to_emoji(message, local_id) {
set_clean_reactions(message);
const clean_reaction_object = message.clean_reactions.get(local_id);
return clean_reaction_object && clean_reaction_object.user_ids.includes(page_params.user_id);
}
function get_message(message_id) {
const message = message_store.get(message_id);
if (!message) {
blueslip.error("reactions: Bad message id: " + message_id);
return undefined;
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
set_clean_reactions(message);
return message;
}
function create_reaction(message_id, reaction_info) {
return {
message_id,
user_id: page_params.user_id,
local_id: get_local_reaction_id(reaction_info),
reaction_type: reaction_info.reaction_type,
emoji_name: reaction_info.emoji_name,
emoji_code: reaction_info.emoji_code,
};
}
function update_ui_and_send_reaction_ajax(message_id, reaction_info) {
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.
spectators.login_to_access();
return;
}
const message = get_message(message_id);
const local_id = get_local_reaction_id(reaction_info);
const has_reacted = current_user_has_reacted_to_emoji(message, local_id);
const operation = has_reacted ? "remove" : "add";
const reaction = create_reaction(message_id, reaction_info);
if (operation === "add") {
add_reaction(reaction);
} else {
remove_reaction(reaction);
}
const args = {
url: "/json/messages/" + message_id + "/reactions",
data: reaction_info,
success() {},
error(xhr) {
const response = channel.xhr_error_message("Error sending reaction", xhr);
// Errors are somewhat common here, due to race conditions
// where the user tries to add/remove the reaction when there is already
// an in-flight request. We eventually want to make this a blueslip
// error, rather than a warning, but we need to implement either
// #4291 or #4295 first.
blueslip.warn(response);
},
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
};
if (operation === "add") {
channel.post(args);
} else if (operation === "remove") {
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
channel.del(args);
}
}
export function toggle_emoji_reaction(message_id, emoji_name) {
// This codepath doesn't support toggling a deactivated realm emoji.
// Since an user can interact with a deactivated realm emoji only by
// clicking on a reaction and that is handled by `process_reaction_click()`
// method. This codepath is to be used only where there is no chance of an
// user interacting with a deactivated realm emoji like emoji picker.
const reaction_info = emoji.get_emoji_details_by_name(emoji_name);
update_ui_and_send_reaction_ajax(message_id, reaction_info);
}
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
export function process_reaction_click(message_id, local_id) {
const message = get_message(message_id);
if (!message) {
blueslip.error("message_id for reaction click is unknown: " + message_id);
return;
}
const clean_reaction_object = message.clean_reactions.get(local_id);
if (!clean_reaction_object) {
blueslip.error(
"Data integrity problem for reaction " + local_id + " (message " + message_id + ")",
);
return;
}
const reaction_info = {
reaction_type: clean_reaction_object.reaction_type,
emoji_name: clean_reaction_object.emoji_name,
emoji_code: clean_reaction_object.emoji_code,
};
update_ui_and_send_reaction_ajax(message_id, reaction_info);
}
function generate_title(emoji_name, user_ids) {
const usernames = people.get_display_full_names(
user_ids.filter((user_id) => user_id !== page_params.user_id),
);
const current_user_reacted = user_ids.length !== usernames.length;
const context = {
emoji_name: ":" + emoji_name + ":",
};
if (user_ids.length === 1) {
if (current_user_reacted) {
return $t({defaultMessage: "You (click to remove) reacted with {emoji_name}"}, context);
}
context.username = usernames[0];
return $t({defaultMessage: "{username} reacted with {emoji_name}"}, context);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
if (user_ids.length === 2 && current_user_reacted) {
context.other_username = usernames[0];
return $t(
{
defaultMessage:
"You (click to remove) and {other_username} reacted with {emoji_name}",
},
context,
);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
context.comma_separated_usernames = usernames.slice(0, -1).join(", ");
context.last_username = usernames.at(-1);
if (current_user_reacted) {
return $t(
{
defaultMessage:
"You (click to remove), {comma_separated_usernames} and {last_username} reacted with {emoji_name}",
},
context,
);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
return $t(
{
defaultMessage:
"{comma_separated_usernames} and {last_username} reacted with {emoji_name}",
},
context,
);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
// Add a tooltip showing who reacted to a message.
export function get_reaction_title_data(message_id, local_id) {
const message = get_message(message_id);
const clean_reaction_object = message.clean_reactions.get(local_id);
const user_list = clean_reaction_object.user_ids;
const emoji_name = clean_reaction_object.emoji_name;
const title = generate_title(emoji_name, user_list);
return title;
}
export function get_reaction_section(message_id) {
const $message_element = $(".message_table").find(`[zid='${CSS.escape(message_id)}']`);
const $section = $message_element.find(".message_reactions");
return $section;
}
export function find_reaction(message_id, local_id) {
const $reaction_section = get_reaction_section(message_id);
const $reaction = $reaction_section.find(`[data-reaction-id='${CSS.escape(local_id)}']`);
return $reaction;
}
export function get_add_reaction_button(message_id) {
const $reaction_section = get_reaction_section(message_id);
const $add_button = $reaction_section.find(".reaction_button");
return $add_button;
}
export function set_reaction_vote_text($reaction, vote_text) {
const $count_element = $reaction.find(".message_reaction_count");
$count_element.text(vote_text);
}
2017-05-29 19:24:31 +02:00
export function add_reaction(event) {
const message_id = event.message_id;
const message = message_store.get(message_id);
if (message === undefined) {
// If we don't have the message in cache, do nothing; if we
// ever fetch it from the server, it'll come with the
// latest reactions attached
return;
}
set_clean_reactions(message);
const local_id = get_local_reaction_id(event);
const user_id = event.user_id;
let clean_reaction_object = message.clean_reactions.get(local_id);
if (clean_reaction_object && clean_reaction_object.user_ids.includes(user_id)) {
return;
}
if (clean_reaction_object) {
clean_reaction_object.user_ids.push(user_id);
update_user_fields(clean_reaction_object, message.clean_reactions);
view.update_existing_reaction(clean_reaction_object, message, user_id);
} else {
clean_reaction_object = make_clean_reaction({
local_id,
user_ids: [user_id],
reaction_type: event.reaction_type,
emoji_name: event.emoji_name,
emoji_code: event.emoji_code,
});
message.clean_reactions.set(local_id, clean_reaction_object);
update_user_fields(clean_reaction_object, message.clean_reactions);
view.insert_new_reaction(clean_reaction_object, message, user_id);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
}
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
view.update_existing_reaction = function (clean_reaction_object, message, acting_user_id) {
// 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.
const local_id = get_local_reaction_id(clean_reaction_object);
const $reaction = find_reaction(message.id, local_id);
const new_label = generate_title(
clean_reaction_object.emoji_name,
clean_reaction_object.user_ids,
);
$reaction.attr("aria-label", new_label);
if (acting_user_id === page_params.user_id) {
$reaction.addClass("reacted");
}
update_vote_text_on_message(message);
};
view.insert_new_reaction = function (clean_reaction_object, message, user_id) {
// 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 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";
if (user_id === page_params.user_id) {
context.class = "message_reaction reacted";
} else {
context.class = "message_reaction";
}
const $new_reaction = $(render_message_reaction(context));
// Now insert it before the add button.
const $reaction_button_element = get_add_reaction_button(message.id);
$new_reaction.insertBefore($reaction_button_element);
update_vote_text_on_message(message);
};
export function remove_reaction(event) {
const message_id = event.message_id;
const user_id = event.user_id;
const message = message_store.get(message_id);
const local_id = get_local_reaction_id(event);
if (message === undefined) {
// If we don't have the message in cache, do nothing; if we
// ever fetch it from the server, it'll come with the
// latest reactions attached
return;
}
set_clean_reactions(message);
const clean_reaction_object = message.clean_reactions.get(local_id);
if (!clean_reaction_object) {
return;
}
if (!clean_reaction_object.user_ids.includes(user_id)) {
return;
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
clean_reaction_object.user_ids = clean_reaction_object.user_ids.filter((id) => id !== user_id);
if (clean_reaction_object.user_ids.length === 0) {
message.clean_reactions.delete(local_id);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
const should_display_reactors = check_should_display_reactors(message.clean_reactions);
update_user_fields(clean_reaction_object, should_display_reactors);
view.remove_reaction(clean_reaction_object, message, user_id);
}
view.remove_reaction = function (clean_reaction_object, message, user_id) {
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;
if (reaction_count === 0) {
// If this user was the only one reacting for this emoji, we simply
// remove the reaction and exit.
$reaction.remove();
update_vote_text_on_message(message);
return;
}
// The emoji still has reactions from other users, so we need to update
// the title/count and, if the user is the current user, turn off the
// "reacted" class.
const new_label = generate_title(
clean_reaction_object.emoji_name,
clean_reaction_object.user_ids,
);
$reaction.attr("aria-label", new_label);
if (user_id === page_params.user_id) {
$reaction.removeClass("reacted");
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
update_vote_text_on_message(message);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
};
export function get_emojis_used_by_user_for_message_id(message_id) {
const user_id = page_params.user_id;
const message = message_store.get(message_id);
set_clean_reactions(message);
const names = [];
for (const clean_reaction_object of message.clean_reactions.values()) {
if (clean_reaction_object.user_ids.includes(user_id)) {
names.push(clean_reaction_object.emoji_name);
}
}
return names;
}
export function get_message_reactions(message) {
set_clean_reactions(message);
return Array.from(message.clean_reactions.values());
}
export function set_clean_reactions(message) {
/*
set_clean_reactions processes the raw message.reactions object,
which will contain one object for each individual reaction, even
if two users react with the same emoji.
As output, it sets message.cleaned_reactions, which is a more
compressed format with one entry per reaction pill that should
be displayed visually to users.
*/
if (message.clean_reactions) {
// Update display details for the reaction. In particular,
// user_settings.display_emoji_reaction_users or the names of
// the users appearing in the reaction may have changed since
// this reaction was first rendered.
const should_display_reactors = check_should_display_reactors(message.clean_reactions);
for (const clean_reaction of message.clean_reactions.values()) {
update_user_fields(clean_reaction, should_display_reactors);
}
return;
}
// 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();
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
for (const reaction of message.reactions) {
const local_id = get_local_reaction_id(reaction);
const user_id = reaction.user_id;
if (!people.is_known_user_id(user_id)) {
blueslip.warn("Unknown user_id " + user_id + " in reaction for message " + message.id);
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
continue;
}
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
if (!distinct_reactions.has(local_id)) {
distinct_reactions.set(local_id, reaction);
user_map.set(local_id, []);
Add frontend support for emoji reactions. This commit replaces the placeholder "clipboard" button with a reaction button. This is done on any message that can't be edited. Also, on messages sent by the user the actions popover (toggled by the down chevron icon) contains an option to add a reaction. When clicked, a popover with a search bar and a list of emojis is displayed. If the right sidebar is collapsed (the viewport is small), the popover is placed to the left of the button. Focus is set to the search bar. Typing in the search bar filters emojis. Emojis with which the user has reacted to this message are highlighted. Clicking them sends an API request to remove that reaction. Clicking on non-highlighted emojis sends an API request to add a reaction. When the popover loses focus it is closed. The frontend listens for reaction events. When an add-reaction event is received, the emoji is displayed at the bottom of the message with a count initialized to 1. If there was an existing reaction to the message with the same emoji, the count is incremented. Old messages fetched from the server contain reactions. They are displayed (along with title and count) at the bottom of each message. When clicking the emoji reaction at the bottom of the message, if the user has already reacted with that emoji to this message, the reaction is removed and the count is decremented. Otherwise, a reaction is added and the count is incremented. Hovering over the emoji reaction at the bottom of the message displays a list of users who have reacted with this emoji along with the emoji name. Hovering over the emoji reactions at the bottom of the message displays a button to add a reaction. Fixes #541.
2016-12-02 13:23:23 +01:00
}
const user_ids = user_map.get(local_id);
if (user_ids.includes(user_id)) {
blueslip.error(
"server sent duplicate reactions for user " + user_id + " (key=" + local_id + ")",
);
continue;
}
user_ids.push(user_id);
}
// TODO: Rather than adding this field to the message object, it
// might be cleaner to create an independent map from message_id
// => clean_reactions data for the message, with care being taken
// to make sure reify_message_id moves the data structure
// properly.
message.clean_reactions = new Map();
for (const local_id of distinct_reactions.keys()) {
const reaction = distinct_reactions.get(local_id);
const user_ids = user_map.get(local_id);
message.clean_reactions.set(
local_id,
make_clean_reaction({local_id, user_ids, ...reaction}),
);
}
// We do update_user_fields in a separate loop, because doing so
// lets us avoid duplicating check_should_display_reactors to
// determine whether to store in the vote_text field a count or
// the names of reactors (users who reacted).
const should_display_reactors = check_should_display_reactors(message.clean_reactions);
for (const clean_reaction of message.clean_reactions.values()) {
update_user_fields(clean_reaction, should_display_reactors);
}
// We don't maintain message.reactions when users react to
// messages we already have a copy of, so it's safest to delete it
// after we've processed the reactions data for a message into the
// clean_reactions data structure, which we do maintain.
delete message.reactions;
}
function make_clean_reaction({local_id, user_ids, emoji_name, emoji_code, reaction_type}) {
const clean_reaction_object = {
local_id,
user_ids,
...emoji.get_emoji_details_for_rendering({emoji_name, emoji_code, reaction_type}),
};
clean_reaction_object.emoji_alt_code = user_settings.emojiset === "text";
clean_reaction_object.is_realm_emoji =
clean_reaction_object.reaction_type === "realm_emoji" ||
clean_reaction_object.reaction_type === "zulip_extra_emoji";
return clean_reaction_object;
}
export function update_user_fields(clean_reaction_object, should_display_reactors) {
// update_user_fields needs to be called whenever the set of users
// whor eacted on a message might have changed, including due to
// upvote/downvotes on ANY reaction in the message, because those
// can change the correct value of should_display_reactors to use.
clean_reaction_object.count = clean_reaction_object.user_ids.length;
clean_reaction_object.label = generate_title(
clean_reaction_object.emoji_name,
clean_reaction_object.user_ids,
);
if (clean_reaction_object.user_ids.includes(page_params.user_id)) {
clean_reaction_object.class = "message_reaction reacted";
} else {
clean_reaction_object.class = "message_reaction";
}
clean_reaction_object.count = clean_reaction_object.user_ids.length;
clean_reaction_object.label = generate_title(
clean_reaction_object.emoji_name,
clean_reaction_object.user_ids,
);
// The vote_text field set here is used directly in the Handlebars
// template for rendering (or rerendering!) a message.
clean_reaction_object.vote_text = get_vote_text(clean_reaction_object, should_display_reactors);
}
export function get_vote_text(clean_reaction_object, should_display_reactors) {
if (should_display_reactors) {
return comma_separated_usernames(clean_reaction_object.user_ids);
}
return `${clean_reaction_object.user_ids.length}`;
}
function check_should_display_reactors(cleaned_reactions) {
if (!user_settings.display_emoji_reaction_users) {
return false;
}
let total_reactions = 0;
for (const r of cleaned_reactions.values()) {
// r.count is not yet initialized when this is called during
// set_clean_reactions.
total_reactions += r.count || r.user_ids.length;
}
return total_reactions <= 3;
}
function comma_separated_usernames(user_list) {
const usernames = people.get_display_full_names(user_list);
const current_user_has_reacted = user_list.includes(page_params.user_id);
if (current_user_has_reacted) {
const current_user_index = user_list.indexOf(page_params.user_id);
usernames[current_user_index] = $t({
defaultMessage: "You",
});
}
const comma_separated_usernames = usernames.join(", ");
return comma_separated_usernames;
}
export function update_vote_text_on_message(message) {
// 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
// message.
const cleaned_reactions = get_message_reactions(message);
const should_display_reactors = check_should_display_reactors(cleaned_reactions);
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, should_display_reactors);
message.clean_reactions.get(reaction).vote_text = vote_text;
set_reaction_vote_text(reaction_elem, vote_text);
}
}