recent_topics: Display recent topics in a table.

* Add action to mute topics.
* We don't need to store muted data per topic as previously planned.
* Moved launch topic test to the top so that they run on non-modified
  data.
This commit is contained in:
Aman Agrawal
2020-05-22 11:46:08 +05:30
committed by Tim Abbott
parent 9328dc8437
commit 464b541363
8 changed files with 321 additions and 55 deletions

View File

@@ -11,9 +11,11 @@ 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('recent_topics', {
process_messages: noop,
});
set_global('page_params', {
pointer: 444,
});

View File

@@ -1,4 +1,3 @@
let rt = zrequire('recent_topics');
zrequire('message_util');
set_global('$', global.make_zjquery());
@@ -15,6 +14,25 @@ set_global('people', {
return id === 1;
},
});
set_global('XDate', zrequire('XDate', 'xdate'));
set_global('timerender', {
last_seen_status_from_date: () => {
return "Just now";
},
});
set_global('unread', {
unread_topic_counter: {
get: () => { return 1; },
},
});
set_global('hash_util', {
by_stream_uri: () => {
return "https://www.example.com";
},
by_stream_topic_uri: () => {
return "https://www.example.com";
},
});
// Custom Data
@@ -57,11 +75,17 @@ set_global('message_list', {
},
},
});
set_global('message_store', {
get: (msg_id) => {
return messages[msg_id - 1];
},
});
let id = 0;
messages[0] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic1,
sender_id: sender1,
@@ -70,6 +94,7 @@ messages[0] = {
messages[1] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic2,
sender_id: sender1,
@@ -78,6 +103,7 @@ messages[1] = {
messages[2] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic2,
sender_id: sender2,
@@ -87,6 +113,7 @@ messages[2] = {
messages[3] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic3,
sender_id: sender2,
@@ -95,6 +122,7 @@ messages[3] = {
messages[4] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic4,
sender_id: sender2,
@@ -104,6 +132,7 @@ messages[4] = {
messages[5] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic5,
sender_id: sender1,
@@ -112,6 +141,7 @@ messages[5] = {
messages[6] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic5,
sender_id: sender2,
@@ -120,6 +150,7 @@ messages[6] = {
messages[7] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic6,
sender_id: sender1,
@@ -128,6 +159,7 @@ messages[7] = {
messages[8] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic6,
sender_id: sender2,
@@ -136,6 +168,7 @@ messages[8] = {
messages[9] = {
stream_id: stream1,
stream: 'stream1',
id: id += 1,
topic: topic7,
sender_id: sender1,
@@ -143,22 +176,120 @@ messages[9] = {
};
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;
participated, starred_count) {
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', () => {
run_test("test_recent_topics_launch", () => {
// Note: unread count and urls are fake,
// since they are generated in external libraries
// and are not to be tested here.
const expected = {
recent_topics: [
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-7',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: true,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-6',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-5',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-4',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-3',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-2',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
{
stream_id: 1,
stream: 'stream1',
topic: 'topic-1',
unread_count: 1,
last_msg_time: 'Just now',
stream_url: 'https://www.example.com',
topic_url: 'https://www.example.com',
hidden: false,
},
],
};
global.stub_templates(function (template_name, data) {
assert.equal(template_name, 'recent_topics_table');
assert.deepEqual(data, expected);
return '<recent_topics table stub>';
});
const rt = zrequire('recent_topics');
rt.process_messages(messages);
rt.launch();
overlays.close_callback();
// incorrect topic_key
assert.equal(rt.inplace_rerender('stream_unknown:topic_unknown'), false);
});
// template rendering is tested in test_recent_topics_launch.
global.stub_templates(function () {
return '<recent_topics table stub>';
});
run_test('basic assertions', () => {
const rt = zrequire('recent_topics');
rt.process_messages(messages);
let all_topics = rt.get();
// update a message
rt.process_messages([messages[9]]);
// Check for expected lengths.
// total 7 topics, 1 muted
assert.equal(all_topics.size, 7);
@@ -218,6 +349,7 @@ run_test('basic assertions', () => {
verify_topic_data(all_topics, stream1, topic3, id, true, 0);
// Send new message to topic7 (muted)
// The topic will be hidden when displayed
rt.process_message({
stream_id: stream1,
id: id += 1,
@@ -229,24 +361,20 @@ run_test('basic assertions', () => {
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);
// unmute topic7
rt.update_topic_is_muted(stream1, topic7, false);
all_topics = rt.get();
verify_topic_data(all_topics, stream1, topic7, id, true, 0, false);
assert.equal(rt.update_topic_is_muted(stream1, topic7, false), true);
// mute topic7
rt.update_topic_is_muted(stream1, topic7, true);
all_topics = rt.get();
verify_topic_data(all_topics, stream1, topic7, id, true, 0, true);
assert.equal(rt.update_topic_is_muted(stream1, topic7, true), true);
// a topic gets muted which we are not tracking
assert.equal(rt.update_topic_is_muted(stream1, "topic-10", true), false);
});
run_test('test_topic_edit', () => {
rt = zrequire('recent_topics');
// NOTE: This test should always run in the end as it modified the messages data.
const rt = zrequire('recent_topics');
rt.process_messages(messages);
let all_topics = rt.get();
@@ -266,19 +394,6 @@ run_test('test_topic_edit', () => {
verify_topic_data(all_topics, stream1, topic8, messages[8].id, true, 0);
assert.equal(all_topics.get(stream1 + ":" + topic6), undefined);
////////////////// test change topic to muted topic //////////////////
verify_topic_data(all_topics, stream1, topic8, messages[8].id, true, 0);
verify_topic_data(all_topics, stream1, topic7, messages[9].id, true, 0, true);
// change topic of topic8 to topic7
messages[7].topic = topic7;
messages[8].topic = topic7;
rt.process_topic_edit(stream1, topic8, topic7);
all_topics = rt.get();
assert.equal(all_topics.get(stream1 + ":" + topic8), undefined);
verify_topic_data(all_topics, stream1, topic7, messages[9].id, true, 0, true);
////////////////// test stream change //////////////////
verify_topic_data(all_topics, stream1, topic1, messages[0].id, true, 0);
assert.equal(all_topics.get(stream2 + ":" + topic1), undefined);
@@ -302,13 +417,3 @@ run_test('test_topic_edit', () => {
assert.equal(all_topics.get(stream2 + ":" + topic1), undefined);
verify_topic_data(all_topics, stream3, topic9, messages[0].id, true, 0);
});
run_test("test_recent_topics_launch", () => {
global.stub_templates(function (template_name) {
assert.equal(template_name, 'recent_topics_table');
return '<recent_topics table stub>';
});
rt.launch();
overlays.close_callback();
});

View File

@@ -528,11 +528,21 @@ exports.make_zjquery = function (opts) {
};
};
fn.after = function (s) {
return s;
};
fn.before = function (s) {
return s;
};
zjquery.fn = fn;
zjquery.clear_all_elements = function () {
elems.clear();
};
zjquery.escapeSelector = function (s) {
return s;
};
return zjquery;
};

View File

@@ -1,13 +1,24 @@
const render_recent_topics_body = require('../templates/recent_topics_table.hbs');
const render_recent_topic_row = require('../templates/recent_topic_row.hbs');
const topics = new Map(); // Key is stream-id:topic.
exports.process_messages = function (messages) {
// Since a complete re-render is expensive, we
// only do it if there are more than 5 messages
// to process.
let do_inplace_rerender = true;
if (messages.length > 5) {
do_inplace_rerender = false;
}
for (const msg of messages) {
exports.process_message(msg);
exports.process_message(msg, do_inplace_rerender);
}
if (!do_inplace_rerender) {
exports.complete_rerender();
}
};
exports.process_message = function (msg) {
exports.process_message = function (msg, do_inplace_rerender) {
if (msg.type !== 'stream') {
return false;
}
@@ -18,7 +29,6 @@ exports.process_message = function (msg) {
last_msg_id: -1,
starred: new Set(),
participated: false,
muted: false,
});
}
// Update topic data
@@ -31,17 +41,11 @@ exports.process_message = function (msg) {
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;
};
exports.update_topic_is_muted = function (stream_id, topic, is_muted) {
const key = stream_id + ":" + topic;
if (!topics.has(key)) {
return false;
if (do_inplace_rerender) {
exports.inplace_rerender(key);
}
const topic_data = topics.get(stream_id + ":" + topic);
topic_data.muted = is_muted;
return true;
};
function get_sorted_topics() {
@@ -68,10 +72,101 @@ exports.process_topic_edit = function (old_stream_id, old_topic, new_topic, new_
exports.process_messages(new_topic_msgs);
};
exports.launch = function () {
const rendered_body = render_recent_topics_body();
$('#recent_topics_table').html(rendered_body);
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 topic = last_msg.topic;
const time = new XDate(last_msg.timestamp * 1000);
const last_msg_time = timerender.last_seen_status_from_date(time);
const unread_count = unread.unread_topic_counter.get(stream_id, topic);
const hidden = muting.is_topic_muted(stream_id, topic);
return {
stream_id: stream_id,
stream: stream,
topic: topic,
unread_count: unread_count,
last_msg_time: last_msg_time,
stream_url: hash_util.by_stream_uri(stream_id),
topic_url: hash_util.by_stream_topic_uri(stream_id, topic),
hidden: hidden,
};
}
function format_all_topics() {
const topics_array = [];
for (const [, value] of exports.get()) {
topics_array.push(format_topic(value));
}
return topics_array;
}
function get_topic_row(topic_key) {
// topic_key = stream_id + ":" + topic
return $("#" + $.escapeSelector("recent_topic:" + topic_key));
}
exports.inplace_rerender = function (topic_key) {
// We remove topic from the UI and reinsert it.
// This makes sure we maintain the correct order
// of topics.
const topic_data = topics.get(topic_key);
if (topic_data === undefined) {
return false;
}
const formatted_values = format_topic(topic_data);
const topic_row = get_topic_row(topic_key);
topic_row.remove();
const rendered_row = render_recent_topic_row(formatted_values);
const sorted_topic_keys = Array.from(get_sorted_topics().keys());
const topic_index = sorted_topic_keys.findIndex(
function (key) {
if (key === topic_key) {
return true;
}
return false;
}
);
if (topic_index === 0) {
// Note: In this function length of sorted_topic_keys is always >= 2,
// since it is called after a complete_rerender has taken place.
// A complete_rerender only takes place after there is a topic to
// display. So, this can at min be the second topic we are dealing with.
get_topic_row(sorted_topic_keys[1]).before(rendered_row);
}
get_topic_row(sorted_topic_keys[topic_index - 1]).after(rendered_row);
};
exports.update_topic_is_muted = function (stream_id, topic, is_muted) {
const key = stream_id + ":" + topic;
if (!topics.has(key)) {
// we receive mute request for a topic we are
// not tracking currently
return false;
}
if (is_muted) {
get_topic_row(key).hide();
} else {
get_topic_row(key).show();
}
return true;
};
exports.complete_rerender = function () {
// NOTE: This function is grows expensive with
// number of topics. Only call when necessary.
// This functions takes around 1ms per topic to process.
const rendered_body = render_recent_topics_body({
recent_topics: format_all_topics(),
});
$('#recent_topics_table').html(rendered_body);
};
exports.launch = function () {
overlays.open_overlay({
name: 'recent_topics',
overlay: $('#recent_topics_overlay'),

View File

@@ -421,6 +421,11 @@ on a dark background, and don't change the dark labels dark either. */
background-color: hsla(0, 0%, 0%, 0.2);
}
.table-hover tbody tr:hover td,
.table-hover tbody tr:hover th {
background-color: hsla(0, 0%, 0%, 0.5);
}
.table-striped tbody tr:nth-child(odd) td {
background-color: hsl(212, 28%, 18%);
}

View File

@@ -56,5 +56,18 @@
margin: 0px;
padding: 15px;
overflow: auto;
thead {
background-color: hsl(0, 0%, 27%);
color: hsl(0, 0%, 100%);
}
.recent_topic_unread_count {
text-align: center;
// width is kept less than width of
// header text "Unread" so, that final width
// is set to header's width.
width: 1px;
}
}
}

View File

@@ -0,0 +1,23 @@
<tr id="recent_topic:{{stream_id}}:{{topic}}" {{#if hidden}}style="display:none;"{{/if}}>
<td class="recent_topic_unread_count">
{{#if unread_count}}
{{unread_count}}
{{else}}
<i class="fa fa-check-circle" title="{{t 'All messages read' }}" aria-hidden="true"></i>
{{/if}}
</td>
<td class="recent_topic_stream">
<a href="{{stream_url}}">{{stream}}</a>
</td>
<td class="recent_topic_name">
<a href="{{topic_url}}">{{topic}}</a>
</td>
<td class="recent_topic_actions">
<i class="fa fa-bell-slash on_hover_topic_mute recipient_bar_icon" data-stream-id="{{stream_id}}" data-topic-name="{{topic}}" title="{{t 'Mute topic' }}" role="button" tabindex="0" aria-label="{{t 'Mute topic' }}"></i>
</td>
<td class='recent_topic_users'>
</td>
<td class="recent_topic_timestamp">
{{ last_msg_time }}
</td>
</tr>

View File

@@ -1,2 +1,15 @@
<table>
<table class="table table-responsive table-hover">
<thead>
<tr>
<th>{{t 'Unread' }}</th>
<th>{{t 'Stream' }}</th>
<th>{{t 'Topic' }}</th>
<th>{{t 'Actions' }}</th>
<th>{{t 'Participants' }}</th>
<th>{{t 'Last message' }}</th>
</tr>
</thead>
{{#each recent_topics}}
{{> recent_topic_row}}
{{/each}}
</table>