mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	This is mostly done for correctness reasons--it is easiest from a logical standpoint to set the realm emojis at the end of the function, since we do not want them to be overwritten by normal emojis. The code worked before this change, but it involved a clunky check to map.has(). There is also probably a very minor performance improvement insofar as N (the number of normal emojis) is typically greater than R (the number of realm emojis), and we eliminate N calls to map.has in return for R calls to map.set. Even if R is quite large, the readability advantages probably far outweigh any performance considerations, since we are using native map calls. Thanks to Austin Riba for this suggestion.
		
			
				
	
	
		
			319 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import _ from "lodash";
 | 
						|
 | 
						|
// We will get actual values when we get initialized.
 | 
						|
let emoji_codes = {};
 | 
						|
 | 
						|
// `emojis_by_name` is the central data source that is supposed to be
 | 
						|
// used by every widget in the web app for gathering data for displaying
 | 
						|
// emojis. Emoji picker uses this data to derive data for its own use.
 | 
						|
export let emojis_by_name = new Map();
 | 
						|
 | 
						|
export const all_realm_emojis = new Map();
 | 
						|
export const active_realm_emojis = new Map();
 | 
						|
export const deactivated_emoji_name_to_code = new Map();
 | 
						|
 | 
						|
let default_emoji_aliases = new Map();
 | 
						|
 | 
						|
// For legacy reasons we track server_realm_emoji_data,
 | 
						|
// since our settings code builds off that format.  We
 | 
						|
// should move it to use all_realm_emojis, which requires
 | 
						|
// adding author_id here and then changing the settings code
 | 
						|
// in a slightly non-trivial way.
 | 
						|
let server_realm_emoji_data = {};
 | 
						|
 | 
						|
// We really want to deprecate this, too.
 | 
						|
export function get_server_realm_emoji_data() {
 | 
						|
    return server_realm_emoji_data;
 | 
						|
}
 | 
						|
 | 
						|
let emoticon_translations = [];
 | 
						|
 | 
						|
function build_emoticon_translations({emoticon_conversions}) {
 | 
						|
    /*
 | 
						|
 | 
						|
    Please keep this as a pure function so that we can
 | 
						|
    eventually share this code with the mobile codebase.
 | 
						|
 | 
						|
    Build a data structure that looks like something
 | 
						|
    like this:
 | 
						|
 | 
						|
    [
 | 
						|
        { regex: /(\:\))/g, replacement_text: ':smile:' },
 | 
						|
        { regex: /(\(\:)/g, replacement_text: ':smile:' },
 | 
						|
        { regex: /(\:\/)/g, replacement_text: ':confused:' },
 | 
						|
        { regex: /(<3)/g, replacement_text: ':heart:' },
 | 
						|
        { regex: /(\:\()/g, replacement_text: ':frown:' },
 | 
						|
        { regex: /(\:\|)/g, replacement_text: ':expressionless:' },
 | 
						|
        ....
 | 
						|
    ]
 | 
						|
 | 
						|
        We build up this list of ~12 emoticon translations even
 | 
						|
        if user_settings.translate_emoticons is false, since
 | 
						|
        that setting can be flipped via live update events.
 | 
						|
        On the other hand, we assume that emoticon_conversions
 | 
						|
        won't change until the next reload, which is fine for
 | 
						|
        now (and we want to avoid creating new regexes on
 | 
						|
        every new message).
 | 
						|
    */
 | 
						|
 | 
						|
    const translations = [];
 | 
						|
    for (const [emoticon, replacement_text] of Object.entries(emoticon_conversions)) {
 | 
						|
        const regex = new RegExp("(" + _.escapeRegExp(emoticon) + ")", "g");
 | 
						|
 | 
						|
        translations.push({
 | 
						|
            regex,
 | 
						|
            replacement_text,
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    return translations;
 | 
						|
}
 | 
						|
 | 
						|
const zulip_emoji = {
 | 
						|
    id: "zulip",
 | 
						|
    emoji_name: "zulip",
 | 
						|
    emoji_url: "/static/generated/emoji/images/emoji/unicode/zulip.png",
 | 
						|
    is_realm_emoji: true,
 | 
						|
    deactivated: false,
 | 
						|
};
 | 
						|
 | 
						|
export function get_emoji_name(codepoint) {
 | 
						|
    // get_emoji_name('1f384') === 'holiday_tree'
 | 
						|
    if (Object.hasOwn(emoji_codes.codepoint_to_name, codepoint)) {
 | 
						|
        return emoji_codes.codepoint_to_name[codepoint];
 | 
						|
    }
 | 
						|
    return undefined;
 | 
						|
}
 | 
						|
 | 
						|
export function get_emoji_codepoint(emoji_name) {
 | 
						|
    // get_emoji_codepoint('avocado') === '1f951'
 | 
						|
    if (Object.hasOwn(emoji_codes.name_to_codepoint, emoji_name)) {
 | 
						|
        return emoji_codes.name_to_codepoint[emoji_name];
 | 
						|
    }
 | 
						|
    return undefined;
 | 
						|
}
 | 
						|
 | 
						|
export function get_realm_emoji_url(emoji_name) {
 | 
						|
    // If the emoji name is a realm emoji, returns the URL for it.
 | 
						|
    // Returns undefined for Unicode emoji.
 | 
						|
    // get_realm_emoji_url('shrug') === '/user_avatars/2/emoji/images/31.png'
 | 
						|
 | 
						|
    const data = active_realm_emojis.get(emoji_name);
 | 
						|
 | 
						|
    if (!data) {
 | 
						|
        // Not all emojis have URLs, plus the user
 | 
						|
        // may have hand-typed an invalid emoji.
 | 
						|
        // The caller can check the result for falsiness
 | 
						|
        // and then try alternate ways of parsing the
 | 
						|
        // emoji (in the case of Markdown) or just do
 | 
						|
        // whatever makes sense for the caller.
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return data.emoji_url;
 | 
						|
}
 | 
						|
 | 
						|
function build_emojis_by_name({
 | 
						|
    realm_emojis,
 | 
						|
    emoji_catalog,
 | 
						|
    get_emoji_name,
 | 
						|
    default_emoji_aliases,
 | 
						|
}) {
 | 
						|
    // Please keep this as a pure function so that we can
 | 
						|
    // eventually share this code with the mobile codebase.
 | 
						|
    const map = new Map();
 | 
						|
 | 
						|
    for (const codepoints of Object.values(emoji_catalog)) {
 | 
						|
        for (const codepoint of codepoints) {
 | 
						|
            const emoji_name = get_emoji_name(codepoint);
 | 
						|
            if (emoji_name !== undefined) {
 | 
						|
                const emoji_dict = {
 | 
						|
                    name: emoji_name,
 | 
						|
                    display_name: emoji_name,
 | 
						|
                    aliases: default_emoji_aliases.get(codepoint),
 | 
						|
                    is_realm_emoji: false,
 | 
						|
                    emoji_code: codepoint,
 | 
						|
                    has_reacted: false,
 | 
						|
                };
 | 
						|
                // We may later get overridden by a realm emoji.
 | 
						|
                map.set(emoji_name, emoji_dict);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    for (const [realm_emoji_name, realm_emoji] of realm_emojis) {
 | 
						|
        const emoji_dict = {
 | 
						|
            name: realm_emoji_name,
 | 
						|
            display_name: realm_emoji_name,
 | 
						|
            aliases: [realm_emoji_name],
 | 
						|
            is_realm_emoji: true,
 | 
						|
            url: realm_emoji.emoji_url,
 | 
						|
            has_reacted: false,
 | 
						|
        };
 | 
						|
 | 
						|
        // We want the realm emoji to overwrite any existing entry in this map.
 | 
						|
        map.set(realm_emoji_name, emoji_dict);
 | 
						|
    }
 | 
						|
 | 
						|
    return map;
 | 
						|
}
 | 
						|
 | 
						|
export function update_emojis(realm_emojis) {
 | 
						|
    // The settings code still works with the
 | 
						|
    // server format of the data.
 | 
						|
    server_realm_emoji_data = realm_emojis;
 | 
						|
 | 
						|
    // all_realm_emojis is emptied before adding the realm-specific emoji
 | 
						|
    // to it. This makes sure that in case of deletion, the deleted realm_emojis
 | 
						|
    // don't persist in active_realm_emojis.
 | 
						|
    all_realm_emojis.clear();
 | 
						|
    active_realm_emojis.clear();
 | 
						|
 | 
						|
    for (const data of Object.values(realm_emojis)) {
 | 
						|
        all_realm_emojis.set(data.id, {
 | 
						|
            id: data.id,
 | 
						|
            emoji_name: data.name,
 | 
						|
            emoji_url: data.source_url,
 | 
						|
            still_url: data.still_url,
 | 
						|
            deactivated: data.deactivated,
 | 
						|
        });
 | 
						|
        if (data.deactivated !== true) {
 | 
						|
            active_realm_emojis.set(data.name, {
 | 
						|
                id: data.id,
 | 
						|
                emoji_name: data.name,
 | 
						|
                emoji_url: data.source_url,
 | 
						|
                still_url: data.still_url,
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Add the special Zulip emoji as though it were a realm emoji.
 | 
						|
 | 
						|
    // The Zulip emoji is the only emoji that uses a string ("zulip")
 | 
						|
    // as its ID. All other emoji use numeric IDs. This special case
 | 
						|
    // is confusing; ideally we'd convert the Zulip emoji to be
 | 
						|
    // implemented using the RealmEmoji infrastructure.
 | 
						|
    all_realm_emojis.set("zulip", zulip_emoji);
 | 
						|
 | 
						|
    // here "zulip" is an emoji name, which is fine.
 | 
						|
    active_realm_emojis.set("zulip", zulip_emoji);
 | 
						|
 | 
						|
    emojis_by_name = build_emojis_by_name({
 | 
						|
        realm_emojis: active_realm_emojis,
 | 
						|
        emoji_catalog: emoji_codes.emoji_catalog,
 | 
						|
        get_emoji_name,
 | 
						|
        default_emoji_aliases,
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
// This function will provide required parameters that would
 | 
						|
// need by template to render an emoji.
 | 
						|
export function get_emoji_details_by_name(emoji_name) {
 | 
						|
    // To call this function you must pass an emoji name.
 | 
						|
    if (!emoji_name) {
 | 
						|
        throw new Error("Emoji name must be passed.");
 | 
						|
    }
 | 
						|
 | 
						|
    const emoji_info = {emoji_name};
 | 
						|
 | 
						|
    if (active_realm_emojis.has(emoji_name)) {
 | 
						|
        if (emoji_name === "zulip") {
 | 
						|
            emoji_info.reaction_type = "zulip_extra_emoji";
 | 
						|
        } else {
 | 
						|
            emoji_info.reaction_type = "realm_emoji";
 | 
						|
        }
 | 
						|
        const emoji_code_info = active_realm_emojis.get(emoji_name);
 | 
						|
        emoji_info.emoji_code = emoji_code_info.id;
 | 
						|
        emoji_info.url = emoji_code_info.emoji_url;
 | 
						|
        if (emoji_code_info.still_url) {
 | 
						|
            emoji_info.still_url = emoji_code_info.still_url;
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        const codepoint = get_emoji_codepoint(emoji_name);
 | 
						|
        if (codepoint === undefined) {
 | 
						|
            throw new Error("Bad emoji name: " + emoji_name);
 | 
						|
        }
 | 
						|
        emoji_info.reaction_type = "unicode_emoji";
 | 
						|
        emoji_info.emoji_code = codepoint;
 | 
						|
    }
 | 
						|
    return emoji_info;
 | 
						|
}
 | 
						|
 | 
						|
export function get_emoji_details_for_rendering(opts) {
 | 
						|
    if (!opts.emoji_name || !opts.emoji_code || !opts.reaction_type) {
 | 
						|
        throw new Error("Invalid params.");
 | 
						|
    }
 | 
						|
 | 
						|
    if (opts.reaction_type !== "unicode_emoji") {
 | 
						|
        const realm_emoji = all_realm_emojis.get(opts.emoji_code);
 | 
						|
        if (!realm_emoji) {
 | 
						|
            throw new Error(`Cannot find realm emoji for code '${opts.emoji_code}'.`);
 | 
						|
        }
 | 
						|
        return {
 | 
						|
            url: realm_emoji.emoji_url,
 | 
						|
            still_url: realm_emoji.still_url,
 | 
						|
            emoji_name: opts.emoji_name,
 | 
						|
            emoji_code: opts.emoji_code,
 | 
						|
            reaction_type: opts.reaction_type,
 | 
						|
        };
 | 
						|
    }
 | 
						|
    // else
 | 
						|
    return {
 | 
						|
        emoji_name: opts.emoji_name,
 | 
						|
        emoji_code: opts.emoji_code,
 | 
						|
        reaction_type: opts.reaction_type,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function build_default_emoji_aliases({names, get_emoji_codepoint}) {
 | 
						|
    // Please keep this as a pure function so that we can
 | 
						|
    // eventually share this code with the mobile codebase.
 | 
						|
 | 
						|
    // Create a map of codepoint -> names
 | 
						|
    const map = new Map();
 | 
						|
 | 
						|
    for (const name of names) {
 | 
						|
        const base_name = get_emoji_codepoint(name);
 | 
						|
 | 
						|
        if (map.has(base_name)) {
 | 
						|
            map.get(base_name).push(name);
 | 
						|
        } else {
 | 
						|
            map.set(base_name, [name]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return map;
 | 
						|
}
 | 
						|
 | 
						|
export function initialize(params) {
 | 
						|
    emoji_codes = params.emoji_codes;
 | 
						|
 | 
						|
    emoticon_translations = build_emoticon_translations({
 | 
						|
        emoticon_conversions: emoji_codes.emoticon_conversions,
 | 
						|
    });
 | 
						|
 | 
						|
    default_emoji_aliases = build_default_emoji_aliases({
 | 
						|
        names: emoji_codes.names,
 | 
						|
        get_emoji_codepoint,
 | 
						|
    });
 | 
						|
 | 
						|
    update_emojis(params.realm_emoji);
 | 
						|
}
 | 
						|
 | 
						|
export function get_canonical_name(emoji_name) {
 | 
						|
    if (active_realm_emojis.has(emoji_name)) {
 | 
						|
        return emoji_name;
 | 
						|
    }
 | 
						|
    const codepoint = get_emoji_codepoint(emoji_name);
 | 
						|
    if (codepoint === undefined) {
 | 
						|
        // Our caller needs to handle this possibility.
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    return get_emoji_name(codepoint);
 | 
						|
}
 | 
						|
 | 
						|
export function get_emoticon_translations() {
 | 
						|
    return emoticon_translations;
 | 
						|
}
 |