From 3f0ed46fa2d2da8ca4c8b0503aad75b9e8abe535 Mon Sep 17 00:00:00 2001 From: m-e-l-u-h-a-n Date: Tue, 8 Jun 2021 22:12:06 +0530 Subject: [PATCH] left-sidebar: Add topic filter input in zoomed topic view. Fixes: #18505. --- frontend_tests/node_tests/topic_list_data.js | 26 +++++-- static/js/click_handlers.js | 3 + static/js/topic_list.js | 74 ++++++++++++++++++++ static/js/topic_list_data.js | 8 ++- static/styles/app_components.css | 6 ++ static/styles/left_sidebar.css | 17 +++++ static/templates/filter_topics.hbs | 8 +++ 7 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 static/templates/filter_topics.hbs diff --git a/frontend_tests/node_tests/topic_list_data.js b/frontend_tests/node_tests/topic_list_data.js index 761d1637a8..a2b77978e3 100644 --- a/frontend_tests/node_tests/topic_list_data.js +++ b/frontend_tests/node_tests/topic_list_data.js @@ -16,6 +16,10 @@ const narrow_state = mock_esm("../../static/js/narrow_state", { topic() {}, }); +const topic_list = mock_esm("../../static/js/topic_list", { + get_topic_search_term() {}, +}); + const stream_data = zrequire("stream_data"); const stream_topic_history = zrequire("stream_topic_history"); const topic_list_data = zrequire("topic_list_data"); @@ -50,14 +54,16 @@ test("get_list_info w/real stream_topic_history", (override) => { num_possible_topics: 0, }); - for (const i of _.range(7)) { - const topic_name = "topic " + i; + function add_topic_message(topic_name, message_id) { stream_topic_history.add_message({ stream_id: general.stream_id, topic_name, - message_id: 1000 + i, + message_id, }); } + for (const i of _.range(7)) { + add_topic_message("topic " + i, 1000 + i); + } override(narrow_state, "topic", () => "topic 6"); @@ -75,12 +81,24 @@ test("get_list_info w/real stream_topic_history", (override) => { url: "#narrow/stream/556-general/topic/topic.206", }); - // If we zoom in, we'll show all 7 topics. + // If we zoom in, our results based on topic filter. + // If topic search input is empty, we show all 7 topics. + const zoomed = true; + override(topic_list, "get_topic_search_term", () => ""); list_info = get_list_info(zoomed); assert.equal(list_info.items.length, 7); assert.equal(list_info.more_topics_unreads, 0); assert.equal(list_info.num_possible_topics, 7); + + add_topic_message("After Brooklyn", 1008); + add_topic_message("Catering", 1009); + // when topic search is open then we list topics based on search term. + override(topic_list, "get_topic_search_term", () => "b,c"); + list_info = get_list_info(zoomed); + assert.equal(list_info.items.length, 2); + assert.equal(list_info.more_topics_unreads, 0); + assert.equal(list_info.num_possible_topics, 2); }); test("get_list_info unreads", (override) => { diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js index 73828afd8c..15f9f23a7c 100644 --- a/static/js/click_handlers.js +++ b/static/js/click_handlers.js @@ -40,6 +40,7 @@ import * as settings_toggle from "./settings_toggle"; import * as stream_edit from "./stream_edit"; import * as stream_list from "./stream_list"; import * as stream_popover from "./stream_popover"; +import * as topic_list from "./topic_list"; import * as ui_util from "./ui_util"; import * as unread_ops from "./unread_ops"; import * as user_status_ui from "./user_status_ui"; @@ -694,6 +695,8 @@ export function initialize() { // LEFT SIDEBAR + $("body").on("click", "#clear_search_topic_button", topic_list.clear_topic_search); + $(".streams_filter_icon").on("click", (e) => { e.stopPropagation(); stream_list.toggle_filter_displayed(e); diff --git a/static/js/topic_list.js b/static/js/topic_list.js index 305bc0adf6..4c7c20acc6 100644 --- a/static/js/topic_list.js +++ b/static/js/topic_list.js @@ -1,6 +1,7 @@ import $ from "jquery"; import _ from "lodash"; +import render_filter_topics from "../templates/filter_topics.hbs"; import render_more_topics from "../templates/more_topics.hbs"; import render_more_topics_spinner from "../templates/more_topics_spinner.hbs"; import render_topic_list_item from "../templates/topic_list_item.hbs"; @@ -115,12 +116,24 @@ export function spinner_li() { }; } +function filter_topics_li() { + const eq = (other) => other.filter_topics; + + return { + key: "filter", + filter_topics: true, + render: render_filter_topics, + eq, + }; +} + export class TopicListWidget { prior_dom = undefined; constructor(parent_elem, my_stream_id) { this.parent_elem = parent_elem; this.my_stream_id = my_stream_id; + this.topic_search_text = ""; } build_list(spinner) { @@ -141,6 +154,10 @@ export class TopicListWidget { nodes.push(spinner_li()); } else if (!is_showing_all_possible_topics) { nodes.push(more_li(more_topics_unreads)); + } else if (zoomed) { + // In the zoomed topic view, we need to add the input + // for filtering through list of topics. + nodes.unshift(filter_topics_li()); } const dom = vdom.ul({ @@ -159,7 +176,36 @@ export class TopicListWidget { return this.my_stream_id; } + update_topic_search_text(text) { + this.topic_search_text = text; + } + + update_topic_search_input() { + const input = this.parent_elem.find("#filter-topic-input"); + if (input.length) { + // Restore topic search text saved in remove() + // after the element was rerendered. + input.val(this.topic_search_text); + input.trigger("focus"); + + // setup event handlers. + const rebuild_list = () => this.build(); + input.on("input", rebuild_list); + } + } + remove() { + // If text was present in the topic search filter, we store + // the input value lazily before removing old elements. This + // is a workaround for the quirk that the filter input is part + // of the region that we rerender. + const input = this.parent_elem.find("#filter-topic-input"); + if (input.length) { + this.update_topic_search_text(input.val()); + } else { + // Clear the topic search input when zooming out. + this.update_topic_search_text(""); + } this.parent_elem.find(".topic-list").remove(); this.prior_dom = undefined; } @@ -170,6 +216,7 @@ export class TopicListWidget { const replace_content = (html) => { this.remove(); this.parent_elem.append(html); + this.update_topic_search_input(); }; const find = () => this.parent_elem.find(".topic-list"); @@ -180,6 +227,25 @@ export class TopicListWidget { } } +export function clear_topic_search(e) { + e.stopPropagation(); + const input = $("#filter-topic-input"); + if (input.length) { + input.val(""); + input.trigger("blur"); + + // Since this changes the contents of the search input, we + // need to rerender the topic list. + const stream_ids = Array.from(active_widgets.keys()); + + const stream_id = stream_ids[0]; + const widget = active_widgets.get(stream_id); + const parent_widget = widget.get_parent(); + + rebuild(parent_widget, stream_id); + } +} + export function active_stream_id() { const stream_ids = Array.from(active_widgets.keys()); @@ -255,6 +321,14 @@ export function zoom_in() { stream_topic_history_util.get_server_history(stream_id, on_success); } +export function get_topic_search_term() { + const filter = $("#filter-topic-input"); + if (filter.val() === undefined) { + return ""; + } + return filter.val().trim(); +} + export function initialize() { $("#stream_filters").on("click", ".topic-box", (e) => { if (e.metaKey || e.ctrlKey) { diff --git a/static/js/topic_list_data.js b/static/js/topic_list_data.js index 34d7115bf8..bb835a3e39 100644 --- a/static/js/topic_list_data.js +++ b/static/js/topic_list_data.js @@ -2,7 +2,9 @@ import * as hash_util from "./hash_util"; import * as muting from "./muting"; import * as narrow_state from "./narrow_state"; import * as stream_topic_history from "./stream_topic_history"; +import * as topic_list from "./topic_list"; import * as unread from "./unread"; +import * as util from "./util"; const max_topics = 5; const max_topics_with_unread = 8; @@ -17,7 +19,11 @@ export function get_list_info(stream_id, zoomed) { active_topic = active_topic.toLowerCase(); } - const topic_names = stream_topic_history.get_recent_topic_names(stream_id); + let topic_names = stream_topic_history.get_recent_topic_names(stream_id); + if (zoomed) { + const search_term = topic_list.get_topic_search_term(); + topic_names = util.filter_by_word_prefix_match(topic_names, search_term, (item) => item); + } const items = []; diff --git a/static/styles/app_components.css b/static/styles/app_components.css index 7c23481c12..1981373eef 100644 --- a/static/styles/app_components.css +++ b/static/styles/app_components.css @@ -413,6 +413,12 @@ div.overlay { width: calc(100% - 34px); margin-left: 10px; } + .input-append .topic-list-filter { + /* Input width = 100% - 11px margin x2 - 6px padding x2 - 1px border x2. */ + width: calc(100% - 36px); + margin-left: 11px; + margin-bottom: 5px; + } } .stream-selection-header-colorblock { diff --git a/static/styles/left_sidebar.css b/static/styles/left_sidebar.css index 5953545c6f..43c7759a9c 100644 --- a/static/styles/left_sidebar.css +++ b/static/styles/left_sidebar.css @@ -100,6 +100,15 @@ li.show-more-topics { padding-right: 0; padding-bottom: 2px; padding-left: $topic_indent; + + &.filter-topics { + padding-bottom: 0; + } + + .input-append.topic_search_section { + margin-bottom: 3px; + margin-left: 3px; + } } } } @@ -515,6 +524,14 @@ li.expanded_private_message { width: 236px; } +.topic-list-filter { + /* Input width = 100% - 30px right-margin - 6px right-padding */ + /* To keep the right edge of input along with its borders inline with other + topic items we consider to subtract the space given for right margin of + other items, and right padding of input element. */ + width: calc(100% - 36px); +} + .zero_count { visibility: hidden; } diff --git a/static/templates/filter_topics.hbs b/static/templates/filter_topics.hbs new file mode 100644 index 0000000000..308ee80ac7 --- /dev/null +++ b/static/templates/filter_topics.hbs @@ -0,0 +1,8 @@ +
  • +
    + + +
    +