mirror of
https://github.com/zulip/zulip.git
synced 2025-11-11 17:36:27 +00:00
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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
static/templates/recent_topic_row.hbs
Normal file
23
static/templates/recent_topic_row.hbs
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user