diff --git a/web/src/unread_ops.ts b/web/src/unread_ops.ts index 570bee11fc..16fe4bfc70 100644 --- a/web/src/unread_ops.ts +++ b/web/src/unread_ops.ts @@ -4,14 +4,17 @@ import assert from "minimalistic-assert"; import {z} from "zod"; import render_confirm_mark_all_as_read from "../templates/confirm_dialog/confirm_mark_all_as_read.hbs"; +import render_inline_decorated_stream_name from "../templates/inline_decorated_stream_name.hbs"; +import render_skipped_marking_unread from "../templates/skipped_marking_unread.hbs"; import * as blueslip from "./blueslip.ts"; import * as channel from "./channel.ts"; import * as confirm_dialog from "./confirm_dialog.ts"; import * as desktop_notifications from "./desktop_notifications.ts"; import * as dialog_widget from "./dialog_widget.ts"; +import * as feedback_widget from "./feedback_widget.ts"; import {Filter} from "./filter.ts"; -import {$t_html} from "./i18n.ts"; +import {$t, $t_html} from "./i18n.ts"; import * as loading from "./loading.ts"; import * as message_flags from "./message_flags.ts"; import * as message_lists from "./message_lists.ts"; @@ -24,11 +27,14 @@ import * as people from "./people.ts"; import * as recent_view_ui from "./recent_view_ui.ts"; import type {MessageDetails} from "./server_event_types.ts"; import type {NarrowTerm} from "./state_data.ts"; +import * as sub_store from "./sub_store.ts"; import * as ui_report from "./ui_report.ts"; import * as unread from "./unread.ts"; import * as unread_ui from "./unread_ui.ts"; +import * as util from "./util.ts"; let loading_indicator_displayed = false; +let unsubscribed_ignored_channels: number[] = []; // We might want to use a slightly smaller batch for the first // request, because empirically, the first request can be @@ -70,8 +76,49 @@ const update_flags_for_narrow_response_schema = z.object({ last_processed_id: z.number().nullable(), found_oldest: z.boolean(), found_newest: z.boolean(), + ignored_because_not_subscribed_channels: z.array(z.number()), }); +const update_flags_for_response_schema = z.object({ + ignored_because_not_subscribed_channels: z.array(z.number()), +}); + +function handle_skipped_unsubscribed_streams( + ignored_because_not_subscribed_channels: number[], +): void { + if (ignored_because_not_subscribed_channels.length > 0) { + // Zulip has an invariant that all unread messages must be in streams + // the user is subscribed to. Notify the user if messages from + // unsubscribed streams are ignored by the server. + const stream_names_with_privacy_symbol_html = ignored_because_not_subscribed_channels.map( + (stream_id) => { + const stream = sub_store.get(stream_id); + const decorated_stream_name = render_inline_decorated_stream_name({stream}); + return `${decorated_stream_name}`; + }, + ); + + const populate: (element: JQuery) => void = ($container) => { + const formatted_stream_list_text = util.format_array_as_list( + stream_names_with_privacy_symbol_html, + "long", + "conjunction", + ); + const rendered_html = render_skipped_marking_unread({ + streams: formatted_stream_list_text, + }); + $container.html(rendered_html); + }; + + const title_text = $t({defaultMessage: "Skipped unsubscribed channels"}); + + feedback_widget.show({ + populate, + title_text, + }); + } +} + function bulk_update_read_flags_for_narrow( narrow: NarrowTerm[], op: "add" | "remove", @@ -330,6 +377,12 @@ function do_mark_unread_by_narrow( success(raw_data) { const data = update_flags_for_narrow_response_schema.parse(raw_data); messages_marked_unread_till_now += data.updated_count; + unsubscribed_ignored_channels = [ + ...new Set([ + ...unsubscribed_ignored_channels, + ...data.ignored_because_not_subscribed_channels, + ]), + ]; if (!data.found_newest) { assert(data.last_processed_id !== null); // If we weren't able to complete the request fully in @@ -358,8 +411,14 @@ function do_mark_unread_by_narrow( FOLLOWUP_BATCH_SIZE, narrow, ); - } else if (loading_indicator_displayed) { - finish_loading(messages_marked_unread_till_now); + } else { + if (loading_indicator_displayed) { + finish_loading(messages_marked_unread_till_now); + } + if (unsubscribed_ignored_channels.length > 0) { + handle_skipped_unsubscribed_streams(unsubscribed_ignored_channels); + unsubscribed_ignored_channels = []; + } } }, error(xhr) { @@ -382,10 +441,16 @@ function do_mark_unread_by_ids(message_ids_to_update: number[]): void { void channel.post({ url: "/json/messages/flags", data: {messages: JSON.stringify(message_ids_to_update), op: "remove", flag: "read"}, - success() { + success(raw_data) { if (loading_indicator_displayed) { finish_loading(message_ids_to_update.length); } + const data = update_flags_for_response_schema.parse(raw_data); + const ignored_because_not_subscribed_channels = + data.ignored_because_not_subscribed_channels; + if (ignored_because_not_subscribed_channels.length > 0) { + handle_skipped_unsubscribed_streams(ignored_because_not_subscribed_channels); + } }, error(xhr) { handle_mark_unread_from_here_error(xhr, { diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 3817867fe6..f64b0d4d8c 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -389,6 +389,10 @@ body.has-overlay-scrollbar { white-space: pre; } +.white-space-nowrap { + white-space: nowrap; +} + /* Set flex display on login buttons only in the spectator view. We want them to display none otherwise. */ diff --git a/web/templates/skipped_marking_unread.hbs b/web/templates/skipped_marking_unread.hbs new file mode 100644 index 0000000000..9a2863db4f --- /dev/null +++ b/web/templates/skipped_marking_unread.hbs @@ -0,0 +1,4 @@ +{{#tr}} + Because you are not subscribed to , messages in this channel were not marked as unread. + {{#*inline "z-streams"}}{{{streams}}}{{/inline}} +{{/tr}}