/* This is the main model code for building the buddy list. We also rely on presence.js to compute the actual presence for users. We glue in other "people" data and do filtering/sorting of the data that we'll send into the view. */ exports.max_size_before_shrinking = 600; const fade_config = { get_user_id: function (item) { return item.user_id; }, fade: function (item) { item.faded = true; }, unfade: function (item) { item.faded = false; }, }; exports.get_user_circle_class = function (user_id) { const status = exports.buddy_status(user_id); switch (status) { case 'active': return 'user_circle_green'; case 'idle': return 'user_circle_orange'; case 'away_them': case 'away_me': return 'user_circle_empty_line'; default: return 'user_circle_empty'; } }; exports.status_description = function (user_id) { const status = exports.buddy_status(user_id); switch (status) { case 'active': return i18n.t('Active'); case 'idle': return i18n.t('Idle'); case 'away_them': case 'away_me': return i18n.t('Unavailable'); default: return i18n.t('Offline'); } }; exports.level = function (user_id) { if (people.is_my_user_id(user_id)) { // Always put current user at the top. return 0; } const status = exports.buddy_status(user_id); switch (status) { case 'active': return 1; case 'idle': return 2; case 'away_them': return 3; default: return 3; } }; exports.buddy_status = function (user_id) { if (user_status.is_away(user_id)) { if (people.is_my_user_id(user_id)) { return 'away_me'; } return 'away_them'; } // get active/idle/etc. return presence.get_status(user_id); }; exports.compare_function = function (a, b) { const level_a = exports.level(a); const level_b = exports.level(b); const diff = level_a - level_b; if (diff !== 0) { return diff; } // Sort equivalent PM names alphabetically const person_a = people.get_person_from_user_id(a); const person_b = people.get_person_from_user_id(b); const full_name_a = person_a ? person_a.full_name : ''; const full_name_b = person_b ? person_b.full_name : ''; return util.strcmp(full_name_a, full_name_b); }; exports.sort_users = function (user_ids) { // TODO sort by unread count first, once we support that user_ids.sort(exports.compare_function); return user_ids; }; function filter_user_ids(filter_text, user_ids) { if (filter_text === '') { return user_ids; } user_ids = _.reject(user_ids, people.is_my_user_id); let search_terms = filter_text.toLowerCase().split(/[|,]+/); search_terms = _.map(search_terms, function (s) { return s.trim(); }); const persons = _.map(user_ids, function (user_id) { return people.get_person_from_user_id(user_id); }); const user_id_dict = people.filter_people_by_search_terms(persons, search_terms); return user_id_dict.keys(); } exports.matches_filter = function (filter_text, user_id) { // This is a roundabout way of checking a user if you look // too hard at it, but it should be fine for now. return filter_user_ids(filter_text, [user_id]).length === 1; }; function get_num_unread(user_id) { if (unread.suppress_unread_counts) { return 0; } return unread.num_unread_for_person(user_id); } exports.my_user_status = function (user_id) { if (!people.is_my_user_id(user_id)) { return; } if (user_status.is_away(user_id)) { return i18n.t('(unavailable)'); } return i18n.t('(you)'); }; exports.user_last_seen_time_status = function (user_id) { const status = presence.get_status(user_id); if (status === "active") { return i18n.t("Active now"); } if (page_params.realm_is_zephyr_mirror_realm) { // We don't send presence data to clients in Zephyr mirroring realms return i18n.t("Unknown"); } // There are situations where the client has incomplete presence // history on a user. This can happen when users are deactivated, // or when they just haven't been present in a long time (and we // may have queries on presence that go back only N weeks). // // We give the somewhat vague status of "Unknown" for these users. const last_active_date = presence.last_active_date(user_id); if (last_active_date === undefined) { return i18n.t("More than 2 weeks ago"); } return timerender.last_seen_status_from_date(last_active_date.clone()); }; exports.info_for = function (user_id) { const user_circle_class = exports.get_user_circle_class(user_id); const person = people.get_person_from_user_id(user_id); const my_user_status = exports.my_user_status(user_id); const user_circle_status = exports.status_description(user_id); return { href: hash_util.pm_with_uri(person.email), name: person.full_name, user_id: user_id, my_user_status: my_user_status, is_current_user: people.is_my_user_id(user_id), num_unread: get_num_unread(user_id), user_circle_class: user_circle_class, user_circle_status: user_circle_status, }; }; function get_last_seen(active_status, last_seen) { if (active_status === 'active') { return last_seen; } const last_seen_text = i18n.t('Last active: __last_seen__', {last_seen: last_seen}); return last_seen_text; } exports.get_title_data = function (user_ids_string, is_group) { if (is_group === true) { // For groups, just return a string with recipient names. return { first_line: people.get_recipients(user_ids_string), second_line: '', third_line: '', }; } // Since it's not a group, user_ids_string is a single user ID. const user_id = user_ids_string; const person = people.get_person_from_user_id(user_id); if (person.is_bot) { // Bot has an owner. if (person.bot_owner_id !== null) { person.bot_owner_full_name = people.get_person_from_user_id( person.bot_owner_id).full_name; const bot_owner_name = i18n.t('Owner: __name__', {name: person.bot_owner_full_name}); return { first_line: person.full_name, second_line: bot_owner_name, third_line: '', }; } // Bot does not have an owner. return { first_line: person.full_name, second_line: '', third_line: '', }; } // For buddy list and individual PMS. Since is_group=False, it's // a single, human, user. const active_status = presence.get_status(user_id); const last_seen = exports.user_last_seen_time_status(user_id); // Users has a status. if (user_status.get_status_text(user_id)) { return { first_line: person.full_name, second_line: user_status.get_status_text(user_id), third_line: get_last_seen(active_status, last_seen), }; } // Users does not have a status. return { first_line: person.full_name, second_line: get_last_seen(active_status, last_seen), third_line: '', }; }; exports.get_item = function (user_id) { const info = exports.info_for(user_id); compose_fade.update_user_info([info], fade_config); return info; }; function user_is_recently_active(user_id) { // return true if the user has a green/orange cirle return exports.level(user_id) <= 2; } function maybe_shrink_list(user_ids, filter_text) { if (user_ids.length <= exports.max_size_before_shrinking) { return user_ids; } if (filter_text) { // If the user types something, we want to show all // users matching the text, even if they have not been // online recently. // For super common letters like "s", we may // eventually want to filter down to only users that // are in presence.get_user_ids(). return user_ids; } user_ids = _.filter(user_ids, user_is_recently_active); return user_ids; } exports.get_filtered_and_sorted_user_ids = function (filter_text) { let user_ids; if (filter_text) { // If there's a filter, select from all users, not just those // recently active. user_ids = filter_user_ids(filter_text, people.get_active_user_ids()); } else { // From large realms, the user_ids in presence may exclude // users who have been idle more than three weeks. When the // filter text is blank, we show only those recently active users. user_ids = presence.get_user_ids(); } user_ids = _.filter(user_ids, function (user_id) { const person = people.get_person_from_user_id(user_id); if (person) { // if the user is bot, do not show in presence data. if (person.is_bot) { return false; } } return true; }); user_ids = maybe_shrink_list(user_ids, filter_text); return exports.sort_users(user_ids); }; exports.get_items_for_users = function (user_ids) { const user_info = _.map(user_ids, exports.info_for).filter(function (person) { // filtered bots and yourself are set to "undefined" in the `info_for` // function. return typeof person !== "undefined"; }); compose_fade.update_user_info(user_info, fade_config); return user_info; }; exports.huddle_fraction_present = function (huddle) { const user_ids = huddle.split(','); let num_present = 0; _.each(user_ids, function (user_id) { if (presence.is_active(user_id)) { num_present += 1; } }); if (num_present === user_ids.length) { return 1; } else if (num_present !== 0) { return 0.5; } return; }; window.buddy_data = exports;