/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/stream_create.ts
709 строк
27 KB
Anders Kaseorg
web: Use named import for jQuery.
29 июл 2026, 02:35
29 июл 2026, 02:35
52a10e8
Код
Авторство
О чём код?
import {$} from "jquery"; import assert from "minimalistic-assert"; import * as z from "zod/mini"; import render_subscription_invites_warning_modal from "../templates/confirm_dialog/confirm_subscription_invites_warning.hbs"; import render_channel_name_conflict_error from "../templates/stream_settings/channel_name_conflict_error.hbs"; import * as channel from "./channel.ts"; import * as channel_folders_ui from "./channel_folders_ui.ts"; import * as confirm_dialog from "./confirm_dialog.ts"; import type {DropdownWidget} from "./dropdown_widget.ts"; import {$t, $t_html} from "./i18n.ts"; import * as keydown_util from "./keydown_util.ts"; import * as loading from "./loading.ts"; import * as onboarding_steps from "./onboarding_steps.ts"; import * as settings_components from "./settings_components.ts"; import * as settings_config from "./settings_config.ts"; import * as settings_data from "./settings_data.ts"; import {current_user, realm} from "./state_data.ts"; import * as stream_create_subscribers from "./stream_create_subscribers.ts"; import * as stream_data from "./stream_data.ts"; import * as stream_edit from "./stream_edit.ts"; import * as stream_settings_components from "./stream_settings_components.ts"; import * as stream_settings_data from "./stream_settings_data.ts"; import {stream_permission_group_settings_schema} from "./stream_types.ts"; import * as stream_ui_updates from "./stream_ui_updates.ts"; import type {GroupSettingPillContainer} from "./typeahead_helper.ts"; import type {HTMLSelectOneElement} from "./types.ts"; import * as ui_report from "./ui_report.ts"; import * as util from "./util.ts"; let created_stream: string | undefined; // Default is true since the current user is added to // the subscribers list initially. let current_user_subscribed_to_created_stream = true; let folder_widget: DropdownWidget | undefined; export function reset_created_stream(): void { created_stream = undefined; } export function set_name(stream: string): void { created_stream = stream; } export function get_name(): string | undefined { return created_stream; } export function reset_current_user_subscribed_to_created_stream(): void { current_user_subscribed_to_created_stream = true; } export function set_current_user_subscribed_to_created_stream(is_subscribed: boolean): void { current_user_subscribed_to_created_stream = is_subscribed; } export function get_current_user_subscribed_to_created_stream(): boolean { return current_user_subscribed_to_created_stream; } export function set_first_stream_created_modal_shown(): void { onboarding_steps.post_onboarding_step_as_read("first_stream_created_banner"); } export function should_show_first_stream_created_modal(): boolean { return onboarding_steps.ONE_TIME_NOTICES_TO_DISPLAY.has("first_stream_created_banner"); } export function maybe_update_error_message(): void { const stream_name = $<HTMLInputElement>("input#create_stream_name").val()!.trim(); stream_name_error.pre_validate(stream_name); } class StreamSubscriptionError { report_no_subs_to_stream(): void { $("#stream_subscription_error").text( $t({defaultMessage: "You cannot create a channel with no subscribers."}), ); $("#stream_subscription_error").show(); } clear_errors(): void { $("#stream_subscription_error").hide(); } } const stream_subscription_error = new StreamSubscriptionError(); class StreamNameError { report_already_exists(rendered_error: string): void { $("#stream_name_error").html(rendered_error); $("#stream_name_error").show(); } clear_errors(): void { $("#stream_name_error").hide(); } report_empty_stream(): void { $("#stream_name_error").text($t({defaultMessage: "Choose a name for the new channel."})); $("#stream_name_error").show(); } select(): void { $("#create_stream_name").trigger("focus").trigger("select"); } pre_validate(stream_name: string): void { // Don't worry about empty strings...we just want to call this // to warn users early before they start doing too much work // after they make the effort to type in a stream name. (The // use case here is that I go to create a stream, only to find // out it already exists, and I was just too lazy to look at // the public streams that I'm not subscribed to yet. Once I // realize the stream already exists, I may want to cancel.) const stream = stream_data.get_sub(stream_name); if (stream_name && stream) { const can_rename = stream.is_archived && stream_settings_data.get_sub_for_settings(stream).can_change_name_description; const rendered_error = render_channel_name_conflict_error({ stream_id: stream.stream_id, is_archived: stream.is_archived, show_rename: can_rename, can_view_channel: true, }); this.report_already_exists(rendered_error); return; } this.clear_errors(); } validate_for_submit(stream_name: string): boolean { if (!stream_name) { this.report_empty_stream(); this.select(); return false; } const stream = stream_data.get_sub(stream_name); if (stream) { const can_rename = stream.is_archived && stream_settings_data.get_sub_for_settings(stream).can_change_name_description; const rendered_error = render_channel_name_conflict_error({ stream_id: stream.stream_id, is_archived: stream.is_archived, show_rename: can_rename, can_view_channel: true, }); this.report_already_exists(rendered_error); this.select(); return false; } // If we got this far, then we think we have a new unique stream // name, so we'll submit to the server. (It's still plausible, // however, that there's some invite-only stream that we don't // know about locally that will cause a name collision.) return true; } } const stream_name_error = new StreamNameError(); function toggle_advanced_configurations(): void { const $advanced_configurations_view = $(".advanced-configurations-collapase-view"); const $toggle_button = $(".toggle-advanced-configurations-icon"); if (!$advanced_configurations_view.hasClass("hide")) { // Toggle into the condensed state $advanced_configurations_view.addClass("hide"); $toggle_button.addClass("fa-caret-right"); $toggle_button.removeClass("fa-caret-down"); } else { // Toggle into the expanded state $advanced_configurations_view.removeClass("hide"); $toggle_button.addClass("fa-caret-down"); $toggle_button.removeClass("fa-caret-right"); } } $("body").on("click", ".settings-sticky-footer #stream_creation_go_to_subscribers", (e) => { e.preventDefault(); e.stopPropagation(); const stream_name = $<HTMLInputElement>("input#create_stream_name").val()!.trim(); const is_stream_name_valid = stream_name_error.validate_for_submit(stream_name); const privacy_type = stream_settings_components.channel_creation_privacy_widget.value(); let invite_only = false; let is_web_public = false; let is_any_stream_group_widget_pending = false; const permission_settings = Object.keys(realm.server_supported_permission_settings.stream); for (const setting_name of permission_settings) { const widget = stream_settings_components.get_group_setting_widget_for_new_stream(setting_name); assert(widget !== undefined); assert(widget !== null); if (widget.is_pending()) { is_any_stream_group_widget_pending = true; // We are not appending any value here, but instead this is // a proxy to invoke the error state for a group widget // that would usually get triggered on pressing enter. widget.appendValue(widget.getCurrentText()!); break; } } if (is_stream_name_valid && !is_any_stream_group_widget_pending) { if (privacy_type === "invite-only") { invite_only = true; } else if (privacy_type === "web-public") { is_web_public = true; } const sub = { name: stream_name, invite_only, is_web_public, }; stream_settings_components.show_subs_pane.create_stream("subscribers_container", sub); } }); $("body").on( "click", ".settings-sticky-footer #stream_creation_go_to_configure_channel_settings", (e) => { e.preventDefault(); e.stopPropagation(); stream_settings_components.show_subs_pane.create_stream("configure_channel_settings"); }, ); $("body").on("click", ".advanced-configurations-container .advance-config-toggle-area", (e) => { e.stopPropagation(); toggle_advanced_configurations(); }); // Stores the previous state of the stream creation checkbox. let stream_announce_previous_value: boolean; // Within the new stream modal... function update_announce_stream_state(): void { // If there is no new_stream_announcements_stream, we simply hide the widget. if (stream_data.get_new_stream_announcements_stream() === "") { $("#announce-new-stream").hide(); return; } // If the stream is invite only, disable the "Announce stream" option. // Otherwise enable it. const $announce_stream_checkbox = $<HTMLInputElement>("#announce-new-stream input"); const $announce_stream_label = $("#announce-new-stream"); let disable_it = false; const privacy_type = stream_settings_components.channel_creation_privacy_widget.value(); const is_invite_only = privacy_type === "invite-only"; $announce_stream_label.removeClass("control-label-disabled"); // Here, we arrange to save the state of the announce checkbox // when switching to creating a private stream; we will restore it // when switching back to a public stream. This input-disabled // check prevents overwriting stream_announce_previous_value with // the false when switching between private stream types. if (!$announce_stream_checkbox.prop("disabled")) { stream_announce_previous_value = util.the($announce_stream_checkbox).checked; } if (is_invite_only) { disable_it = true; $announce_stream_checkbox.prop("checked", false); $announce_stream_label.addClass("control-label-disabled"); } else { // If the stream was already public, this will be a noop. $announce_stream_checkbox.prop("checked", stream_announce_previous_value); } $announce_stream_checkbox.prop("disabled", disable_it); $("#announce-new-stream").show(); } function create_stream(): void { const stream_name = $<HTMLInputElement>("input#create_stream_name").val()!.trim(); const description = $<HTMLInputElement>("input#create_stream_description").val()!.trim(); created_stream = stream_name; // Even though we already check to make sure that while typing the user cannot enter // newline characters (by pressing the Enter key) it would still be possible to copy // and paste over a description with newline characters in it. Prevent that. if (description.includes("\n")) { ui_report.client_error( $t_html({defaultMessage: "The channel description cannot contain newline characters."}), $(".stream_create_info"), ); return; } const subscriptions = JSON.stringify([{name: stream_name, description}]); let invite_only; let is_web_public; const privacy_type = stream_settings_components.channel_creation_privacy_widget.value(); switch (privacy_type) { case "invite-only": { invite_only = true; is_web_public = false; break; } case "web-public": { invite_only = false; is_web_public = true; break; } default: { invite_only = false; is_web_public = false; } } const history_public_to_subscribers = util.the( $<HTMLInputElement>("input#id_new_history_public_to_subscribers"), ).checked; const default_stream = util.the( $<HTMLInputElement>("#stream_creation_form input.is_default_stream"), ).checked; let message_retention_selection = $( "#stream_creation_form select[name=stream_message_retention_setting]", ).val(); if (message_retention_selection === "custom_period") { message_retention_selection = Number.parseInt( $<HTMLInputElement>( "#stream_creation_form input[name=stream-message-retention-days]", ).val()!, 10, ); } let announce = stream_data.get_new_stream_announcements_stream() !== "" && util.the($<HTMLInputElement>("#announce-new-stream input")).checked; if ( stream_data.get_new_stream_announcements_stream() === "" && stream_data.realm_has_new_stream_announcements_stream() && !invite_only ) { announce = true; } // TODO: We can eliminate the user_ids -> principals conversion // once we upgrade the backend to accept user_ids. const user_ids = stream_create_subscribers.get_principals(); const principals = JSON.stringify(user_ids); set_current_user_subscribed_to_created_stream(user_ids.includes(current_user.user_id)); const group_setting_values: Record<string, string> = {}; for (const setting_name of Object.keys(realm.server_supported_permission_settings.stream)) { assert(group_setting_widgets[setting_name] !== undefined); group_setting_values[setting_name] = JSON.stringify( settings_components.get_group_setting_widget_value(group_setting_widgets[setting_name]), ); } loading.make_indicator($("#stream_creating_indicator"), { text: $t({defaultMessage: "Creating channel…"}), }); const topics_policy = $("#id_new_topics_policy").val(); const data: Record<string, string> = { subscriptions, is_web_public: JSON.stringify(is_web_public), invite_only: JSON.stringify(invite_only), history_public_to_subscribers: JSON.stringify(history_public_to_subscribers), is_default_stream: JSON.stringify(default_stream), message_retention_days: JSON.stringify(message_retention_selection), announce: JSON.stringify(announce), topics_policy: JSON.stringify(topics_policy), principals, ...group_setting_values, }; // Only present for admins, so guard against it being absent. const $default_push_notifications = $<HTMLInputElement>("#id_new_default_push_notifications"); if ($default_push_notifications.length > 0) { data["default_push_notifications"] = JSON.stringify( $default_push_notifications.prop("checked"), ); } assert(folder_widget !== undefined); const folder_id = folder_widget.value(); if (folder_id !== settings_config.no_folder_selected) { // We do not include "folder_id" in request data if // new stream will not be added to any folder. data["folder_id"] = JSON.stringify(folder_id); } // Subscribe yourself and possible other people to a new stream. void channel.post({ url: "/json/users/me/subscriptions", data, success(): void { $("#create_stream_name").val(""); $("#create_stream_description").val(""); loading.destroy_indicator($("#stream_creating_indicator")); // The rest of the work is done via the subscribe event we will get }, error(xhr): void { const error_message = z.object({msg: z.optional(z.string())}).parse(xhr.responseJSON); if (error_message?.msg?.includes("access")) { // If we can't access the stream, we can safely // assume it's a duplicate stream that we are not invited to. // // BUG: This check should be using error codes, not // parsing the error string, so it works correctly // with i18n. And likely we should be reporting the // error text directly rather than turning it into // "Error creating channel"? const rendered_error = render_channel_name_conflict_error({ stream_id: undefined, is_archived: false, show_rename: false, can_view_channel: false, }); stream_name_error.report_already_exists(rendered_error); stream_name_error.select(); stream_settings_components.show_subs_pane.create_stream( "configure_channel_settings", ); } else { ui_report.error( $t_html({defaultMessage: "Error creating channel"}), xhr, $(".stream_create_info"), ); } loading.destroy_indicator($("#stream_creating_indicator")); }, }); } export function new_stream_clicked(stream_name: string, folder_id: number | undefined): void { // this changes the tab switcher (settings/preview) which isn't necessary // to a add new stream title. stream_settings_components.show_subs_pane.create_stream(); $(".stream-row.active").removeClass("active"); if (stream_name !== "") { $("#create_stream_name").val(stream_name); } show_new_stream_modal(); assert(folder_widget !== undefined); if (folder_id) { folder_widget.render(folder_id); } else { folder_widget.render(-1); } $("#create_stream_name").trigger("focus"); } function clear_error_display(): void { stream_name_error.clear_errors(); $(".stream_create_info").hide(); stream_subscription_error.clear_errors(); } export function show_new_stream_modal(): void { $("#stream-creation").removeClass("hide"); $(".right .settings").hide(); stream_create_subscribers.build_widgets(); // Make the options default to the same each time // The message retention setting is visible to owners only. The below block // sets the default state of setting if it is visible. if (current_user.is_owner) { $("#stream_creation_form .stream-message-retention-days-input").hide(); $("#stream_creation_form select[name=stream_message_retention_setting]").val( "realm_default", ); // The user is not allowed to set the setting to amy value other than // "realm_default" for realms on limited plans, so we disable the setting. $("#stream_creation_form select[name=stream_message_retention_setting]").prop( "disabled", !realm.zulip_plan_is_not_limited, ); // This listener is only needed if the dropdown setting is enabled. if (realm.zulip_plan_is_not_limited) { // Add listener to .show stream-message-retention-days-input that we've hidden above $<HTMLSelectOneElement>("#stream_creation_form .stream_message_retention_setting").on( "change", (e) => { if (e.target.value === "custom_period") { $("#stream_creation_form .stream-message-retention-days-input").show(); } else { $("#stream_creation_form .stream-message-retention-days-input").hide(); } }, ); } } const $add_subscribers_container = $( "#stream_creation_form .add_subscribers_container", ).expectOne(); stream_ui_updates.enable_or_disable_add_subscribers_elements( $add_subscribers_container, // User should always be allowed to add subscribers when // creating a new channel regardless of their presence in // realm.can_add_subscribers_group true, true, ); $("#id_new_topics_policy").val(settings_config.get_stream_topics_policy_values().inherit.code); if (!stream_data.user_can_set_topics_policy()) { $("#id_new_topics_policy").prop("disabled", true); } if (!stream_data.user_can_set_delete_message_policy()) { settings_components.disable_group_permission_setting( $("#id_new_can_delete_any_message_group"), ); settings_components.disable_group_permission_setting( $("#id_new_can_delete_own_message_group"), ); } // set default state for "announce stream" and "default stream" option. $("#stream_creation_form .default-stream input").prop("checked", false); update_announce_stream_state(); stream_ui_updates.update_can_subscribe_group_label($("#stream-creation")); stream_ui_updates.update_default_stream_option_state($("#stream-creation")); stream_ui_updates.update_history_public_to_subscribers_state($("#stream-creation")); stream_ui_updates.update_can_create_topic_group_setting_state($("#stream-creation")); clear_error_display(); } let group_setting_widgets: Record<string, GroupSettingPillContainer | undefined> = {}; function set_up_group_setting_widgets(): void { for (const setting_name of Object.keys(realm.server_supported_permission_settings.stream)) { group_setting_widgets[setting_name] = settings_components.create_stream_group_setting_widget({ $pill_container: $("#id_new_" + setting_name), setting_name: stream_permission_group_settings_schema.parse(setting_name), }); stream_settings_components.set_group_setting_widget_for_new_stream( setting_name, group_setting_widgets[setting_name], ); } // Enable or disable protected history stream privacy option when // can_create_topic_group setting is updated. const can_create_topic_group_widget = group_setting_widgets["can_create_topic_group"]!; can_create_topic_group_widget.onPillCreate(() => { stream_ui_updates.update_history_public_to_subscribers_state($("#stream-creation")); }); can_create_topic_group_widget.onPillRemove(() => { stream_ui_updates.update_history_public_to_subscribers_state($("#stream-creation")); }); } export function set_up_handlers(): void { if ( !settings_data.user_can_create_public_streams() && !settings_data.user_can_create_web_public_streams() && !settings_data.user_can_create_private_streams() ) { // Return early if user does not have permission to create // streams at all as they cannot access the stream UI. return; } stream_announce_previous_value = settings_data.user_can_create_public_streams() || settings_data.user_can_create_web_public_streams(); const $subscribers_container = $(".subscribers_container").expectOne(); stream_create_subscribers.create_handlers($subscribers_container); const $container = $("#stream-creation").expectOne(); $container.on("click", ".finalize_create_stream", (e) => { e.preventDefault(); clear_error_display(); const stream_name = $<HTMLInputElement>("input#create_stream_name").val()!.trim(); const name_ok = stream_name_error.validate_for_submit(stream_name); if (!name_ok) { stream_settings_components.show_subs_pane.create_stream("configure_channel_settings"); return; } const principals = stream_create_subscribers.get_principals(); if (principals.length === 0) { stream_subscription_error.report_no_subs_to_stream(); return; } assert(stream_create_subscribers.pill_widget !== undefined); assert(stream_create_subscribers.pill_widget !== null); if (stream_create_subscribers.pill_widget.is_pending()) { // We are not appending any value here, but instead this is // a proxy to invoke the error state for a group widget // that would usually get triggered on pressing enter. stream_create_subscribers.pill_widget.appendValue( stream_create_subscribers.pill_widget.getCurrentText()!, ); return; } if (principals.length >= 50) { const modal_content_html = render_subscription_invites_warning_modal({ channel_name: stream_name, count: principals.length, }); confirm_dialog.launch({ modal_title_html: $t_html({defaultMessage: "Large number of subscribers"}), modal_content_html, is_compact: true, on_click() { create_stream(); }, }); } else { create_stream(); } }); $container.on("input", "#create_stream_name", () => { const stream_name = $<HTMLInputElement>("input#create_stream_name").val()!.trim(); // This is an inexpensive check. stream_name_error.pre_validate(stream_name); }); // Do not allow the user to enter newline characters while typing out the // stream's description during it's creation. $container.on("keydown", "#create_stream_description", (e) => { if (keydown_util.is_enter_event(e)) { e.preventDefault(); } }); $container.on("input", "#id_new_history_public_to_subscribers", () => { stream_ui_updates.update_can_create_topic_group_setting_state($("#stream-creation")); }); $container.on("click", ".create-channel-folder-button", (e) => { e.preventDefault(); channel_folders_ui.add_channel_folder(set_channel_folder_dropdown_value); }); set_up_group_setting_widgets(); settings_components.enable_opening_typeahead_on_clicking_label($container); folder_widget = stream_settings_components.set_up_folder_dropdown_widget(); stream_edit.set_up_channel_privacy_dropdown_widget(update_announce_stream_state); } export function initialize(): void { $("#channels_overlay_container").on( "click", "#archived_stream_rename", function (this: HTMLElement, e) { e.preventDefault(); e.stopPropagation(); const stream_id = Number.parseInt($(e.currentTarget).attr("data-stream-id")!, 10); stream_edit.open_stream_edit_modal(stream_id, $(this).attr("id")!); }, ); } export function set_channel_folder_dropdown_value(folder_id: number): void { assert(folder_widget !== undefined); folder_widget.render(folder_id); } export function maybe_reset_channel_folder_dropdown(archived_folder_id: number): void { assert(folder_widget !== undefined); if (folder_widget.value() === archived_folder_id) { set_channel_folder_dropdown_value(settings_config.no_folder_selected); } } export function get_channel_folder_dropdown_value(): number { assert(folder_widget !== undefined); const value = folder_widget.value()!; assert(typeof value === "number"); return value; }