import $ from "jquery"; import assert from "minimalistic-assert"; import type * as tippy from "tippy.js"; import {z} from "zod"; import render_inline_decorated_channel_name from "../templates/inline_decorated_channel_name.hbs"; import render_inline_stream_or_topic_reference from "../templates/inline_stream_or_topic_reference.hbs"; import render_topic_already_exists_warning_banner from "../templates/modal_banner/topic_already_exists_warning_banner.hbs"; import render_move_topic_to_stream from "../templates/move_topic_to_stream.hbs"; import render_left_sidebar_stream_actions_popover from "../templates/popovers/left_sidebar/left_sidebar_stream_actions_popover.hbs"; import * as blueslip from "./blueslip.ts"; import type {Typeahead} from "./bootstrap_typeahead.ts"; import * as browser_history from "./browser_history.ts"; import * as clipboard_handler from "./clipboard_handler.ts"; import * as compose_banner from "./compose_banner.ts"; import * as composebox_typeahead from "./composebox_typeahead.ts"; import * as dialog_widget from "./dialog_widget.ts"; import * as dropdown_widget from "./dropdown_widget.ts"; import * as hash_util from "./hash_util.ts"; import {$t, $t_html} from "./i18n.ts"; import * as message_edit from "./message_edit.ts"; import * as message_lists from "./message_lists.ts"; import type {Message} from "./message_store.ts"; import * as message_util from "./message_util.ts"; import * as message_view from "./message_view.ts"; import * as narrow_state from "./narrow_state.ts"; import * as popover_menus from "./popover_menus.ts"; import {left_sidebar_tippy_options} from "./popover_menus.ts"; import {web_channel_default_view_values} from "./settings_config.ts"; import * as settings_data from "./settings_data.ts"; import {realm} from "./state_data.ts"; import * as stream_data from "./stream_data.ts"; import * as stream_settings_api from "./stream_settings_api.ts"; import * as stream_settings_components from "./stream_settings_components.ts"; import * as stream_settings_ui from "./stream_settings_ui.ts"; import * as stream_topic_history from "./stream_topic_history.ts"; import * as sub_store from "./sub_store.ts"; import * as ui_report from "./ui_report.ts"; import * as ui_util from "./ui_util.ts"; import * as unread from "./unread.ts"; import * as unread_ops from "./unread_ops.ts"; import {user_settings} from "./user_settings.ts"; import * as util from "./util.ts"; // In this module, we manage stream popovers // that pop up from the left sidebar. let stream_widget_value: number | undefined; let move_topic_to_stream_topic_typeahead: Typeahead | undefined; const last_propagate_mode_for_conversation = new Map(); export function stream_sidebar_menu_handle_keyboard(key: string): void { if (popover_menus.is_color_picker_popover_displayed()) { const $color_picker_popover_instance = popover_menus.get_color_picker_popover(); if (!$color_picker_popover_instance) { return; } popover_menus.sidebar_menu_instance_handle_keyboard($color_picker_popover_instance, key); return; } const $stream_actions_popover_instance = popover_menus.get_stream_actions_popover(); if (!$stream_actions_popover_instance) { return; } popover_menus.sidebar_menu_instance_handle_keyboard($stream_actions_popover_instance, key); } export function elem_to_stream_id($elem: JQuery): number { const stream_id = Number.parseInt($elem.attr("data-stream-id")!, 10); if (stream_id === undefined) { blueslip.error("could not find stream id"); } return stream_id; } export function hide_stream_popover(instance: tippy.Instance): void { ui_util.hide_left_sidebar_menu_icon(); instance.destroy(); popover_menus.popover_instances.stream_actions_popover = null; } function stream_popover_sub( e: JQuery.ClickEvent, ): sub_store.StreamSubscription { const $elem = $(e.currentTarget).parents("ul"); const stream_id = elem_to_stream_id($elem); const sub = sub_store.get(stream_id); if (!sub) { throw new Error(`Unknown stream ${stream_id}`); } return sub; } function build_stream_popover(opts: {elt: HTMLElement; stream_id: number}): void { const {elt, stream_id} = opts; // This will allow the user to close the popover by clicking // on the reference element if the popover is already open. if (popover_menus.get_stream_actions_popover()?.reference === elt) { return; } const stream_hash = hash_util.by_stream_url(stream_id); const show_go_to_channel_feed = user_settings.web_channel_default_view !== web_channel_default_view_values.channel_feed.code; const stream_unread = unread.unread_count_info_for_stream(stream_id); const stream_unread_count = stream_unread.unmuted_count + stream_unread.muted_count; const has_unread_messages = stream_unread_count > 0; const content = render_left_sidebar_stream_actions_popover({ stream: { ...sub_store.get(stream_id), url: browser_history.get_full_url(stream_hash), }, has_unread_messages, show_go_to_channel_feed, }); popover_menus.toggle_popover_menu(elt, { // Add a delay to separate `hideOnClick` and `onShow` so that // `onShow` is called after `hideOnClick`. // See https://github.com/atomiks/tippyjs/issues/230 for more details. delay: [100, 0], ...left_sidebar_tippy_options, onCreate(instance) { const $popover = $(instance.popper); $popover.addClass("stream-popover-root"); instance.setContent(ui_util.parse_html(content)); }, onMount(instance) { const $popper = $(instance.popper); popover_menus.popover_instances.stream_actions_popover = instance; ui_util.show_left_sidebar_menu_icon(elt); // Go to channel feed instead of first topic. $popper.on("click", ".stream-popover-go-to-channel-feed", (e) => { e.preventDefault(); e.stopPropagation(); const sub = stream_popover_sub(e); hide_stream_popover(instance); message_view.show( [ { operator: "stream", operand: sub.stream_id.toString(), }, ], {trigger: "stream-popover"}, ); }); // Stream settings $popper.on("click", ".open_stream_settings", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); // Admin can change any stream's name & description either stream is public or // private, subscribed or unsubscribed. const can_change_stream_permissions = stream_data.can_change_permissions_requiring_metadata_access(sub); let stream_edit_hash = hash_util.channels_settings_edit_url(sub, "general"); if (!can_change_stream_permissions) { stream_edit_hash = hash_util.channels_settings_edit_url(sub, "personal"); } browser_history.go_to_location(stream_edit_hash); }); // Pin/unpin $popper.on("click", ".pin_to_top", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); stream_settings_ui.toggle_pin_to_top_stream(sub); e.stopPropagation(); }); // Mark all messages in stream as read $popper.on("click", ".mark_stream_as_read", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); unread_ops.mark_stream_as_read(sub.stream_id); e.stopPropagation(); }); // Mark all messages in stream as unread $popper.on("click", ".mark_stream_as_unread", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); unread_ops.mark_stream_as_unread(sub.stream_id); e.stopPropagation(); }); // Mute/unmute $popper.on("click", ".toggle_stream_muted", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); stream_settings_api.set_stream_property(sub, { property: "is_muted", value: !sub.is_muted, }); e.stopPropagation(); }); // Unsubscribe $popper.on("click", ".popover_sub_unsub_button", (e) => { const sub = stream_popover_sub(e); hide_stream_popover(instance); stream_settings_components.sub_or_unsub(sub); e.preventDefault(); e.stopPropagation(); }); $popper.on("click", ".copy_stream_link", (e) => { assert(e.currentTarget instanceof HTMLElement); clipboard_handler.popover_copy_link_to_clipboard(instance, $(e.currentTarget)); }); }, onHidden(instance) { hide_stream_popover(instance); }, }); } async function get_message_placement_from_server( current_stream_id: number, topic_name: string, current_message_id: number, ): Promise<"first" | "intermediate" | "last"> { return new Promise((resolve) => { message_edit.is_message_oldest_or_newest( current_stream_id, topic_name, current_message_id, (is_oldest, is_newest) => { if (is_oldest) { resolve("first"); } else if (is_newest) { resolve("last"); } else { resolve("intermediate"); } }, ); }); } async function get_message_placement_in_conversation( current_stream_id: number, topic_name: string, current_message_id: number, ): Promise<"first" | "intermediate" | "last"> { assert(message_lists.current !== undefined); // First we check if the placement of the message can be determined // in the current message list. This allows us to avoid a server call // in most cases. if (message_lists.current.data.filter.supports_collapsing_recipients()) { // Next we check if we are in a conversation view. If we are // in a conversation view, we check if the message is the // first or the last message in the current view. If not, we // can conclude that the message is an intermediate message. // // It's safe to assume message_lists.current.data is non-empty, because // current_message_id must be present in it. if (message_lists.current.data.filter.is_conversation_view()) { if ( message_lists.current.data.fetch_status.has_found_oldest() && message_lists.current.data.first()?.id === current_message_id ) { return "first"; } else if ( message_lists.current.data.fetch_status.has_found_newest() && message_lists.current.data.last()?.id === current_message_id ) { return "last"; } return "intermediate"; } // If we are not in a conversation view, but still know // the view contains the entire conversation, we check if // we can find the adjacent messages in the current view // through which we can determine if the message is an // intermediate message or not. const msg_list = message_lists.current.data.all_messages(); let found_newer_matching_message = false; let found_older_matching_message = false; const current_dict = { stream_id: current_stream_id, topic: topic_name, }; for (let i = msg_list.length - 1; i >= 0; i -= 1) { const message = msg_list[i]; if (message?.type === "stream" && util.same_stream_and_topic(current_dict, message)) { if (message.id > current_message_id) { found_newer_matching_message = true; } else if (message.id < current_message_id) { found_older_matching_message = true; } if (found_newer_matching_message && found_older_matching_message) { return "intermediate"; } } } if ( message_lists.current.data.fetch_status.has_found_newest() && !found_newer_matching_message ) { return "last"; } if ( message_lists.current.data.fetch_status.has_found_oldest() && !found_older_matching_message ) { return "first"; } } // In case we are unable to determine the placement of the message // in the current message list, we make a server call to determine // the placement. return await get_message_placement_from_server( current_stream_id, topic_name, current_message_id, ); } export async function build_move_topic_to_stream_popover( current_stream_id: number, topic_name: string, only_topic_edit: boolean, message?: Message, ): Promise { const current_stream_name = sub_store.get(current_stream_id)!.name; const stream = sub_store.get(current_stream_id); const topic_display_name = util.get_final_topic_display_name(topic_name); const empty_string_topic_display_name = util.get_final_topic_display_name(""); const is_empty_string_topic = topic_name === ""; const args: { topic_name: string; empty_string_topic_display_name: string; current_stream_id: number; notify_new_thread: boolean; notify_old_thread: boolean; from_message_actions_popover: boolean; only_topic_edit: boolean; disable_topic_input?: boolean; message_placement?: "first" | "intermediate" | "last"; stream: sub_store.StreamSubscription | undefined; max_topic_length: number; } = { topic_name, empty_string_topic_display_name, current_stream_id, stream, notify_new_thread: message_edit.notify_new_thread_default, notify_old_thread: message_edit.notify_old_thread_default, from_message_actions_popover: message !== undefined, only_topic_edit, max_topic_length: realm.max_topic_length, }; // When the modal is opened for moving the whole topic from left sidebar, // we do not have any message object and so we disable the stream input // based on the can_move_messages_between_channels_group setting and topic // input based on can_move_messages_between_topics_group. In other cases, message object is // available and thus we check the time-based permissions as well in the // below if block to enable or disable the stream and topic input. let disable_stream_input = !settings_data.user_can_move_messages_between_streams(); args.disable_topic_input = !settings_data.user_can_move_messages_to_another_topic(); let modal_heading; if (only_topic_edit) { modal_heading = $t_html( {defaultMessage: "Rename "}, { "z-stream-or-topic": () => render_inline_stream_or_topic_reference({ topic_display_name, is_empty_string_topic, stream, show_colored_icon: true, }), }, ); } else { modal_heading = $t_html( {defaultMessage: "Move "}, { "z-stream-or-topic": () => render_inline_stream_or_topic_reference({ topic_display_name, is_empty_string_topic, stream, show_colored_icon: true, }), }, ); } if (message !== undefined) { modal_heading = $t_html( {defaultMessage: "Move messages from "}, { "z-stream-or-topic": () => render_inline_stream_or_topic_reference({ stream, topic_display_name, is_empty_string_topic, show_colored_icon: true, }), }, ); // We disable topic input only for modal is opened from the message actions // popover and not when moving the whole topic from left sidebar. This is // because topic editing permission depend on message and we do not have // any message object when opening the modal and the first message of // topic is fetched from the server after clicking submit. // Though, this will be changed soon as we are going to make topic // edit permission independent of message. // We potentially got to this function by clicking a button that implied the // user would be able to move their message. Give a little bit of buffer in // case the button has been around for a bit, e.g. we show the // move_message_button (hovering plus icon) as long as the user would have // been able to click it at the time the mouse entered the message_row. Also // a buffer in case their computer is slow, or stalled for a second, etc // If you change this number also change edit_limit_buffer in // zerver.actions.message_edit.check_update_message const move_limit_buffer = 5; args.disable_topic_input = !message_edit.is_topic_editable(message, move_limit_buffer); disable_stream_input = !message_edit.is_stream_editable(message, move_limit_buffer); // If message is in a search view, default to "move only this message" option, // same as if it were the last message in any view. if (narrow_state.is_search_view()) { args.message_placement = "last"; } // Else, default option is based on the message placement in the view. else { args.message_placement = await get_message_placement_in_conversation( current_stream_id, topic_name, message.id, ); } } const params_schema = z.object({ current_stream_id: z.string(), new_topic_name: z.string().optional(), old_topic_name: z.string(), propagate_mode: z.enum(["change_one", "change_later", "change_all"]).optional(), send_notification_to_new_thread: z.literal("on").optional(), send_notification_to_old_thread: z.literal("on").optional(), }); function get_params_from_form(): z.output { return params_schema.parse( Object.fromEntries( $("#move_topic_form") .serializeArray() .map(({name, value}) => [name, value]), ), ); } function update_submit_button_disabled_state(select_stream_id: number): void { const params = get_params_from_form(); const current_stream_id = params.current_stream_id; const new_topic_name = params.new_topic_name?.trim(); const old_topic_name = params.old_topic_name.trim(); // Unlike most topic comparisons in Zulip, we intentionally do // a case-sensitive comparison, since adjusting the // capitalization of a topic is a valid operation. // new_topic_name can be undefined when the new topic input is // disabled in case when user does not have permission to edit // topic and thus submit button is disabled if stream is also // not changed. let is_disabled = false; if ( realm.realm_mandatory_topics && (new_topic_name === "" || new_topic_name === "(no topic)") ) { is_disabled = true; } if ( Number.parseInt(current_stream_id, 10) === select_stream_id && (new_topic_name === undefined || new_topic_name === old_topic_name) ) { is_disabled = true; } util.the($("#move_topic_modal button.dialog_submit_button")).disabled = is_disabled; } function move_topic(): void { const params = get_params_from_form(); const old_topic_name = params.old_topic_name.trim(); let select_stream_id; if (only_topic_edit) { select_stream_id = undefined; } else { select_stream_id = stream_widget_value; } let new_topic_name = params.new_topic_name; const send_notification_to_new_thread = params.send_notification_to_new_thread === "on"; const send_notification_to_old_thread = params.send_notification_to_old_thread === "on"; const current_stream_id = Number.parseInt(params.current_stream_id, 10); if (new_topic_name !== undefined) { // new_topic_name can be undefined when the new topic input is disabled when // user does not have permission to edit topic. new_topic_name = new_topic_name.trim(); } if (old_topic_name === new_topic_name) { // We use `undefined` to tell the server that // there has been no change in the topic name. new_topic_name = undefined; } if (select_stream_id === current_stream_id) { // We use `undefined` to tell the server that // there has been no change in stream. This is // important for cases when changing stream is // not allowed or when changes other than // stream-change has been made. select_stream_id = undefined; } let propagate_mode = "change_all"; if (message !== undefined) { // We already have the message_id here which means that modal is opened using // message popover. assert(params.propagate_mode !== undefined); propagate_mode = params.propagate_mode; const toast_params = propagate_mode === "change_one" ? { new_stream_id: select_stream_id ?? current_stream_id, new_topic_name: new_topic_name ?? old_topic_name, } : undefined; message_edit.move_topic_containing_message_to_stream( message.id, select_stream_id, new_topic_name, send_notification_to_new_thread, send_notification_to_old_thread, propagate_mode, toast_params, ); return; } dialog_widget.show_dialog_spinner(); message_edit.with_first_message_id( current_stream_id, old_topic_name, (message_id) => { if (message_id === undefined) { // There are no messages in the given topic, so we show an error banner // and return, preventing any attempts to move a non-existent topic. dialog_widget.hide_dialog_spinner(); ui_report.client_error( $t_html({defaultMessage: "There are no messages to move."}), $("#move_topic_modal #dialog_error"), ); return; } message_edit.move_topic_containing_message_to_stream( message_id, select_stream_id, new_topic_name, send_notification_to_new_thread, send_notification_to_old_thread, propagate_mode, ); }, (xhr) => { dialog_widget.hide_dialog_spinner(); ui_report.error( $t_html({defaultMessage: "Error moving topic"}), xhr, $("#move_topic_modal #dialog_error"), ); }, ); } function set_stream_topic_typeahead(): void { const $topic_input = $("#move_topic_form input.move_messages_edit_topic"); assert(stream_widget_value !== undefined); const new_stream_name = sub_store.get(stream_widget_value)!.name; move_topic_to_stream_topic_typeahead?.unlisten(); move_topic_to_stream_topic_typeahead = composebox_typeahead.initialize_topic_edit_typeahead( $topic_input, new_stream_name, false, ); } function show_topic_already_exists_warning(): boolean { // Don't show warning if the submit button is disabled. if ($("#move_topic_modal .dialog_submit_button").expectOne().prop("disabled")) { return false; } // Don't show warning if we are only moving one message. if ($("#move_topic_modal select.message_edit_topic_propagate").val() === "change_one") { return false; } let {new_topic_name} = get_params_from_form(); if (!settings_data.user_can_move_messages_to_another_topic()) { // new_topic_name is undefined since the new topic input is disabled when // user does not have permission to edit topic. new_topic_name = args.topic_name; } assert(new_topic_name !== undefined); // Don't show warning for empty topic as the user is probably // about to type a new topic name. Note that if topics are // mandatory, then the submit button is disabled, which returns // early above. if (new_topic_name === "" || new_topic_name === "(no topic)") { return false; } let stream_id: number; if (stream_widget_value === undefined) { // Set stream_id to current_stream_id since the user is not // allowed to edit the stream in topic-edit only UI. stream_id = current_stream_id; } else { stream_id = stream_widget_value; } const stream_topics = stream_topic_history .get_recent_topic_names(stream_id) .map((topic) => topic.toLowerCase()); if (stream_topics.includes(new_topic_name.trim().toLowerCase())) { return true; } return false; } function maybe_show_topic_already_exists_warning(): void { const $move_topic_warning_container = $("#move_topic_modal .move_topic_warning_container"); if (show_topic_already_exists_warning()) { $move_topic_warning_container.html( render_topic_already_exists_warning_banner({ banner_type: compose_banner.WARNING, hide_close_button: true, classname: "topic_already_exists_warning", }), ); $move_topic_warning_container.show(); } else { $move_topic_warning_container.hide(); } } function render_selected_stream(): void { assert(stream_widget_value !== undefined); const stream = stream_data.get_sub_by_id(stream_widget_value); if (stream === undefined) { $("#move_topic_to_stream_widget .dropdown_widget_value").text( $t({defaultMessage: "Select a channel"}), ); } else { $("#move_topic_to_stream_widget .dropdown_widget_value").html( render_inline_decorated_channel_name({stream, show_colored_icon: true}), ); } } function move_topic_on_update(event: JQuery.ClickEvent, dropdown: {hide: () => void}): void { stream_widget_value = Number.parseInt($(event.currentTarget).attr("data-unique-id")!, 10); update_submit_button_disabled_state(stream_widget_value); set_stream_topic_typeahead(); render_selected_stream(); maybe_show_topic_already_exists_warning(); dropdown.hide(); event.preventDefault(); event.stopPropagation(); // Move focus to the topic input after a new stream is selected. $("#move_topic_form .move_messages_edit_topic").trigger("focus"); } // The following logic is correct only when // both message_lists.current.data.fetch_status.has_found_newest // and message_lists.current.data.fetch_status.has_found_oldest are true; // otherwise, we cannot be certain of the correct count. function get_count_of_messages_to_be_moved( selected_option: string, message_id?: number, ): number { if (selected_option === "change_one") { return 1; } if (selected_option === "change_later" && message_id !== undefined) { return message_util.get_count_of_messages_in_topic_sent_after_current_message( current_stream_id, topic_name, message_id, ); } return message_util.get_messages_in_topic(current_stream_id, topic_name).length; } function update_move_messages_count_text(selected_option: string, message_id?: number): void { const message_move_count = get_count_of_messages_to_be_moved(selected_option, message_id); const is_topic_narrowed = narrow_state.narrowed_by_topic_reply(); const is_stream_narrowed = narrow_state.narrowed_by_stream_reply(); const is_same_stream = narrow_state.stream_id() === current_stream_id; const is_same_topic = narrow_state.topic() === topic_name; const can_have_exact_count_in_narrow = (is_stream_narrowed && is_same_stream) || (is_topic_narrowed && is_same_stream && is_same_topic); let exact_message_count = false; if (selected_option === "change_one") { exact_message_count = true; } else if (can_have_exact_count_in_narrow) { const has_found_newest = message_lists.current?.data.fetch_status.has_found_newest(); const has_found_oldest = message_lists.current?.data.fetch_status.has_found_oldest(); if (selected_option === "change_later" && has_found_newest) { exact_message_count = true; } if (selected_option === "change_all" && has_found_newest && has_found_oldest) { exact_message_count = true; } } let message_text; if (exact_message_count) { message_text = $t( { defaultMessage: "{count, plural, one {# message} other {# messages}} will be moved.", }, {count: message_move_count}, ); } else { message_text = $t( { defaultMessage: "At least {count, plural, one {# message} other {# messages}} will be moved.", }, {count: message_move_count}, ); } $("#move_messages_count").text(message_text); } function update_topic_input_placeholder_visibility(topic_input_value: string): void { if (!realm.realm_mandatory_topics) { const $topic_not_mandatory_placeholder = $(".move-topic-new-topic-placeholder"); $topic_not_mandatory_placeholder.toggleClass( "move-topic-new-topic-placeholder-visible", topic_input_value === "", ); } } function setup_resize_observer($topic_input: JQuery): void { // Update position of topic typeahead because showing/hiding the // "topic already exists" warning changes the size of the modal. const update_topic_typeahead_position = new ResizeObserver((_entries) => { requestAnimationFrame(() => { $topic_input.trigger(new $.Event("typeahead.refreshPosition")); }); }); const move_topic_form = document.querySelector("#move_topic_form"); if (move_topic_form) { update_topic_typeahead_position.observe(move_topic_form); } } function update_clear_move_topic_button_state(): void { const $clear_topic_name_button = $("#clear_move_topic_new_topic_name"); const topic_input_value = $("input#move-topic-new-topic-name").val(); if (topic_input_value === "") { $clear_topic_name_button.css("visibility", "hidden"); } else { $clear_topic_name_button.css("visibility", "visible"); } } function move_topic_post_render(): void { $("#move_topic_modal .dialog_submit_button").prop("disabled", true); $("#move_topic_modal .move_topic_warning_container").hide(); const $topic_input = $("#move_topic_form input.move_messages_edit_topic"); move_topic_to_stream_topic_typeahead = composebox_typeahead.initialize_topic_edit_typeahead( $topic_input, current_stream_name, false, ); if (!realm.realm_mandatory_topics) { const $topic_not_mandatory_placeholder = $(".move-topic-new-topic-placeholder"); if (topic_name === "") { $topic_not_mandatory_placeholder.addClass( "move-topic-new-topic-placeholder-visible", ); } $topic_input.on("focus", () => { if ($topic_input.val() === "") { $topic_input.attr("placeholder", ""); $topic_input.removeClass("empty-topic-display"); $topic_not_mandatory_placeholder.addClass( "move-topic-new-topic-placeholder-visible", ); $("#clear_move_topic_new_topic_name").css("visibility", "hidden"); } $topic_input.one("blur", () => { if ($topic_input.val() === "") { $topic_not_mandatory_placeholder.removeClass( "move-topic-new-topic-placeholder-visible", ); $topic_input.attr("placeholder", empty_string_topic_display_name); $topic_input.addClass("empty-topic-display"); $("#clear_move_topic_new_topic_name").css("visibility", "visible"); } }); }); } setup_resize_observer($topic_input); update_clear_move_topic_button_state(); $("#clear_move_topic_new_topic_name").on("click", (e) => { e.stopPropagation(); const $topic_input = $("#move-topic-new-topic-name").expectOne(); $topic_input.val(""); $topic_input.trigger("input").trigger("focus"); move_topic_to_stream_topic_typeahead?.hide(); }); if (only_topic_edit) { // Set select_stream_id to current_stream_id since we user is not allowed // to edit stream in topic-edit only UI. const select_stream_id = current_stream_id; $topic_input.on("input", () => { update_submit_button_disabled_state(select_stream_id); maybe_show_topic_already_exists_warning(); const topic_input_value = $topic_input.val(); assert(topic_input_value !== undefined); update_topic_input_placeholder_visibility(topic_input_value); update_clear_move_topic_button_state(); }); return; } stream_widget_value = current_stream_id; const streams_list_options = (): dropdown_widget.Option[] => stream_data.get_options_for_dropdown_widget().filter(({stream}) => { if (stream.stream_id === current_stream_id) { return true; } return stream_data.can_post_messages_in_stream(stream); }); new dropdown_widget.DropdownWidget({ widget_name: "move_topic_to_stream", get_options: streams_list_options, item_click_callback: move_topic_on_update, $events_container: $("#move_topic_modal"), tippy_props: { // Show dropdown search input below stream selection button. offset: [0, 2], }, }).setup(); render_selected_stream(); $("#move_topic_to_stream_widget").prop("disabled", disable_stream_input); $topic_input.on("input", () => { assert(stream_widget_value !== undefined); update_submit_button_disabled_state(stream_widget_value); maybe_show_topic_already_exists_warning(); const topic_input_value = $topic_input.val(); assert(topic_input_value !== undefined); update_topic_input_placeholder_visibility(topic_input_value); update_clear_move_topic_button_state(); }); if (!args.from_message_actions_popover) { update_move_messages_count_text("change_all"); } else { // Generate unique key for this conversation const conversation_key = `${current_stream_id}_${topic_name}`; let selected_option = String($("#message_move_select_options").val()); // If a user has changed the smart defaults of `propagate_mode` to "change_one", we // remember that forced change and apply the same default for `propagate_mode` next // time when the user tries to move the message of the same topic to save the time // of user manually selecting "change_one" every time. const previously_used_propagate_mode = last_propagate_mode_for_conversation.get(conversation_key); if (previously_used_propagate_mode === "change_one") { selected_option = "change_one"; $("#message_move_select_options").val(selected_option); } update_move_messages_count_text(selected_option, message?.id); $("#message_move_select_options").on("change", function () { selected_option = String($(this).val()); last_propagate_mode_for_conversation.set(conversation_key, selected_option); maybe_show_topic_already_exists_warning(); update_move_messages_count_text(selected_option, message?.id); }); } } function focus_on_move_modal_render(): void { if (!args.disable_topic_input) { ui_util.place_caret_at_end(util.the($(".move_messages_edit_topic"))); } } dialog_widget.launch({ html_heading: modal_heading, html_body: render_move_topic_to_stream(args), html_submit_button: $t_html({defaultMessage: "Confirm"}), id: "move_topic_modal", form_id: "move_topic_form", on_click: move_topic, loading_spinner: true, on_shown: focus_on_move_modal_render, on_hidden() { move_topic_to_stream_topic_typeahead = undefined; }, post_render: move_topic_post_render, }); } export function initialize(): void { $("#stream_filters").on("click", ".stream-sidebar-menu-icon", function (this: HTMLElement, e) { const $stream_li = $(this).parents("li"); const stream_id = elem_to_stream_id($stream_li); build_stream_popover({ elt: this, stream_id, }); e.stopPropagation(); }); $("body").on("click", ".inbox-stream-menu", function (this: HTMLElement, e) { const stream_id = Number.parseInt($(this).attr("data-stream-id")!, 10); build_stream_popover({ elt: this, stream_id, }); e.stopPropagation(); }); }