const render_recent_topics_body = require('../templates/recent_topics_table.hbs'); const render_recent_topic_row = require('../templates/recent_topic_row.hbs'); const render_recent_topics_filters = require('../templates/recent_topics_filters.hbs'); const topics = new Map(); // Key is stream-id:topic. let topics_widget; // Sets the number of avatars to display. // Rest of the avatars, if present, are displayed as {+x} const MAX_AVATAR = 4; let filters = new Set(); function get_topic_key(stream_id, topic) { return stream_id + ":" + topic.toLowerCase(); } exports.process_messages = function (messages) { // FIX: Currently, we do a complete_rerender everytime // we process a new message. // While this is inexpensive and handles all the cases itself, // the UX can be bad if user wants to scroll down the list as // the UI will be returned to the beginning of the list on every // update. for (const msg of messages) { exports.process_message(msg); } exports.complete_rerender(); }; exports.process_message = function (msg) { if (msg.type !== 'stream') { return false; } // Initialize topic data const key = get_topic_key(msg.stream_id, msg.topic); if (!topics.has(key)) { topics.set(key, { last_msg_id: -1, participated: false, }); } // Update topic data const is_ours = people.is_my_user_id(msg.sender_id); const topic_data = topics.get(key); if (topic_data.last_msg_id < msg.id) { // NOTE: This also stores locally echoed msg_id which // has not been successfully received from the server. // We store it now and reify it when response is available // from server. topic_data.last_msg_id = msg.id; } // TODO: Add backend support for participated topics. // Currently participated === Recently Participated // i.e. Only those topics are participated for which we have the user's // message fetched in the topic. Ideally we would want this to be attached // to topic info fetched from backend, which is currently not a thing. topic_data.participated = is_ours || topic_data.participated; return true; }; exports.reify_message_id_if_available = function (opts) { // We don't need to reify the message_id of the topic // if a new message arrives in the topic from another user, // since it replaces the last_msg_id of the topic which // we were trying to reify. for (const [, value] of topics.entries()) { if (value.last_msg_id === opts.old_id) { value.last_msg_id = opts.new_id; return true; } } return false; }; function get_sorted_topics() { // Sort all recent topics by last message time. return new Map(Array.from(topics.entries()).sort(function (a, b) { return b[1].last_msg_id - a[1].last_msg_id; })); } exports.get = function () { return get_sorted_topics(); }; function format_topic(topic_data) { const last_msg = message_store.get(topic_data.last_msg_id); const stream = last_msg.stream; const stream_id = last_msg.stream_id; const stream_info = stream_data.get_sub(stream); const topic = last_msg.topic; const time = new XDate(last_msg.timestamp * 1000); const last_msg_time = timerender.last_seen_status_from_date(time); // We hide the row according to filters or if it's muted. // We only supply the data to the topic rows and let jquery // display / hide them according to filters instead of // doing complete re-render. const topic_muted = !!muting.is_topic_muted(stream_id, topic); const stream_muted = stream_data.is_muted(stream_id); const muted = topic_muted || stream_muted; const unread_count = unread.unread_topic_counter.get(stream_id, topic); // Display in most recent sender first order const all_senders = recent_senders.get_topic_recent_senders(stream_id, topic); const senders = all_senders.slice(-MAX_AVATAR); const senders_info = people.sender_info_with_small_avatar_urls_for_sender_ids(senders); return { // stream info stream_id: stream_id, stream: stream, stream_color: stream_info.color, invite_only: stream_info.invite_only, is_web_public: stream_info.is_web_public, stream_url: hash_util.by_stream_uri(stream_id), topic: topic, topic_key: get_topic_key(stream_id, topic), unread_count: unread_count, last_msg_time: last_msg_time, topic_url: hash_util.by_stream_topic_uri(stream_id, topic), senders: senders_info, other_senders_count: Math.max(0, all_senders.length - MAX_AVATAR), muted: muted, topic_muted: topic_muted, participated: topic_data.participated, }; } function get_topic_row(topic_data) { const msg = message_store.get(topic_data.last_msg_id); const topic_key = get_topic_key(msg.stream_id, msg.topic); return $("#" + $.escapeSelector("recent_topic:" + topic_key)); } exports.process_topic_edit = function (old_stream_id, old_topic, new_topic, new_stream_id) { // See `recent_senders.process_topic_edit` for // logic behind this and important notes on use of this function. topics.delete(get_topic_key(old_stream_id, old_topic)); const old_topic_msgs = message_util.get_messages_in_topic(old_stream_id, old_topic); exports.process_messages(old_topic_msgs); new_stream_id = new_stream_id || old_stream_id; const new_topic_msgs = message_util.get_messages_in_topic(new_stream_id, new_topic); exports.process_messages(new_topic_msgs); }; function topic_in_search_results(keyword, stream, topic) { if (keyword === "") { return true; } // split the search text around whitespace(s). // eg: "Denamark recent" -> ["Denamrk", "recent"] const search_keywords = $.trim(keyword).split(/\s+/); // turn the search keywords into word boundary groups // eg: ["Denamrk", "recent"] -> "^(?=.*\bDenmark\b)(?=.*\brecent\b).*$" const val = '^(?=.*\\b' + search_keywords.join('\\b)(?=.*\\b') + ').*$'; const reg = RegExp(val, 'i'); // i for ignorecase const text = (stream + " " + topic).replace(/\s+/g, ' '); return reg.test(text); } function filters_should_hide_topic(topic_data) { const msg = message_store.get(topic_data.last_msg_id); if (filters.has('unread')) { const unreadCount = unread.unread_topic_counter.get(msg.stream_id, msg.topic); if (unreadCount === 0) { return true; } } if (!topic_data.participated && filters.has('participated')) { return true; } if (!filters.has('include_muted')) { const topic_muted = !!muting.is_topic_muted(msg.stream_id, msg.topic); const stream_muted = stream_data.is_muted(msg.stream_id); if (topic_muted || stream_muted) { return true; } } const search_keyword = $("#recent_topics_search").val(); if (!topic_in_search_results(search_keyword, msg.stream, msg.topic)) { return true; } return false; } exports.inplace_rerender = function (topic_key) { if (!overlays.recent_topics_open()) { return false; } if (!topics.has(topic_key)) { return false; } const topic_data = topics.get(topic_key); topics_widget.render_item(topic_data); const topic_row = get_topic_row(topic_data); if (filters_should_hide_topic(topic_data)) { topic_row.hide(); } else { topic_row.show(); } return true; }; exports.update_topic_is_muted = function (stream_id, topic) { const key = get_topic_key(stream_id, topic); if (!topics.has(key)) { // we receive mute request for a topic we are // not tracking currently return false; } exports.inplace_rerender(key); return true; }; exports.update_topic_unread_count = function (message) { const topic_key = get_topic_key(message.stream_id, message.topic); exports.inplace_rerender(topic_key); }; exports.set_filter = function (filter) { // This function updates the `filters` variable // after user clicks on one of the filter buttons // based on `btn-recent-selected` class and current // set `filters`. // Get the button which was clicked. const filter_elem = $('#recent_topics_filter_buttons') .find('[data-filter="' + filter + '"]'); // If user clicks `All`, we clear all filters. if (filter === 'all' && filters.size !== 0) { filters = new Set(); // If the button was already selected, remove the filter. } else if (filter_elem.hasClass('btn-recent-selected')) { filters.delete(filter); // If the button was not selected, we add the filter. } else { filters.add(filter); } }; function show_selected_filters() { // Add `btn-selected-filter` to the buttons to show // which filters are applied. if (filters.size === 0) { $('#recent_topics_filter_buttons') .find('[data-filter="all"]') .addClass('btn-recent-selected'); } else { for (const filter of filters) { $('#recent_topics_filter_buttons') .find('[data-filter="' + filter + '"]') .addClass('btn-recent-selected'); } } } exports.update_filters_view = function () { const rendered_filters = render_recent_topics_filters({ filter_participated: filters.has('participated'), filter_unread: filters.has('unread'), filter_muted: filters.has('include_muted'), }); $("#recent_filters_group").html(rendered_filters); show_selected_filters(); topics_widget.hard_redraw(); }; function stream_sort(a, b) { const a_stream = message_store.get(a.last_msg_id).stream; const b_stream = message_store.get(b.last_msg_id).stream; if (a_stream > b_stream) { return 1; } else if (a_stream === b_stream) { return 0; } return -1; } function topic_sort(a, b) { const a_topic = message_store.get(a.last_msg_id).topic; const b_topic = message_store.get(b.last_msg_id).topic; if (a_topic > b_topic) { return 1; } else if (a_topic === b_topic) { return 0; } return -1; } exports.complete_rerender = function () { if (!overlays.recent_topics_open()) { return false; } // Prepare header const rendered_body = render_recent_topics_body({ filter_participated: filters.has('participated'), filter_unread: filters.has('unread'), filter_muted: filters.has('include_muted'), search_val: $("#recent_topics_search").val() || "", }); $('#recent_topics_table').html(rendered_body); show_selected_filters(); // Show topics list const container = $('.recent_topics_table table tbody'); container.empty(); const mapped_topic_values = Array.from(exports.get().values()).map(function (value) { return value; }); topics_widget = list_render.create(container, mapped_topic_values, { name: "recent_topics_table", parent_container: $("#recent_topics_table"), modifier: function (item) { return render_recent_topic_row(format_topic(item)); }, filter: { // We use update_filters_view & filters_should_hide_topic to do all the // filtering for us, which is called using click_handlers. predicate: function (topic_data) { return !filters_should_hide_topic(topic_data); }, }, sort_fields: { stream_sort: stream_sort, topic_sort: topic_sort, }, html_selector: get_topic_row, }); }; exports.launch = function () { overlays.open_overlay({ name: 'recent_topics', overlay: $('#recent_topics_overlay'), on_close: function () { hashchange.exit_overlay(); }, }); exports.complete_rerender(); $("#recent_topics_search").select(); }; window.recent_topics = exports;