From 1aef79078cff7d1ec73ff6a8206c1aab3465ff59 Mon Sep 17 00:00:00 2001 From: sanchi-t Date: Sat, 1 Feb 2025 11:17:23 +0530 Subject: [PATCH] stream_settings: Display archived channels. By default, archived channels will be hidden. --- web/src/hash_util.ts | 13 +-- web/src/stream_data.ts | 4 + web/src/stream_settings_data.ts | 8 +- web/src/stream_settings_ui.ts | 109 +++++++++++++++++- web/styles/subscriptions.css | 12 +- web/templates/message_view_header.hbs | 6 - .../stream_settings_overlay.hbs | 3 + web/tests/stream_data.test.cjs | 2 + web/tests/stream_settings_ui.test.cjs | 11 ++ 9 files changed, 148 insertions(+), 20 deletions(-) diff --git a/web/src/hash_util.ts b/web/src/hash_util.ts index 9cc6af146d..15d6170048 100644 --- a/web/src/hash_util.ts +++ b/web/src/hash_util.ts @@ -256,18 +256,13 @@ export function validate_channels_settings_hash(hash: string): string { const stream_id = Number.parseInt(section, 10); const sub = sub_store.get(stream_id); // There are a few situations where we can't display stream settings: - // 1. This is a stream that's been archived. (sub.is_archived=true) - // 2. The stream ID is invalid. (sub=undefined) - // 3. The current user is a guest, and was unsubscribed from the stream + // 1. The stream ID is invalid. (sub=undefined) + // 2. The current user is a guest, and was unsubscribed from the stream // stream in the current session. (In future sessions, the stream will // not be in sub_store). // - // In all these cases we redirect the user to 'subscribed' tab. - if ( - sub === undefined || - sub.is_archived || - (page_params.is_guest && !stream_data.is_subscribed(stream_id)) - ) { + // In both cases we redirect the user to 'subscribed' tab. + if (sub === undefined || (page_params.is_guest && !stream_data.is_subscribed(stream_id))) { return channels_settings_section_url(); } diff --git a/web/src/stream_data.ts b/web/src/stream_data.ts index 76d77bb22b..60eb521ad1 100644 --- a/web/src/stream_data.ts +++ b/web/src/stream_data.ts @@ -355,6 +355,10 @@ export function subscribed_stream_ids(): number[] { return subscribed_subs().map((sub) => sub.stream_id); } +export function get_archived_subs(): StreamSubscription[] { + return [...stream_info.values()].filter((sub) => sub.is_archived); +} + export function muted_stream_ids(): number[] { return subscribed_subs() .filter((sub) => sub.is_muted) diff --git a/web/src/stream_settings_data.ts b/web/src/stream_settings_data.ts index d0c1789402..a9be9115d7 100644 --- a/web/src/stream_settings_data.ts +++ b/web/src/stream_settings_data.ts @@ -28,6 +28,12 @@ export type SettingsSubscription = StreamSubscription & { subscriber_count: number; }; +export const FILTERS = { + ALL_CHANNELS: "all_channels", + NON_ARCHIVED_CHANNELS: "non_archived_channels", + ARCHIVED_CHANNELS: "archived_channels", +}; + export function get_sub_for_settings(sub: StreamSubscription): SettingsSubscription { return { ...sub, @@ -66,7 +72,7 @@ function get_subs_for_settings(subs: StreamSubscription[]): SettingsSubscription // delegating, so that we can more efficiently compute subscriber counts // (in bulk). If that plan appears to have been aborted, feel free to // inline this. - return subs.filter((sub) => !sub.is_archived).map((sub) => get_sub_for_settings(sub)); + return subs.map((sub) => get_sub_for_settings(sub)); } export function get_updated_unsorted_subs(): SettingsSubscription[] { diff --git a/web/src/stream_settings_ui.ts b/web/src/stream_settings_ui.ts index 3ca7f27bf6..73560ea93c 100644 --- a/web/src/stream_settings_ui.ts +++ b/web/src/stream_settings_ui.ts @@ -1,6 +1,7 @@ import $ from "jquery"; import _ from "lodash"; import assert from "minimalistic-assert"; +import type * as tippy from "tippy.js"; import render_stream_creation_confirmation_banner from "../templates/modal_banner/stream_creation_confirmation_banner.hbs"; import render_stream_info_banner from "../templates/modal_banner/stream_info_banner.hbs"; @@ -15,6 +16,7 @@ import type {Toggle} from "./components.ts"; import * as compose_banner from "./compose_banner.ts"; import * as compose_recipient from "./compose_recipient.ts"; import * as compose_state from "./compose_state.ts"; +import * as dropdown_widget from "./dropdown_widget.ts"; import * as hash_parser from "./hash_parser.ts"; import * as hash_util from "./hash_util.ts"; import {$t} from "./i18n.ts"; @@ -46,6 +48,10 @@ import * as stream_ui_updates from "./stream_ui_updates.ts"; import * as sub_store from "./sub_store.ts"; import type {StreamSubscription} from "./sub_store.ts"; import * as util from "./util.ts"; +import * as views_util from "./views_util.ts"; + +let archived_status_dropdown_filter: string; +let filters_dropdown_widget: dropdown_widget.DropdownWidget; export function is_sub_already_present(sub: StreamSubscription): boolean { return stream_ui_updates.row_for_stream_id(sub.stream_id).length > 0; @@ -384,6 +390,20 @@ export function update_settings_for_unsubscribed(slim_sub: StreamSubscription): } function triage_stream(left_panel_params: LeftPanelParams, sub: StreamSubscription): string { + const current_channel_visibility_filter = archived_status_dropdown_filter; + const channel_visibility_filters = stream_settings_data.FILTERS; + if ( + current_channel_visibility_filter === channel_visibility_filters.NON_ARCHIVED_CHANNELS && + sub.is_archived + ) { + return "rejected"; + } + if ( + current_channel_visibility_filter === channel_visibility_filters.ARCHIVED_CHANNELS && + !sub.is_archived + ) { + return "rejected"; + } if (left_panel_params.show_subscribed && !sub.subscribed) { // reject non-subscribed streams return "rejected"; @@ -485,12 +505,15 @@ export function update_empty_left_panel_message(): void { has_streams = stream_data.get_unsorted_subs().length; } - const has_hidden_streams = + const all_channels_hidden = $("#channels_overlay_container .stream-row:not(.notdisplayed)").length === 0; const has_search_query = $("#stream_filter input[type='text']").val()!.trim() !== ""; - // Show "no channels match" text if all channels are hidden and there's a search query. - if (has_hidden_streams && has_search_query) { + const has_filter = + archived_status_dropdown_filter !== stream_settings_data.FILTERS.ALL_CHANNELS; + + // Both search queries and filters can lead to all channels being hidden. + if (all_channels_hidden && (has_search_query || (has_filter && has_streams))) { $(".no-streams-to-show").children().hide(); $(".no_stream_match_filter_empty_text").show(); $(".no-streams-to-show").show(); @@ -639,6 +662,66 @@ export function switch_stream_sort(tab_name: string): void { redraw_left_panel(); } +function filters_dropdown_options(current_value: string | number | undefined): { + unique_id: string; + name: string; + bold_current_selection: boolean; +}[] { + return [ + { + unique_id: stream_settings_data.FILTERS.ARCHIVED_CHANNELS, + name: $t({defaultMessage: "Archived channels"}), + bold_current_selection: + current_value === stream_settings_data.FILTERS.ARCHIVED_CHANNELS, + }, + { + unique_id: stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS, + name: $t({defaultMessage: "Non-archived channels"}), + bold_current_selection: + current_value === stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS, + }, + { + unique_id: stream_settings_data.FILTERS.ALL_CHANNELS, + name: $t({defaultMessage: "Archived and non-archived"}), + bold_current_selection: current_value === stream_settings_data.FILTERS.ALL_CHANNELS, + }, + ]; +} + +export function set_filters_for_tests(filter_widget: dropdown_widget.DropdownWidget): void { + filters_dropdown_widget = filter_widget; +} + +function filter_click_handler( + event: JQuery.TriggeredEvent, + dropdown: tippy.Instance, + widget: dropdown_widget.DropdownWidget, +): void { + event.preventDefault(); + event.stopPropagation(); + + const filter_id = $(event.currentTarget).attr("data-unique-id"); + assert(filter_id !== undefined); + // We don't support multiple filters, so we clear existing and add the new filter. + archived_status_dropdown_filter = filter_id; + redraw_left_panel(); + dropdown.hide(); + widget.render(); +} + +function set_up_dropdown_widget(): void { + archived_status_dropdown_filter = stream_settings_data.FILTERS.NON_ARCHIVED_CHANNELS; + filters_dropdown_widget = new dropdown_widget.DropdownWidget({ + ...views_util.COMMON_DROPDOWN_WIDGET_PARAMS, + get_options: filters_dropdown_options, + widget_name: "stream_settings_filter", + item_click_callback: filter_click_handler, + $events_container: $("#stream_filter"), + default_id: archived_status_dropdown_filter, + }); + filters_dropdown_widget.setup(); +} + function setup_page(callback: () => void): void { // We should strongly consider only setting up the page once, // but I am writing these comments write before a big release, @@ -723,6 +806,7 @@ function setup_page(callback: () => void): void { const new_stream_announcements_stream_sub = stream_data.get_sub_by_name( new_stream_announcements_stream, ); + const realm_has_archived_channels = stream_data.get_archived_subs().length > 0; const template_data = { new_stream_announcements_stream_sub, @@ -747,6 +831,7 @@ function setup_page(callback: () => void): void { disable_message_retention_setting: !realm.zulip_plan_is_not_limited || !current_user.is_owner, group_setting_labels: settings_config.all_group_setting_labels.stream, + realm_has_archived_channels, }; const rendered = render_stream_settings_overlay(template_data); @@ -756,6 +841,7 @@ function setup_page(callback: () => void): void { initialize_components(); redraw_left_panel(); stream_create.set_up_handlers(); + set_up_dropdown_widget(); const throttled_redraw_left_panel = _.throttle(redraw_left_panel, 50); $("#stream_filter input[type='text']").on("input", () => { @@ -876,6 +962,23 @@ export function change_state( toggler.goto(left_side_tab); } switch_to_stream_row(stream_id); + + const sub = stream_data.get_sub_by_id(stream_id); + if (sub) { + const FILTERS = stream_settings_data.FILTERS; + const should_update_filter = + (archived_status_dropdown_filter === FILTERS.NON_ARCHIVED_CHANNELS && + sub.is_archived) || + (archived_status_dropdown_filter === FILTERS.ARCHIVED_CHANNELS && !sub.is_archived); + if (should_update_filter) { + if (sub.is_archived) { + archived_status_dropdown_filter = FILTERS.ARCHIVED_CHANNELS; + } else { + archived_status_dropdown_filter = FILTERS.NON_ARCHIVED_CHANNELS; + } + filters_dropdown_widget.render(archived_status_dropdown_filter); + } + } return; } diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index fa972747d4..5b20549303 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -642,11 +642,21 @@ h4.user_group_setting_subsection_title { padding: 8px 10px; display: grid; grid-template: - "search-input clear-search more-options-button" auto / minmax(0, 1fr) + "search-input clear-search dropdown-widget" auto / minmax(0, 1fr) 30px; border-bottom: 1px solid var(--color-border-modal-bar); } +#stream_settings_filter_widget { + margin-left: 10px; + gap: 3px; + width: auto; +} + +.stream_settings_filter_container.hide_filter { + display: none; +} + #user_group_visibility_settings_widget { grid-area: more-options-button; margin-left: 10px; diff --git a/web/templates/message_view_header.hbs b/web/templates/message_view_header.hbs index ae36f64541..1bfc749bd1 100644 --- a/web/templates/message_view_header.hbs +++ b/web/templates/message_view_header.hbs @@ -1,5 +1,4 @@ {{#if stream_settings_link}} -{{#unless stream.is_archived}} {{> navbar_icon_and_title . }} @@ -13,11 +12,6 @@ {{/unless}} -{{else}} - - {{> navbar_icon_and_title . }} - -{{/unless}} {{#if rendered_narrow_description}} {{rendered_markdown rendered_narrow_description}} diff --git a/web/templates/stream_settings/stream_settings_overlay.hbs b/web/templates/stream_settings/stream_settings_overlay.hbs index 0058e2fab3..08c27bda73 100644 --- a/web/templates/stream_settings/stream_settings_overlay.hbs +++ b/web/templates/stream_settings/stream_settings_overlay.hbs @@ -25,6 +25,9 @@ +
+ {{> ../dropdown_widget widget_name="stream_settings_filter"}} +
diff --git a/web/tests/stream_data.test.cjs b/web/tests/stream_data.test.cjs index c6c0c8de84..24dce183ad 100644 --- a/web/tests/stream_data.test.cjs +++ b/web/tests/stream_data.test.cjs @@ -716,6 +716,7 @@ test("delete_sub", () => { stream_data.add_sub(canada); const num_subscribed_subs = stream_data.num_subscribed_subs(); + const archived_subs = stream_data.get_archived_subs(); assert.ok(stream_data.is_subscribed(canada.stream_id)); assert.equal(stream_data.get_sub("Canada").stream_id, canada.stream_id); @@ -728,6 +729,7 @@ test("delete_sub", () => { assert.ok(stream_data.get_sub("Canada")); assert.ok(sub_store.get(canada.stream_id)); assert.equal(stream_data.num_subscribed_subs(), num_subscribed_subs); + assert.equal(stream_data.get_archived_subs().length, archived_subs.length + 1); blueslip.expect("warn", "Failed to archive stream 99999"); stream_data.delete_sub(99999); diff --git a/web/tests/stream_settings_ui.test.cjs b/web/tests/stream_settings_ui.test.cjs index c2181183b9..4df951cba8 100644 --- a/web/tests/stream_settings_ui.test.cjs +++ b/web/tests/stream_settings_ui.test.cjs @@ -209,6 +209,11 @@ run_test("redraw_left_panel", ({override, mock_template}) => { populated_subs = data.subscriptions; }); + const filters_dropdown_widget = { + render: function render() {}, + }; + stream_settings_ui.set_filters_for_tests(filters_dropdown_widget); + stream_settings_ui.render_left_panel_superset(); const sub_stubs = []; @@ -223,6 +228,11 @@ run_test("redraw_left_panel", ({override, mock_template}) => { $.create("#channels_overlay_container .stream-row", {children: sub_stubs}); + const $no_streams_message = $(".no-streams-to-show"); + const $child_element = $(".subscribed_streams_tab_empty_text"); + $no_streams_message.children = () => $child_element; + $child_element.hide = () => []; + let ui_called = false; scroll_util.reset_scrollbar = ($elem) => { ui_called = true; @@ -360,6 +370,7 @@ run_test("redraw_left_panel", ({override, mock_template}) => { test_filter({input: "d", show_subscribed: true}, [poland]); assert.ok($(".stream-row-denmark").hasClass("active")); + $(".stream-row.active").attr("data-stream-id", 101); stream_settings_ui.switch_stream_tab("subscribed"); assert.ok(!$(".stream-row-denmark").hasClass("active")); assert.ok(!$(".right .settings").visible());