diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 43198932b4..7e6334bc68 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -284,6 +284,9 @@ EXEMPT_FILES = make_set( "web/tests/lib/real_jquery.js", "web/tests/lib/zjquery_element.js", "web/tests/lib/zpage_billing_params.js", + # There are some important functions which are not called right now but will + # be reused when we add tests for dropdown widget so it doesn't make sense to remove them. + "web/tests/recent_view.test.js", ] ) diff --git a/web/src/recent_view_ui.js b/web/src/recent_view_ui.js index 302265c273..985cc1ec8d 100644 --- a/web/src/recent_view_ui.js +++ b/web/src/recent_view_ui.js @@ -10,6 +10,7 @@ import * as blueslip from "./blueslip"; import * as buddy_data from "./buddy_data"; import * as compose_closed_ui from "./compose_closed_ui"; import * as compose_state from "./compose_state"; +import * as dropdown_widget from "./dropdown_widget"; import * as hash_util from "./hash_util"; import {$t} from "./i18n"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area"; @@ -39,6 +40,7 @@ import * as user_topics from "./user_topics"; import * as views_util from "./views_util"; let topics_widget; +let filters_dropdown_widget; // Sets the number of avatars to display. // Rest of the avatars, if present, are displayed as {+x} const MAX_AVATAR = 4; @@ -81,20 +83,24 @@ const MAX_SELECTABLE_DIRECT_MESSAGE_COLS = 3; // we use localstorage to persist the recent topic filters const ls_key = "recent_topic_filters"; +const ls_dropdown_key = "recent_topic_dropdown_filters"; const ls = localstorage(); let filters = new Set(); +let dropdown_filters = new Set(); const recent_conversation_key_prefix = "recent_conversation:"; export function clear_for_tests() { filters.clear(); + dropdown_filters.clear(); recent_view_data.conversations.clear(); topics_widget = undefined; } export function save_filters() { ls.set(ls_key, [...filters]); + ls.set(ls_dropdown_key, [...dropdown_filters]); } export function is_in_focus() { @@ -362,6 +368,11 @@ export function revive_current_focus() { return true; } + if ($current_focus_elem.hasClass("dropdown-widget-button")) { + $("#recent-view-filter_widget").trigger("focus"); + return true; + } + const filter_button = $current_focus_elem.data("filter"); if (!filter_button) { set_default_focus(); @@ -655,7 +666,7 @@ export function filters_should_hide_topic(topic_data) { return true; } - if (!filters.has("include_muted") && topic_data.type === "stream") { + if (dropdown_filters.has(views_util.FILTERS.UNMUTED_TOPICS) && topic_data.type === "stream") { // We want to show the unmuted or followed topics within muted // streams in Recent Conversations. const topic_unmuted_or_followed = Boolean( @@ -680,6 +691,24 @@ export function filters_should_hide_topic(topic_data) { } } + if ( + dropdown_filters.has(views_util.FILTERS.FOLLOWED_TOPICS) && + topic_data.type === "stream" && + !user_topics.is_topic_followed(msg.stream_id, msg.topic) + ) { + return true; + } + + if ( + dropdown_filters.has(views_util.FILTERS.UNMUTED_TOPICS) && + topic_data.type === "stream" && + (user_topics.is_topic_muted(msg.stream_id, msg.topic) || + stream_data.is_muted(msg.stream_id)) && + !user_topics.is_topic_unmuted_or_followed(msg.stream_id, msg.topic) + ) { + return true; + } + const search_keyword = $("#recent_view_search").val(); const stream_name = stream_data.get_stream_name_from_id(msg.stream_id); if (!topic_in_search_results(search_keyword, stream_name, msg.topic)) { @@ -793,18 +822,28 @@ function show_selected_filters() { function get_recent_view_filters_params() { return { filter_participated: filters.has("participated"), - filter_unread: filters.has("unread"), filter_muted: filters.has("include_muted"), filter_pm: filters.has("include_private"), is_spectator: page_params.is_spectator, }; } +function setup_dropdown_filters_widget() { + filters_dropdown_widget = new dropdown_widget.DropdownWidget({ + ...views_util.COMMON_DROPDOWN_WIDGET_PARAMS, + widget_name: "recent-view-filter", + item_click_callback: filter_click_handler, + $events_container: $("#recent_view_filter_buttons"), + default_id: dropdown_filters.values().next().value, + }); + filters_dropdown_widget.setup(); +} + export function update_filters_view() { const rendered_filters = render_recent_view_filters(get_recent_view_filters_params()); $("#recent_filters_group").html(rendered_filters); show_selected_filters(); - + filters_dropdown_widget.render(); topics_widget.hard_redraw(); } @@ -939,6 +978,25 @@ function callback_after_render() { setTimeout(revive_current_focus, 0); } +function filter_click_handler(event, dropdown, widget) { + event.preventDefault(); + event.stopPropagation(); + + if (page_params.is_spectator) { + // Filter buttons are disabled for spectator. + return; + } + + const filter_id = $(event.currentTarget).attr("data-unique-id"); + // We don't support multiple filters yet, so we clear existing and add the new filter. + dropdown_filters = new Set([filter_id]); + dropdown.hide(); + widget.render(); + save_filters(); + + topics_widget.hard_redraw(); +} + export function complete_rerender() { if (!recent_view_util.is_visible()) { return; @@ -996,6 +1054,7 @@ export function complete_rerender() { }, get_min_load_count, }); + setup_dropdown_filters_widget(); } export function show() { @@ -1217,7 +1276,7 @@ export function change_focused_element($elt, input_key) { set_table_focus(row_focus, col_focus); return true; } - } else if ($elt.hasClass("btn-recent-filters")) { + } else if ($elt.hasClass("btn-recent-filters") || $elt.hasClass("dropdown-widget-button")) { switch (input_key) { case "click": $current_focus_elem = $elt; @@ -1329,19 +1388,31 @@ export function change_focused_element($elt, input_key) { return false; } -export function initialize({ - on_click_participant, - on_mark_pm_as_read, - on_mark_topic_as_read, - maybe_load_older_messages, -}) { +function load_filters() { // load filters from local storage. if (!page_params.is_spectator) { // A user may have a stored filter and can log out // to see web public view. This ensures no filters are // selected for spectators. filters = new Set(ls.get(ls_key)); + dropdown_filters = new Set(ls.get(ls_dropdown_key)); } + // Verify that the dropdown_filters are valid. + const valid_filters = new Set(Object.values(views_util.FILTERS)); + // If saved filters are not in the list of valid filters, we reset to default. + const is_subset = [...dropdown_filters].every((filter) => valid_filters.has(filter)); + if (dropdown_filters.size === 0 || !is_subset) { + dropdown_filters = new Set([views_util.FILTERS.UNMUTED_TOPICS]); + } +} + +export function initialize({ + on_click_participant, + on_mark_pm_as_read, + on_mark_topic_as_read, + maybe_load_older_messages, +}) { + load_filters(); $("body").on("click", "#recent_view_table .recent_view_participant_avatar", function (e) { const participant_user_id = Number.parseInt($(this).parent().attr("data-user-id"), 10); @@ -1445,6 +1516,15 @@ export function initialize({ revive_current_focus(); }); + $("body").on("click", "#recent-view-filter_widget", (e) => { + if (page_params.is_spectator) { + // Filter buttons are disabled for spectator. + return; + } + + change_focused_element($(e.currentTarget), "click"); + }); + $("body").on("click", "td.recent_topic_stream", (e) => { e.stopPropagation(); const topic_row_index = $(e.target).closest("tr").index(); diff --git a/web/styles/inbox.css b/web/styles/inbox.css index bf59b97097..5c560353a7 100644 --- a/web/styles/inbox.css +++ b/web/styles/inbox.css @@ -551,6 +551,7 @@ } } +#recent-view-filter_widget .dropdown_widget_value, #inbox-filter_widget .dropdown_widget_value { text-overflow: ellipsis; white-space: nowrap; @@ -559,30 +560,45 @@ text-align: left; } +#recent-view-filter_widget .fa-chevron-down, #inbox-filter_widget .fa-chevron-down { color: var(--color-icons-inbox); opacity: 0.4; } -.inbox-filter-dropdown-list-container .dropdown-list-wrapper { - width: 100%; - min-width: unset; +.dropdown-list-item-common-styles .dropdown-list-bold-selected { + font-weight: 700; } +.recent-view-filter-dropdown-list-container .dropdown-list-wrapper, +.inbox-filter-dropdown-list-container .dropdown-list-wrapper { + width: 220px; +} + +.recent-view-filter-dropdown-list-container .dropdown-list-item-common-styles, .inbox-filter-dropdown-list-container .dropdown-list-item-common-styles { padding: 5px 10px; + display: flex; + flex-direction: column; } +.recent-view-filter-dropdown-list-container .dropdown-list-item-name, .inbox-filter-dropdown-list-container .dropdown-list-item-name { white-space: nowrap; font-weight: 500; padding: 0; margin: 0; + text-overflow: ellipsis; + overflow: hidden; } +.recent-view-filter-dropdown-list-container .dropdown-list-item-description, .inbox-filter-dropdown-list-container .dropdown-list-item-description { white-space: nowrap; font-weight: 400; + font-size: 13px; opacity: 0.8; padding: 0; + text-overflow: ellipsis; + overflow: hidden; } diff --git a/web/styles/recent_view.css b/web/styles/recent_view.css index 6575c82f4a..6016a8ad0f 100644 --- a/web/styles/recent_view.css +++ b/web/styles/recent_view.css @@ -543,3 +543,17 @@ display: none; position: relative; } + +#recent-view-filter_widget { + display: inline-flex; + width: 150px; + margin: 0 5px 10px 0; + + &:hover { + background-color: var(--color-background-inbox-search-hover); + } + + &:focus { + outline: 2px solid var(--color-outline-focus); + } +} diff --git a/web/templates/recent_view_filters.hbs b/web/templates/recent_view_filters.hbs index e76c27c560..420ec1c00c 100644 --- a/web/templates/recent_view_filters.hbs +++ b/web/templates/recent_view_filters.hbs @@ -1,3 +1,4 @@ +{{> ./dropdown_widget widget_name="recent-view-filter"}} -