From 76b0c6de86026d569b719d14a2052f58d21459d6 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Sat, 21 Mar 2020 19:12:10 +0530 Subject: [PATCH] recent-topics: Add module. Add methods to extract recent topics from received messages. Process new messages as they are received. Use new messages received from the server to extract recent_topics. Node tests added. --- .eslintrc.json | 1 + frontend_tests/node_tests/general.js | 1 + frontend_tests/node_tests/message_fetch.js | 1 + frontend_tests/node_tests/recent_topics.js | 210 +++++++++++++++++++++ static/js/bundles/app.js | 1 + static/js/global.d.ts | 1 + static/js/message_events.js | 1 + static/js/message_fetch.js | 1 + static/js/recent_topics.js | 48 +++++ 9 files changed, 265 insertions(+) create mode 100644 frontend_tests/node_tests/recent_topics.js create mode 100644 static/js/recent_topics.js diff --git a/.eslintrc.json b/.eslintrc.json index 7ce1a519f5..e48faad3c0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -325,6 +325,7 @@ "realm_logo": false, "realm_night_logo": false, "recent_senders": false, + "recent_topics": false, "reload": false, "reload_state": false, "reminder": false, diff --git a/frontend_tests/node_tests/general.js b/frontend_tests/node_tests/general.js index b588f80b7e..daed444c08 100644 --- a/frontend_tests/node_tests/general.js +++ b/frontend_tests/node_tests/general.js @@ -99,6 +99,7 @@ alert_words.process_message = noop; zrequire('recent_senders'); zrequire('unread'); zrequire('stream_topic_history'); +zrequire('recent_topics'); // And finally require the module that we will test directly: zrequire('message_store'); diff --git a/frontend_tests/node_tests/message_fetch.js b/frontend_tests/node_tests/message_fetch.js index 08fd5251f9..c65c212678 100644 --- a/frontend_tests/node_tests/message_fetch.js +++ b/frontend_tests/node_tests/message_fetch.js @@ -11,6 +11,7 @@ zrequire('FetchStatus', 'js/fetch_status'); zrequire('Filter', 'js/filter'); zrequire('MessageListData', 'js/message_list_data'); zrequire('message_list'); +zrequire('recent_topics'); zrequire('people'); set_global('page_params', { diff --git a/frontend_tests/node_tests/recent_topics.js b/frontend_tests/node_tests/recent_topics.js new file mode 100644 index 0000000000..e127f568e6 --- /dev/null +++ b/frontend_tests/node_tests/recent_topics.js @@ -0,0 +1,210 @@ +const rt = zrequire('recent_topics'); +set_global('people', { + is_my_user_id: function (id) { + return id === 1; + }, +}); + +// Custom Data + +// New stream +const stream1 = 1; + +// Topics in the stream +const topic1 = "topic-1"; // No Other sender +const topic2 = "topic-2"; // Other sender +const topic3 = "topic-3"; // User not present +const topic4 = "topic-4"; // User not present +const topic5 = "topic-5"; // other sender +const topic6 = "topic-6"; // other sender +const topic7 = "topic-7"; // muted topic + +set_global('muting', { + is_topic_muted: (stream_id, topic) => { + if (stream_id === stream1 && topic === topic7) { + return true; + } + return false; + }, +}); + +// sender1 == current user +// sender2 == any other user +const sender1 = 1; +const sender2 = 2; + +const messages = []; + +let id = 0; + +messages[0] = { + stream_id: stream1, + id: id += 1, + topic: topic1, + sender_id: sender1, + type: 'stream', +}; + +messages[1] = { + stream_id: stream1, + id: id += 1, + topic: topic2, + sender_id: sender1, + type: 'stream', +}; + +messages[2] = { + stream_id: stream1, + id: id += 1, + topic: topic2, + sender_id: sender2, + type: 'stream', + starred: true, +}; + +messages[3] = { + stream_id: stream1, + id: id += 1, + topic: topic3, + sender_id: sender2, + type: 'stream', +}; + +messages[4] = { + stream_id: stream1, + id: id += 1, + topic: topic4, + sender_id: sender2, + type: 'stream', + starred: true, +}; + +messages[5] = { + stream_id: stream1, + id: id += 1, + topic: topic5, + sender_id: sender1, + type: 'stream', +}; + +messages[6] = { + stream_id: stream1, + id: id += 1, + topic: topic5, + sender_id: sender2, + type: 'stream', +}; + +messages[7] = { + stream_id: stream1, + id: id += 1, + topic: topic6, + sender_id: sender1, + type: 'stream', +}; + +messages[8] = { + stream_id: stream1, + id: id += 1, + topic: topic6, + sender_id: sender2, + type: 'stream', +}; + +messages[9] = { + stream_id: stream1, + id: id += 1, + topic: topic7, + sender_id: sender1, + type: 'stream', +}; + +function verify_topic_data(all_topics, stream, topic, last_msg_id, + participated, starred_count, is_muted) { + // default is_muted to false since most of the test cases will + // be not muted + is_muted = is_muted || false; + const topic_data = all_topics.get(stream + ':' + topic); + assert.equal(topic_data.last_msg_id, last_msg_id); + assert.equal(topic_data.participated, participated); + assert.equal(topic_data.starred.size, starred_count); + assert.equal(topic_data.muted, is_muted); +} + +run_test('basic assertions', () => { + + rt.process_messages(messages); + let all_topics = rt.get(); + + // Check for expected lengths. + // total 7 topics, 1 muted + assert.equal(all_topics.size, 7); + assert.equal(Array.from(all_topics.keys()).toString(), + '1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2,1:topic-1'); + + rt.process_message({ + type: 'private', + }); + + // Private msgs are not processed. + assert.equal(all_topics.size, 7); + assert.equal(Array.from(all_topics.keys()).toString(), + '1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2,1:topic-1'); + + // participated but not starred + verify_topic_data(all_topics, stream1, topic1, messages[0].id, true, 0); + + // starred and participated + verify_topic_data(all_topics, stream1, topic2, messages[2].id, true, 1); + + // No message was sent by us. + verify_topic_data(all_topics, stream1, topic3, messages[3].id, false, 0); + + // Not participated but starred + verify_topic_data(all_topics, stream1, topic4, messages[4].id, false, 1); + + // topic1 now starred + rt.process_message({ + stream_id: stream1, + id: id += 1, + topic: topic1, + sender_id: sender1, + type: 'stream', + starred: true, + }); + + all_topics = rt.get(); + + assert.equal(Array.from(all_topics.keys()).toString(), + '1:topic-1,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-3,1:topic-2'); + + verify_topic_data(all_topics, stream1, topic1, id, true, 1); + + // topic3 now participated + rt.process_message({ + stream_id: stream1, + id: id += 1, + topic: topic3, + sender_id: sender1, + type: 'stream', + }); + + all_topics = rt.get(); + assert.equal(Array.from(all_topics.keys()).toString(), + '1:topic-3,1:topic-1,1:topic-7,1:topic-6,1:topic-5,1:topic-4,1:topic-2'); + verify_topic_data(all_topics, stream1, topic3, id, true, 0); + + // Send new message to topic7 (muted) + rt.process_message({ + stream_id: stream1, + id: id += 1, + topic: topic7, + sender_id: sender1, + type: 'stream', + }); + + all_topics = rt.get(); + assert.equal(Array.from(all_topics.keys()).toString(), + '1:topic-7,1:topic-3,1:topic-1,1:topic-6,1:topic-5,1:topic-4,1:topic-2'); + verify_topic_data(all_topics, stream1, topic7, id, true, 0, true); +}); diff --git a/static/js/bundles/app.js b/static/js/bundles/app.js index 5d8014444e..d4cc3ccc1a 100644 --- a/static/js/bundles/app.js +++ b/static/js/bundles/app.js @@ -192,6 +192,7 @@ import "../ui_init.js"; import "../emoji_picker.js"; import "../compose_ui.js"; import "../panels.js"; +import "../recent_topics.js"; import "../settings_ui.js"; import "../search_pill.js"; import "../search_pill_widget.js"; diff --git a/static/js/global.d.ts b/static/js/global.d.ts index 893bf6e161..b89df18fb3 100644 --- a/static/js/global.d.ts +++ b/static/js/global.d.ts @@ -99,6 +99,7 @@ declare let reactions: any; declare let realm_icon: any; declare let realm_logo: any; declare let recent_senders: any; +declare let recent_topics: any; declare let reload: any; declare let reload_state: any; declare let reminder: any; diff --git a/static/js/message_events.js b/static/js/message_events.js index 44fd4e58b6..a73f328998 100644 --- a/static/js/message_events.js +++ b/static/js/message_events.js @@ -99,6 +99,7 @@ exports.insert_new_messages = function insert_new_messages(messages, sent_by_thi notifications.received_messages(messages); stream_list.update_streams_sidebar(); pm_list.update_private_messages(); + recent_topics.process_messages(messages); }; exports.update_messages = function update_messages(events) { diff --git a/static/js/message_fetch.js b/static/js/message_fetch.js index 79f927a6c4..f591422508 100644 --- a/static/js/message_fetch.js +++ b/static/js/message_fetch.js @@ -49,6 +49,7 @@ function process_result(data, opts) { activity.process_loaded_messages(messages); stream_list.update_streams_sidebar(); pm_list.update_private_messages(); + recent_topics.process_messages(messages); if (opts.pre_scroll_cont !== undefined) { opts.pre_scroll_cont(data); diff --git a/static/js/recent_topics.js b/static/js/recent_topics.js new file mode 100644 index 0000000000..d8dbfba896 --- /dev/null +++ b/static/js/recent_topics.js @@ -0,0 +1,48 @@ +const topics = new Map(); // Key is stream-id:topic. + +exports.process_messages = function (messages) { + for (const msg of messages) { + exports.process_message(msg); + } +}; + +exports.process_message = function (msg) { + if (msg.type !== 'stream') { + return false; + } + // Initialize topic data + const key = msg.stream_id + ':' + msg.topic; + if (!topics.has(key)) { + topics.set(key, { + last_msg_id: -1, + starred: new Set(), + participated: false, + muted: 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) { + topic_data.last_msg_id = msg.id; + } + if (msg.starred) { + topic_data.starred.add(msg.id); + } + topic_data.participated = is_ours || topic_data.participated; + topic_data.muted = topic_data.muted || muting.is_topic_muted(msg.stream_id, msg.topic); + return true; +}; + +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(); +}; + +window.recent_topics = exports;