Files
zulip/static/js/topic_list.js
Anders Kaseorg 141088586b Completely replace perfect-scrollbar with SimpleBar.
perfect-scrollbar replaces both the appearance and the behavior of the
scrollbar, and its emulated behavior will never feel native on most
platforms.  SimpleBar customizes the appearance while preserving the
native behavior.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2019-05-17 12:06:51 -07:00

344 lines
9.3 KiB
JavaScript

var Dict = require('./dict').Dict;
var topic_list = (function () {
var exports = {};
/*
Track all active widgets with a Dict.
(We have at max one for now, but we may
eventually allow multiple streams to be
expanded.)
*/
var active_widgets = new Dict();
// We know whether we're zoomed or not.
var zoomed = false;
exports.remove_expanded_topics = function () {
stream_popover.hide_topic_popover();
_.each(active_widgets.values(), function (widget) {
widget.remove();
});
active_widgets.clear();
};
exports.close = function () {
zoomed = false;
exports.remove_expanded_topics();
};
exports.zoom_out = function () {
zoomed = false;
var stream_ids = active_widgets.keys();
if (stream_ids.length !== 1) {
blueslip.error('Unexpected number of topic lists to zoom out.');
return;
}
var stream_id = stream_ids[0];
var widget = active_widgets.get(stream_id);
var parent_widget = widget.get_parent();
exports.rebuild(parent_widget, stream_id);
};
function update_unread_count(unread_count_elem, count) {
// unread_count_elem is a jquery element...we expect DOM
// to look like this:
// <div class="topic-unread-count {{#if is_zero}}zero_count{{/if}}">
// <div class="value">{{unread}}</div>
// </div>
var value_span = unread_count_elem.find('.value');
if (value_span.length === 0) {
blueslip.error('malformed dom for unread count');
return;
}
if (count === 0) {
unread_count_elem.hide();
value_span.text('');
} else {
unread_count_elem.removeClass("zero_count");
unread_count_elem.show();
value_span.text(count);
}
}
exports.set_count = function (stream_id, topic, count) {
var widget = active_widgets.get(stream_id);
if (widget === undefined) {
return;
}
widget.set_count(topic, count);
};
exports.widget = function (parent_elem, my_stream_id) {
var self = {};
self.build_list = function () {
self.topic_items = new Dict({fold_case: true});
var max_topics = 5;
var topic_names = topic_data.get_recent_names(my_stream_id);
var ul = $('<ul class="topic-list">');
_.each(topic_names, function (topic_name, idx) {
var num_unread = unread.num_unread_for_topic(my_stream_id, topic_name);
if (!zoomed) {
// Show the most recent topics, as well as any with unread messages
var show_topic = idx < max_topics || num_unread > 0 ||
self.active_topic === topic_name.toLowerCase();
if (!show_topic) {
return;
}
}
var topic_info = {
topic_name: topic_name,
unread: num_unread,
is_zero: num_unread === 0,
is_muted: muting.is_topic_muted(my_stream_id, topic_name),
url: hash_util.by_stream_topic_uri(my_stream_id, topic_name),
};
var li = $(templates.render('topic_list_item', topic_info));
self.topic_items.set(topic_name, li);
ul.append(li);
});
// Now, we decide whether we need to show the "more topics"
// widget. We need it if there are at least 5 topics in the
// frontend's cache, or if we (possibly) don't have all
// historical topics in the browser's cache.
var show_more = self.build_more_topics_section();
var sub = stream_data.get_sub_by_id(my_stream_id);
if (topic_names.length > max_topics || !stream_data.all_topics_in_cache(sub)) {
ul.append(show_more);
}
return ul;
};
self.build_more_topics_section = function () {
var show_more_html = templates.render('more_topics');
return $(show_more_html);
};
self.get_parent = function () {
return parent_elem;
};
self.get_stream_id = function () {
return my_stream_id;
};
self.get_dom = function () {
return self.dom;
};
self.remove = function () {
self.dom.remove();
};
self.num_items = function () {
return self.topic_items.num_items();
};
self.set_count = function (topic, count) {
if (!self.topic_items.has(topic)) {
// This can happen for truncated topic lists. No need
// to warn about it.
return;
}
var topic_li = self.topic_items.get(topic);
var unread_count_elem = topic_li.find('.topic-unread-count').expectOne();
update_unread_count(unread_count_elem, count);
};
self.activate_topic = function () {
var li = self.topic_items.get(self.active_topic);
if (li) {
li.addClass('active-sub-filter');
}
};
self.show_spinner = function () {
// The spinner will go away once we get results and redraw
// the whole list.
var spinner = self.dom.find('.searching-for-more-topics');
spinner.show();
};
self.show_no_more_topics = function () {
var elem = self.dom.find('.no-more-topics-found');
elem.show();
self.no_more_topics = true;
};
self.build = function (active_topic, no_more_topics) {
self.no_more_topics = false; // for now
if (active_topic) {
active_topic = active_topic.toLowerCase();
}
self.active_topic = active_topic;
self.dom = self.build_list();
parent_elem.append(self.dom);
// We often rebuild an entire topic list, and the
// caller will pass us in no_more_topics as true
// if we were showing "No more topics found" from
// the initial zooming.
if (no_more_topics) {
self.show_no_more_topics();
}
if (active_topic) {
self.activate_topic();
}
};
return self;
};
exports.active_stream_id = function () {
var stream_ids = active_widgets.keys();
if (stream_ids.length !== 1) {
return;
}
return stream_ids[0];
};
exports.get_stream_li = function () {
var widgets = active_widgets.values();
if (widgets.length !== 1) {
return;
}
var stream_li = widgets[0].get_parent();
return stream_li;
};
exports.need_to_show_no_more_topics = function (stream_id) {
// This function is important, and the use case here is kind of
// subtle. We do complete redraws of the topic list when new
// messages come in, and we don't want to overwrite the
// "no more topics" error message.
if (!zoomed) {
return false;
}
if (!active_widgets.has(stream_id)) {
return false;
}
var widget = active_widgets.get(stream_id);
return widget.no_more_topics;
};
exports.rebuild = function (stream_li, stream_id) {
var active_topic = narrow_state.topic();
var no_more_topics = exports.need_to_show_no_more_topics(stream_id);
exports.remove_expanded_topics();
var widget = exports.widget(stream_li, stream_id);
widget.build(active_topic, no_more_topics);
active_widgets.set(stream_id, widget);
};
// For zooming, we only do topic-list stuff here...let stream_list
// handle hiding/showing the non-narrowed streams
exports.zoom_in = function () {
zoomed = true;
var stream_id = exports.active_stream_id();
if (!stream_id) {
blueslip.error('Cannot find widget for topic history zooming.');
return;
}
var active_widget = active_widgets.get(stream_id);
var before_count = active_widget.num_items();
function on_success() {
if (!active_widgets.has(stream_id)) {
blueslip.warn('User re-narrowed before topic history was returned.');
return;
}
if (!zoomed) {
blueslip.warn('User zoomed out before topic history was returned.');
// Note that we could attempt to re-draw the zoomed out topic list
// here, given that we have more history, but that might be more
// confusing than helpful to a user who is likely trying to browse
// other streams.
return;
}
var widget = active_widgets.get(stream_id);
exports.rebuild(widget.get_parent(), stream_id);
var after_count = widget.num_items();
if (after_count === before_count) {
widget.show_no_more_topics();
}
}
ui.get_scroll_element($('#stream-filters-container')).scrollTop(0);
active_widget.show_spinner();
topic_data.get_server_history(stream_id, on_success);
};
exports.initialize = function () {
$('#stream_filters').on('click', '.topic-box', function (e) {
if (e.metaKey || e.ctrlKey) {
return;
}
// In a more componentized world, we would delegate some
// of this stuff back up to our parents.
var stream_id = $(e.target).parents('.narrow-filter').attr('data-stream-id');
var sub = stream_data.get_sub_by_id(stream_id);
var topic = $(e.target).parents('li').attr('data-topic-name');
narrow.activate([
{operator: 'stream', operand: sub.name},
{operator: 'topic', operand: topic}],
{trigger: 'sidebar'});
e.preventDefault();
});
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = topic_list;
}
window.topic_list = topic_list;