/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/settings_components.ts
2 137 строк
81 KB
Anders Kaseorg
eslint: Fix unicorn/prefer-hoisting-branch-code.
29 июл 2026, 02:35
29 июл 2026, 02:35
5f42399
Код
Авторство
О чём код?
import {$} from "jquery"; import _ from "lodash"; import assert from "minimalistic-assert"; import * as tippy from "tippy.js"; import * as z from "zod/mini"; import render_compose_banner from "../templates/compose_banner/compose_banner.hbs"; import * as blueslip from "./blueslip.ts"; import * as buttons from "./buttons.ts"; import * as compose_banner from "./compose_banner.ts"; import type {DropdownWidget, Option} from "./dropdown_widget.ts"; import * as group_permission_settings from "./group_permission_settings.ts"; import type { AssignedGroupPermission, GroupGroupSettingName, RealmGroupSettingNameSupportingAnonymousGroups, } from "./group_permission_settings.ts"; import * as group_setting_pill from "./group_setting_pill.ts"; import {$t, get_language_list_columns} from "./i18n.ts"; import {page_params} from "./page_params.ts"; import * as people from "./people.ts"; import { realm_default_settings_schema, realm_user_settings_defaults, } from "./realm_user_settings_defaults.ts"; import * as scroll_util from "./scroll_util.ts"; import * as settings_config from "./settings_config.ts"; import * as settings_data from "./settings_data.ts"; import type {CustomProfileField, GroupSettingValue} from "./state_data.ts"; import {current_user, realm, realm_schema} from "./state_data.ts"; import * as stream_data from "./stream_data.ts"; import * as stream_topic_history from "./stream_topic_history.ts"; import { type StreamPermissionGroupSetting, stream_permission_group_settings_schema, } from "./stream_types.ts"; import type {StreamSubscription} from "./sub_store.ts"; import {stream_subscription_schema} from "./sub_store.ts"; import type {GroupSettingPillContainer} from "./typeahead_helper.ts"; import {group_setting_value_schema} from "./types.ts"; import type {HTMLSelectOneElement} from "./types.ts"; import * as ui_util from "./ui_util.ts"; import * as user_group_pill from "./user_group_pill.ts"; import * as user_groups from "./user_groups.ts"; import type {UserGroup} from "./user_groups.ts"; import * as user_pill from "./user_pill.ts"; import * as util from "./util.ts"; const MAX_CUSTOM_TIME_LIMIT_SETTING_VALUE = 2147483647; type SettingOptionValue = { order?: number; code: number; description: string; }; export type SettingOptionValueWithKey = SettingOptionValue & {key: string}; export function get_sorted_options_list( option_values_object: Record<string, SettingOptionValue>, ): SettingOptionValueWithKey[] { const options_list: SettingOptionValueWithKey[] = Object.entries(option_values_object).map( ([key, value]) => ({...value, key}), ); let comparator: (x: SettingOptionValueWithKey, y: SettingOptionValueWithKey) => number; if (options_list[0] !== undefined && !options_list[0].order) { comparator = (x, y) => { const key_x = x.key.toUpperCase(); const key_y = y.key.toUpperCase(); if (key_x < key_y) { return -1; } if (key_x > key_y) { return 1; } return 0; }; } else { comparator = (x, y) => { assert(x.order !== undefined); assert(y.order !== undefined); return x.order - y.order; }; } options_list.sort(comparator); return options_list; } export type MessageMoveTimeLimitSetting = | "realm_move_messages_within_stream_limit_seconds" | "realm_move_messages_between_streams_limit_seconds"; export type MessageTimeLimitSetting = | MessageMoveTimeLimitSetting | "realm_message_content_edit_limit_seconds" | "realm_message_content_delete_limit_seconds"; export function get_realm_time_limits_in_minutes(property: MessageTimeLimitSetting): string { const setting_value = realm[property]; if (setting_value === null) { // This represents "Anytime" case. return ""; } let val = (setting_value / 60).toFixed(1); if (Number.parseFloat(val) === Number.parseInt(val, 10)) { val = (setting_value / 60).toFixed(0); } return val; } type RealmSetting = typeof realm; export const realm_setting_property_schema = z.union([ z.keyof(realm_schema), z.literal("realm_org_join_restrictions"), ]); type RealmSettingProperty = z.infer<typeof realm_setting_property_schema>; type RealmUserSettingDefaultType = typeof realm_user_settings_defaults; export const realm_user_settings_default_properties_schema = z.union([ z.keyof(realm_default_settings_schema), z.literal("email_notification_batching_period_edit_minutes"), ]); type RealmUserSettingDefaultProperties = z.infer< typeof realm_user_settings_default_properties_schema >; export const stream_settings_property_schema = z.union([ z.keyof(stream_subscription_schema), z.enum(["channel_privacy", "is_default_stream"]), ]); type StreamSettingProperty = z.infer<typeof stream_settings_property_schema>; type valueof<T> = T[keyof T]; export function get_realm_settings_property_value( property_name: RealmSettingProperty, ): valueof<RealmSetting> { if (property_name === "realm_org_join_restrictions") { if (realm.realm_emails_restricted_to_domains) { return "only_selected_domain"; } if (realm.realm_disallow_disposable_email_addresses) { return "no_disposable_email"; } return "no_restriction"; } if (property_name === "realm_authentication_methods") { return JSON.stringify(realm_authentication_methods_to_boolean_dict()); } return realm[property_name]; } export function get_stream_settings_property_value( property_name: StreamSettingProperty, sub: StreamSubscription, ): valueof<StreamSubscription> { if (property_name === "channel_privacy") { return stream_data.get_stream_privacy_policy(sub.stream_id); } if (property_name === "is_default_stream") { return stream_data.is_default_stream_id(sub.stream_id); } return sub[property_name]; } export function get_group_property_value( property_name: keyof UserGroup, group: UserGroup, ): valueof<UserGroup> { return group[property_name]; } export function get_custom_profile_property_value( property_name: keyof CustomProfileField, custom_profile_field: CustomProfileField, ): valueof<CustomProfileField> { const value = custom_profile_field[property_name]; if (property_name === "display_in_profile_summary" && value === undefined) { return false; } if (property_name === "use_for_user_matching" && value === undefined) { return false; } return value; } export function get_realm_default_setting_property_value( property_name: RealmUserSettingDefaultProperties, ): valueof<RealmUserSettingDefaultType> { if (property_name === "twenty_four_hour_time") { return JSON.stringify(realm_user_settings_defaults.twenty_four_hour_time); } if ( property_name === "email_notifications_batching_period_seconds" || property_name === "email_notification_batching_period_edit_minutes" ) { return realm_user_settings_defaults.email_notifications_batching_period_seconds; } return realm_user_settings_defaults[property_name]; } export function realm_authentication_methods_to_boolean_dict(): Record<string, boolean> { return Object.fromEntries( Object.entries(realm.realm_authentication_methods) .toSorted(([a], [b]) => Number(a > b) - Number(a < b)) .map(([auth_method_name, auth_method_info]) => [ auth_method_name, auth_method_info.enabled, ]), ); } export function extract_property_name($elem: JQuery, for_realm_default_settings?: boolean): string { const elem_id = $elem.attr("id"); assert(elem_id !== undefined); if (for_realm_default_settings) { // ID for realm_user_default_settings elements are of the form // "realm_{settings_name}}" because both user and realm default // settings use the same template and each element should have // unique id. return /^realm_(.*)$/.exec(elem_id.replaceAll("-", "_"))![1]!; } if (elem_id.startsWith("id_authmethod")) { // Authentication Method component IDs include authentication method name // for uniqueness, anchored to "id_authmethod" prefix, e.g. "id_authmethodapple_<property_name>". // We need to strip that whole construct down to extract the actual property name. // The [\da-z]+ part of the regexp covers the auth method name itself. // We assume it's not an empty string and can contain only digits and lowercase ASCII letters, // this is ensured by a respective allowlist-based filter in populate_auth_methods(). return /^id_authmethod[\da-z]+_(.*)$/.exec(elem_id)![1]!; } if (elem_id.startsWith("id-custom-profile-field")) { return /^id_custom_profile_field_(.*)$/.exec(elem_id.replaceAll("-", "_"))![1]!; } return /^id_(.*)$/.exec(elem_id.replaceAll("-", "_"))![1]!; } export function get_subsection_property_elements($subsection: JQuery): HTMLElement[] { return [...$subsection.find(".prop-element")]; } export const simple_dropdown_realm_settings_schema = z.pick(realm_schema, { realm_org_type: true, realm_media_preview_size: true, realm_message_edit_history_visibility_policy: true, realm_topics_policy: true, }); export type SimpleDropdownRealmSettings = z.infer<typeof simple_dropdown_realm_settings_schema>; export function set_property_dropdown_value( property_name: keyof SimpleDropdownRealmSettings, ): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_value = get_realm_settings_property_value( property_name, ) as valueof<SimpleDropdownRealmSettings>; $(`#id_${CSS.escape(property_name)}`).val(property_value); } export function change_element_block_display_property( elem_id: string, show_element: boolean, ): void { const $elem = $(`#${CSS.escape(elem_id)}`); if (show_element) { $elem.parent().show(); } else { $elem.parent().hide(); } } export function is_video_chat_provider_jitsi_meet(): boolean { const video_chat_provider_id = Number.parseInt( $<HTMLSelectOneElement>("select:not([multiple])#id_realm_video_chat_provider").val()!, 10, ); const jitsi_meet_id = realm.realm_available_video_chat_providers.jitsi_meet.id; return video_chat_provider_id === jitsi_meet_id; } function get_jitsi_server_url_setting_value( $input_elem: JQuery<HTMLSelectElement>, for_api_data = true, ): string | null { // If the video chat provider dropdown is not set to Jitsi, we return // `realm_jitsi_server_url` to indicate that the property remains unchanged. // This ensures the appropriate state of the save button and prevents the // addition of the `jitsi_server_url` in the API data. if (!is_video_chat_provider_jitsi_meet()) { return realm.realm_jitsi_server_url; } const select_elem_val = $input_elem.val(); if (select_elem_val === "server_default") { if (!for_api_data) { return null; } return JSON.stringify("default"); } const $custom_input_elem = $<HTMLInputElement>("input#id_realm_jitsi_server_url_custom_input"); if (!for_api_data) { return $custom_input_elem.val()!; } return JSON.stringify($custom_input_elem.val()); } export function update_custom_time_limit_minute_text($input: JQuery<HTMLInputElement>): void { const $minutes_text = $input.parent().find(".time-unit-text"); const count = Number.parseInt($input.val()!, 10); $minutes_text.text( $t( { defaultMessage: "{count, plural, one {minute} other {minutes}}", }, {count}, ), ); } export function update_custom_value_input(property_name: MessageTimeLimitSetting): void { const $dropdown_elem = $(`#id_${CSS.escape(property_name)}`); const custom_input_elem_id = $dropdown_elem .parent() .find(".time-limit-custom-input") .attr("id")!; const show_custom_limit_input = $dropdown_elem.val() === "custom_period"; change_element_block_display_property(custom_input_elem_id, show_custom_limit_input); if (!show_custom_limit_input) { return; } $(`#${CSS.escape(custom_input_elem_id)}`).val(get_realm_time_limits_in_minutes(property_name)); update_custom_time_limit_minute_text($(`#${CSS.escape(custom_input_elem_id)}`)); } export function get_time_limit_dropdown_setting_value( property_name: MessageTimeLimitSetting, ): string { const value = realm[property_name]; if (value === null) { return "any_time"; } const valid_limit_values = settings_config.time_limit_dropdown_values.map((x) => x.value); if (valid_limit_values.includes(value)) { return value.toString(); } return "custom_period"; } export function set_time_limit_setting(property_name: MessageTimeLimitSetting): void { const dropdown_elem_val = get_time_limit_dropdown_setting_value(property_name); $(`#id_${CSS.escape(property_name)}`).val(dropdown_elem_val); const $custom_input = $(`#id_${CSS.escape(property_name)}`) .parent() .find(".time-limit-custom-input"); $custom_input.val(get_realm_time_limits_in_minutes(property_name)); change_element_block_display_property( $custom_input.attr("id")!, dropdown_elem_val === "custom_period", ); } function get_message_retention_setting_value( $input_elem: JQuery<HTMLSelectElement>, for_api_data = true, ): string | number | null { const select_elem_val = $input_elem.val(); if (select_elem_val === "unlimited") { if (!for_api_data) { return settings_config.retain_message_forever; } return JSON.stringify("unlimited"); } if (select_elem_val === "realm_default") { if (!for_api_data) { return null; } return JSON.stringify("realm_default"); } const custom_input_val = $input_elem .parent() .find<HTMLInputElement>(".message-retention-setting-custom-input") .val()!; return util.check_time_input(custom_input_val); } export const custom_profile_field_choices_schema = z.record( z.string(), z.object({text: z.string(), order: z.string()}), ); export type SelectFieldData = z.output<typeof custom_profile_field_choices_schema>; function read_custom_profile_field_choices_from_form( $profile_field_form: JQuery, old_field_data: unknown, ): SelectFieldData { const field_data: SelectFieldData = {}; let field_order = 1; const old_option_value_map = new Map<string, string>(); if (old_field_data !== undefined) { const choices = custom_profile_field_choices_schema.parse(old_field_data); for (const [value, choice] of Object.entries(choices)) { assert(typeof choice !== "string"); old_option_value_map.set(choice.text, value); } } $profile_field_form.find("div.choice-row").each(function (this: HTMLElement) { const text = util.the($(this).find("input")).value; if (text) { let value = old_option_value_map.get(text); if (value !== undefined) { // Resetting the data-value in the form is // important if the user removed an option string // and then added it back again before saving // changes. $(this).attr("data-value", value); } else { value = $(this).attr("data-value")!; } field_data[value] = {text, order: field_order.toString()}; field_order += 1; } }); return field_data; } export const external_account_field_schema = z.object({ subtype: z.string(), url_pattern: z.optional(z.string()), }); export type ExternalAccountFieldData = z.output<typeof external_account_field_schema>; function read_external_account_field_data( $profile_field_form: JQuery, old_field_data: ExternalAccountFieldData | undefined, ): ExternalAccountFieldData { let field_data: ExternalAccountFieldData; // Use dropdown widget value in "create field form". if (old_field_data === undefined) { const widget = get_widget_for_dropdown_list_settings("external_accounts_type"); assert(widget !== null); const value = widget.value(); assert(typeof value === "string"); field_data = { subtype: value, }; } // Use existing subtype in "edit field form". else { field_data = { subtype: old_field_data.subtype, }; } if (field_data.subtype === "custom") { field_data.url_pattern = $profile_field_form .find<HTMLInputElement>("input[name=url_pattern]") .val()!; } return field_data; } export type FieldData = SelectFieldData | ExternalAccountFieldData; export function read_field_data_from_form( field_type_id: number, $profile_field_form: JQuery, old_field_data: unknown, ): FieldData | undefined { const field_types = realm.custom_profile_field_types; // Only the following field types support associated field data. if (field_type_id === field_types.DROPDOWN.id) { return read_custom_profile_field_choices_from_form($profile_field_form, old_field_data); } if (field_type_id === field_types.EXTERNAL_ACCOUNT.id) { const parsed_old_field_data = old_field_data ? external_account_field_schema.parse(old_field_data) : undefined; return read_external_account_field_data($profile_field_form, parsed_old_field_data); } return undefined; } function get_field_data_input_value($input_elem: JQuery): string | undefined { const $profile_field_form = $input_elem.closest(".profile-field-form"); const profile_field_id = Number.parseInt( $($profile_field_form).attr("data-profile-field-id")!, 10, ); const field = realm.custom_profile_fields.find((field) => field.id === profile_field_id)!; const proposed_value = read_field_data_from_form( field.type, $profile_field_form, JSON.parse(field.field_data), ); return JSON.stringify(proposed_value); } const dropdown_widget_map = new Map<string, DropdownWidget | null>([ ["realm_moderation_request_channel_id", null], ["realm_new_stream_announcements_stream_id", null], ["realm_signup_announcements_stream_id", null], ["realm_zulip_update_announcements_stream_id", null], ["realm_default_code_block_language", null], ["realm_default_language", null], ["realm_can_access_all_users_group", null], ["realm_can_create_web_public_channel_group", null], ["folder_id", null], ["channel_privacy", null], ["external_accounts_type", null], ]); export function get_widget_for_dropdown_list_settings( property_name: string, ): DropdownWidget | null { const dropdown_widget = dropdown_widget_map.get(property_name); if (dropdown_widget === undefined) { blueslip.error("No dropdown list widget for property", {property_name}); return null; } return dropdown_widget; } export function set_dropdown_setting_widget(property_name: string, widget: DropdownWidget): void { if (!dropdown_widget_map.has(property_name)) { blueslip.error("No dropdown list widget for property", {property_name}); return; } dropdown_widget_map.set(property_name, widget); } export function set_dropdown_list_widget_setting_value( property_name: string, value: number | string, ): void { const widget = get_widget_for_dropdown_list_settings(property_name); assert(widget !== null); widget.render(value); } export function get_dropdown_list_widget_setting_value($input_elem: JQuery): number | string { const widget_name = extract_property_name($input_elem); const setting_widget = get_widget_for_dropdown_list_settings(widget_name); assert(setting_widget !== null); const setting_value = setting_widget.value(); assert(setting_value !== undefined); return setting_value; } export function change_save_button_state( $element: JQuery, state: string, error_callback?: () => void, ): void { function show_hide_element( $element: JQuery, show: boolean, fadeout_delay: number, fadeout_callback: (this: HTMLElement) => void, ): void { if (show) { $element.removeClass("hide").addClass(".show").fadeIn(300); return; } setTimeout(() => { $element.fadeOut(300, fadeout_callback); }, fadeout_delay); } const $save_button = $element.find(".save-button"); const $textEl = $save_button.find(".action-button-label"); if (state === "discarded") { if ( // When the save button is in the "saving" or "saved" state, // we don't want the realm sync settings logic to hide the // save discard widget before the success callback could show the // "saved" state in the button. Moreover, the visibility of the // save discard widget will be handled by either the "succeeded" // or the "failed" state after the request is complete. $save_button.attr("data-status") === "saved" || $save_button.attr("data-status") === "saving" ) { return; } show_hide_element($element, false, 0, () => { enable_or_disable_save_button($element.closest(".settings-subsection-parent")); }); return; } if (state === "failed") { assert(error_callback !== undefined); show_hide_element($element, false, 0, () => { error_callback(); }); return; } if (state === "succeeded" && $save_button.attr("data-status") === "unsaved") { // We don't show the "saved" state if the save button is in the "unsaved" // state, as that would indicate that user has made some other changes // during the saving process. return; } if (state !== "saving") { buttons.hide_button_loading_indicator($save_button); } let button_text = $t({defaultMessage: "Save changes"}); let data_status; let is_show; switch (state) { case "unsaved": data_status = "unsaved"; is_show = true; $element.find(".discard-button").show(); break; case "saving": // We don't change the button text on the saving // state to avoid changing the button size while // we show the loading indicator. data_status = "saving"; is_show = true; $element.find(".discard-button").hide(); buttons.show_button_loading_indicator($save_button); break; case "succeeded": button_text = $t({defaultMessage: "Saved"}); data_status = "saved"; is_show = false; break; } requestAnimationFrame(() => { // We need to use requestAnimationFrame to ensure that the // button text and style are updated in the same frame. $textEl.text(button_text); if (state === "succeeded") { buttons.modify_action_button_style($save_button, { variant: "text", intent: "success", }); } else { buttons.modify_action_button_style($save_button, { variant: "solid", intent: "brand", }); } }); assert(data_status !== undefined); $save_button.attr("data-status", data_status); if (state === "unsaved") { // Do not scroll if the currently focused element is a textarea or an input // of type text, to not interrupt the user's typing flow. Scrolling will happen // anyway when the field loses focus (via the change event) if necessary. if ( !document.activeElement || !$(document.activeElement).is('textarea, input[type="text"]') ) { // Ensure the save button is visible when the state is "unsaved", // so the user does not miss saving their changes. scroll_util.scroll_element_into_container( $element.parent(".subsection-header"), $("#settings_content"), ); } enable_or_disable_save_button($element.closest(".settings-subsection-parent")); } assert(is_show !== undefined); show_hide_element($element, is_show, 800, () => { // There is no need for a callback here since we have already // called the function to enable or disable save button. }); } export function get_input_type($input_elem: JQuery, input_type?: string): string { if (input_type !== undefined && ["boolean", "string", "number"].includes(input_type)) { return input_type; } input_type = $input_elem.attr("data-setting-widget-type"); assert(input_type !== undefined); return input_type; } export let get_input_element_value = ( input_elem: HTMLElement, input_type?: string, ): boolean | number | string | null | undefined | GroupSettingValue => { const $input_elem = $(input_elem); input_type = get_input_type($input_elem, input_type); let input_value; switch (input_type) { case "boolean": assert(input_elem instanceof HTMLInputElement); return input_elem.checked; case "string": assert( input_elem instanceof HTMLInputElement || input_elem instanceof HTMLSelectElement || input_elem instanceof HTMLTextAreaElement, ); input_value = $(input_elem).val()!; assert(typeof input_value === "string"); return input_value.trim(); case "number": assert( input_elem instanceof HTMLInputElement || input_elem instanceof HTMLSelectElement, ); input_value = $(input_elem).val()!; assert(typeof input_value === "string"); return Number.parseInt(input_value.trim(), 10); case "radio-group": { const selected_val = $input_elem.find<HTMLInputElement>("input:checked").val()!; if ($input_elem.data("setting-choice-type") === "number") { return Number.parseInt(selected_val, 10); } return selected_val.trim(); } case "time-limit": assert(input_elem instanceof HTMLSelectElement); return get_time_limit_setting_value($(input_elem)); case "jitsi-server-url-setting": assert(input_elem instanceof HTMLSelectElement); return get_jitsi_server_url_setting_value($(input_elem)); case "message-retention-setting": assert(input_elem instanceof HTMLSelectElement); return get_message_retention_setting_value($(input_elem)); case "dropdown-list-widget": return get_dropdown_list_widget_setting_value($input_elem); case "field-data-setting": return get_field_data_input_value($input_elem); case "language-setting": return $input_elem.attr("data-language-code"); case "auth-methods": return JSON.stringify(get_auth_method_list_data()); case "group-setting-type": { const setting_name = extract_property_name($input_elem); const pill_widget = get_group_setting_widget(setting_name); assert(pill_widget !== null); return get_group_setting_widget_value(pill_widget); } case "info-density-setting": assert(input_elem instanceof HTMLInputElement); return Number.parseInt($(input_elem).val()!, 10); default: return undefined; } }; export function rewire_get_input_element_value(value: typeof get_input_element_value): void { get_input_element_value = value; } export function set_input_element_value( $input_elem: JQuery, value: number | string | boolean, ): void { const input_type = get_input_type($input_elem, typeof value); if (input_type) { if (input_type === "boolean") { assert(typeof value === "boolean"); $input_elem.prop("checked", value); return; } if (input_type === "string" || input_type === "number") { assert(typeof value !== "boolean"); $input_elem.val(value); return; } } blueslip.error("Failed to set value of property", { property: extract_property_name($input_elem), }); return; } export function get_auth_method_list_data(): Record<string, boolean> { const new_auth_methods: Record<string, boolean> = {}; const $auth_method_rows = $("#id_realm_authentication_methods").find("div.method_row"); for (const method_row of $auth_method_rows) { const method = $(method_row).attr("data-method"); assert(method !== undefined); new_auth_methods[method] = util.the($(method_row).find<HTMLInputElement>("input")).checked; } return new_auth_methods; } export function parse_time_limit($elem: JQuery<HTMLInputElement>): number { const time_limit_in_minutes = util.check_time_input($elem.val()!, true); return Math.floor(time_limit_in_minutes * 60); } function get_time_limit_setting_value( $input_elem: JQuery<HTMLSelectElement>, for_api_data = true, ): string | number | null { const select_elem_val = $input_elem.val()!; if (select_elem_val === "any_time") { // "unlimited" is sent to API when a user wants to set the setting to // "Any time" and the message_content_edit_limit_seconds field is "null" // for that case. if (!for_api_data) { return null; } return JSON.stringify("unlimited"); } if (select_elem_val !== "custom_period") { assert(typeof select_elem_val === "string"); return Number.parseInt(select_elem_val, 10); } const $custom_input_elem = $input_elem .parent() .find<HTMLInputElement>("input.time-limit-custom-input"); if ($custom_input_elem.val() === "") { // This handles the case where the initial setting value is "Any time" and then // dropdown is changed to "Custom" where the input box is empty initially and // thus we do not show the save-discard widget until something is typed in the // input box. return null; } if ($input_elem.attr("id") === "id_realm_waiting_period_threshold") { // For realm waiting period threshold setting, the custom input element contains // number of days. return util.check_time_input($custom_input_elem.val()!); } return parse_time_limit($custom_input_elem); } export function check_realm_settings_property_changed(elem: HTMLElement): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($elem) as RealmSettingProperty; const current_val = get_realm_settings_property_value(property_name); let proposed_val; switch (property_name) { case "realm_authentication_methods": proposed_val = get_input_element_value(elem, "auth-methods"); break; case "realm_moderation_request_channel_id": case "realm_new_stream_announcements_stream_id": case "realm_signup_announcements_stream_id": case "realm_zulip_update_announcements_stream_id": case "realm_default_code_block_language": case "realm_default_language": case "realm_can_access_all_users_group": case "realm_can_create_web_public_channel_group": proposed_val = get_dropdown_list_widget_setting_value($elem); break; case "realm_can_add_custom_emoji_group": case "realm_can_add_subscribers_group": case "realm_can_create_bots_group": case "realm_can_create_groups": case "realm_can_create_public_channel_group": case "realm_can_create_private_channel_group": case "realm_can_create_write_only_bots_group": case "realm_can_delete_any_message_group": case "realm_can_delete_own_message_group": case "realm_can_invite_users_group": case "realm_can_manage_all_groups": case "realm_can_manage_billing_group": case "realm_can_mention_many_users_group": case "realm_can_move_messages_between_channels_group": case "realm_can_move_messages_between_topics_group": case "realm_can_resolve_topics_group": case "realm_can_set_delete_message_policy_group": case "realm_can_set_topics_policy_group": case "realm_can_summarize_topics_group": case "realm_create_multiuse_invite_group": case "realm_direct_message_initiator_group": case "realm_direct_message_permission_group": case "realm_workplace_users_group": { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); proposed_val = get_group_setting_widget_value(pill_widget); break; } case "realm_message_content_edit_limit_seconds": case "realm_message_content_delete_limit_seconds": case "realm_move_messages_between_streams_limit_seconds": case "realm_move_messages_within_stream_limit_seconds": case "realm_waiting_period_threshold": assert(elem instanceof HTMLSelectElement); proposed_val = get_time_limit_setting_value($(elem), false); break; case "realm_message_retention_days": assert(elem instanceof HTMLSelectElement); proposed_val = get_message_retention_setting_value($(elem), false); break; case "realm_jitsi_server_url": assert(elem instanceof HTMLSelectElement); proposed_val = get_jitsi_server_url_setting_value($(elem), false); break; case "realm_default_avatar_source": proposed_val = get_input_element_value(elem, "radio-group"); break; default: if (current_val !== undefined) { proposed_val = get_input_element_value(elem, typeof current_val); } else { blueslip.error("Element refers to unknown property", {property_name}); } } return !_.isEqual(current_val, proposed_val); } export function check_stream_settings_property_changed( elem: HTMLElement, sub: StreamSubscription, ): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($elem) as StreamSettingProperty; const current_val = get_stream_settings_property_value(property_name, sub); let proposed_val; if (Object.keys(realm.server_supported_permission_settings.stream).includes(property_name)) { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); proposed_val = get_group_setting_widget_value(pill_widget); return !_.isEqual(current_val, proposed_val); } switch (property_name) { case "message_retention_days": assert(elem instanceof HTMLSelectElement); proposed_val = get_message_retention_setting_value($(elem), false); break; case "channel_privacy": proposed_val = get_dropdown_list_widget_setting_value($(elem)); break; case "folder_id": proposed_val = get_channel_folder_value_from_dropdown_widget($(elem)); break; default: if (current_val !== undefined) { proposed_val = get_input_element_value(elem, typeof current_val); } else { blueslip.error("Element refers to unknown property", {property_name}); } } return !_.isEqual(current_val, proposed_val); } export function get_group_setting_widget_value( pill_widget: GroupSettingPillContainer, ): GroupSettingValue { const setting_pills = pill_widget.items(); const direct_subgroups: number[] = []; const direct_members: number[] = []; for (const pill of setting_pills) { if (pill.type === "user_group") { direct_subgroups.push(pill.group_id); } else { assert(pill.user_id !== undefined); direct_members.push(pill.user_id); } } if (direct_members.length === 0 && direct_subgroups.length === 0) { const nobody_group = user_groups.get_user_group_from_name("role:nobody")!; return nobody_group.id; } if (direct_members.length === 0 && direct_subgroups.length === 1) { assert(direct_subgroups[0] !== undefined); return direct_subgroups[0]; } direct_subgroups.sort((a, b) => a - b); direct_members.sort((a, b) => a - b); return {direct_subgroups, direct_members}; } export function check_group_property_changed(elem: HTMLElement, group: UserGroup): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($elem) as keyof UserGroup; const current_val = get_group_property_value(property_name, group); let proposed_val; if (Object.keys(realm.server_supported_permission_settings.group).includes(property_name)) { const pill_widget = get_group_setting_widget(property_name); assert(pill_widget !== null); proposed_val = get_group_setting_widget_value(pill_widget); } else if (current_val !== undefined) { proposed_val = get_input_element_value(elem, typeof current_val); } else { blueslip.error("Element refers to unknown property", {property_name}); } return !_.isEqual(current_val, proposed_val); } export function check_custom_profile_property_changed( elem: HTMLElement, custom_profile_field: CustomProfileField, ): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($elem) as keyof CustomProfileField; const current_val = get_custom_profile_property_value(property_name, custom_profile_field); let proposed_val; if (property_name === "field_data") { proposed_val = get_input_element_value(elem, "field-data-setting"); } else if (current_val !== undefined) { proposed_val = get_input_element_value(elem, typeof current_val); } else { blueslip.error("Element refers to unknown property", {property_name}); } return current_val !== proposed_val; } export function check_realm_default_settings_property_changed(elem: HTMLElement): boolean { const $elem = $(elem); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($elem, true) as RealmUserSettingDefaultProperties; const current_val = get_realm_default_setting_property_value(property_name); let proposed_val; switch (property_name) { case "color_scheme": case "emojiset": case "user_list_style": proposed_val = get_input_element_value(elem, "radio-group"); break; case "email_notifications_batching_period_seconds": assert(elem instanceof HTMLSelectElement); proposed_val = get_time_limit_setting_value($(elem), false); break; case "web_font_size_px": case "web_line_height_percent": assert(elem instanceof HTMLInputElement); proposed_val = Number.parseInt($(elem).val()!, 10); break; default: if (current_val !== undefined) { proposed_val = get_input_element_value(elem, typeof current_val); } else { blueslip.error("Element refers to unknown property", {property_name}); } } return current_val !== proposed_val; } function get_request_data_for_org_join_restrictions(selected_val: string): { disallow_disposable_email_addresses: boolean; emails_restricted_to_domains: boolean; } { switch (selected_val) { case "only_selected_domain": { return { emails_restricted_to_domains: true, disallow_disposable_email_addresses: false, }; } case "no_disposable_email": { return { emails_restricted_to_domains: false, disallow_disposable_email_addresses: true, }; } default: { return { disallow_disposable_email_addresses: false, emails_restricted_to_domains: false, }; } } } export function populate_data_for_realm_settings_request( $subsection_elem: JQuery, ): Record<string, string | boolean | number> { let data: Record<string, string | boolean | number> = {}; const properties_elements = get_subsection_property_elements($subsection_elem); for (const input_elem of properties_elements) { const $input_elem = $(input_elem); if (check_realm_settings_property_changed(input_elem)) { const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { let match_array; if ($input_elem.attr("id")!.startsWith("id_authmethod")) { // Authentication Method component IDs include authentication method name // for uniqueness, anchored to "id_authmethod" prefix, e.g. "id_authmethodapple_<property_name>". // We need to strip that whole construct down to extract the actual property name. // The [\da-z]+ part of the regexp covers the auth method name itself. // We assume it's not an empty string and can contain only digits and lowercase ASCII letters, // this is ensured by a respective allowlist-based filter in populate_auth_methods(). match_array = /^id_authmethod[\da-z]+_(.*)$/.exec($input_elem.attr("id")!); } else { match_array = /^id_realm_(.*)$/.exec($input_elem.attr("id")!); } assert(match_array !== null); const property_name = match_array[1]!; if (property_name === "org_join_restrictions") { assert(typeof input_value === "string"); data = { ...data, ...get_request_data_for_org_join_restrictions(input_value), }; continue; } const realm_group_settings = new Set([ "can_access_all_users_group", "can_add_custom_emoji_group", "can_add_subscribers_group", "can_create_bots_group", "can_create_groups", "can_create_private_channel_group", "can_create_public_channel_group", "can_create_web_public_channel_group", "can_create_write_only_bots_group", "can_manage_all_groups", "can_manage_billing_group", "can_delete_any_message_group", "can_delete_own_message_group", "can_invite_users_group", "can_mention_many_users_group", "can_move_messages_between_channels_group", "can_move_messages_between_topics_group", "can_resolve_topics_group", "can_set_delete_message_policy_group", "can_set_topics_policy_group", "can_summarize_topics_group", "create_multiuse_invite_group", "direct_message_initiator_group", "direct_message_permission_group", "workplace_users_group", ]); if (realm_group_settings.has(property_name)) { const old_value = get_realm_settings_property_value( // eslint-disable-next-line @typescript-eslint/consistent-type-assertions ("realm_" + property_name) as RealmSettingProperty, ); data[property_name] = JSON.stringify({ new: input_value, old: old_value, }); continue; } assert(typeof input_value !== "object"); data[property_name] = input_value; } } } return data; } export function populate_data_for_stream_settings_request( $subsection_elem: JQuery, sub: StreamSubscription, ): Record<string, string | boolean | number> { let data: Record<string, string | boolean | number> = {}; const properties_elements = get_subsection_property_elements($subsection_elem); for (const input_elem of properties_elements) { const $input_elem = $(input_elem); if (check_stream_settings_property_changed(input_elem, sub)) { const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { const property_name = extract_property_name($input_elem); if (property_name === "channel_privacy") { assert(typeof input_value === "string"); data = { ...data, ...settings_data.get_request_data_for_stream_privacy(input_value), }; continue; } if (stream_permission_group_settings_schema.safeParse(property_name).success) { const old_value = get_stream_settings_property_value( stream_settings_property_schema.parse(property_name), sub, ); data[property_name] = JSON.stringify({ new: input_value, old: old_value, }); continue; } if (property_name === "folder_id") { const folder_id = get_channel_folder_value_from_dropdown_widget($input_elem); data[property_name] = JSON.stringify(folder_id); continue; } if (property_name === "history_public_to_subscribers") { data[property_name] = JSON.stringify(input_value); continue; } assert(typeof input_value !== "object"); data[property_name] = input_value; } } } return data; } export function populate_data_for_group_request( $subsection_elem: JQuery, group: UserGroup, ): Record<string, string | boolean | number> { const data: Record<string, string | boolean | number> = {}; const properties_elements = get_subsection_property_elements($subsection_elem); for (const input_elem of properties_elements) { const $input_elem = $(input_elem); if (check_group_property_changed(input_elem, group)) { const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const property_name = extract_property_name($input_elem) as keyof UserGroup; const old_value = get_group_property_value(property_name, group); data[property_name] = JSON.stringify({ new: input_value, old: old_value, }); } } } return data; } export function populate_data_for_custom_profile_field_request( $subsection_elem: JQuery, custom_profile_field: CustomProfileField, ): Record<string, string | boolean | number> { const data: Record<string, string | boolean | number> = {}; const properties_elements = get_subsection_property_elements($subsection_elem); for (const input_elem of properties_elements) { const $input_elem = $(input_elem); if (check_custom_profile_property_changed(input_elem, custom_profile_field)) { const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { const property_name = extract_property_name($input_elem); assert(typeof input_value !== "object"); data[property_name] = input_value; } } } return data; } export function populate_data_for_default_realm_settings_request( $subsection_elem: JQuery, ): Record<string, string | boolean | number> { const data: Record<string, string | boolean | number> = {}; const properties_elements = get_subsection_property_elements($subsection_elem); for (const input_elem of properties_elements) { const $input_elem = $(input_elem); if (check_realm_default_settings_property_changed(input_elem)) { const input_value = get_input_element_value(input_elem); if (input_value !== undefined && input_value !== null) { const property_name: string = extract_property_name($input_elem, true); assert(typeof input_value !== "object"); data[property_name] = input_value; } } } return data; } function switching_to_private(properties_elements: HTMLElement[]): boolean { for (const elem of properties_elements) { const $elem = $(elem); const property_name = extract_property_name($elem); if (property_name !== "channel_privacy") { continue; } const proposed_val = get_input_element_value(elem); return proposed_val === "invite-only"; } return false; } export function save_discard_realm_settings_widget_status_handler($subsection: JQuery): void { $subsection.find(".alert-notification").hide(); $subsection.find(".save-button").show(); const properties_elements = get_subsection_property_elements($subsection); const show_change_process_button = properties_elements.some((elem) => check_realm_settings_property_changed(elem), ); const $save_button_controls = $subsection.find(".subsection-header .save-button-controls"); const button_state = show_change_process_button ? "unsaved" : "discarded"; change_save_button_state($save_button_controls, button_state); } export function save_discard_stream_settings_widget_status_handler( $subsection: JQuery, sub: StreamSubscription | undefined, ): void { $subsection.find(".alert-notification").hide(); $subsection.find(".save-button").show(); const properties_elements = get_subsection_property_elements($subsection); let show_change_process_button = false; if (sub !== undefined) { show_change_process_button = properties_elements.some((elem) => check_stream_settings_property_changed(elem, sub), ); } const $save_button_controls = $subsection.find(".subsection-header .save-button-controls"); const button_state = show_change_process_button ? "unsaved" : "discarded"; change_save_button_state($save_button_controls, button_state); // If the stream isn't currently private but being changed to private, // and the user changing this setting isn't subscribed, we show a // warning that they won't be able to access the stream after // making it private unless they subscribe. if ( button_state === "unsaved" && sub && !sub.invite_only && !sub.subscribed && switching_to_private(properties_elements) ) { if ($("#stream_settings .stream_privacy_warning").length > 0) { return; } const context = { banner_type: compose_banner.WARNING, banner_text: $t({ defaultMessage: "You will lose access to content in this channel if you make it private. To keep access, subscribe or grant yourself permission to do so under Advanced configurations.", }), button_text: $t({defaultMessage: "Subscribe"}), classname: "stream_privacy_warning", stream_id: sub.stream_id, }; $("#stream_settings .stream-permissions-warning-banner").append( $(render_compose_banner(context)), ); } else { $("#stream_settings .stream-permissions-warning-banner").empty(); } } export function save_discard_group_widget_status_handler( $subsection: JQuery, group: UserGroup, ): void { $subsection.find(".alert-notification").hide(); $subsection.find(".save-button").show(); const properties_elements = get_subsection_property_elements($subsection); const show_change_process_button = properties_elements.some((elem) => check_group_property_changed(elem, group), ); const $save_button_controls = $subsection.find(".subsection-header .save-button-controls"); const button_state = show_change_process_button ? "unsaved" : "discarded"; change_save_button_state($save_button_controls, button_state); } export function save_discard_default_realm_settings_widget_status_handler( $subsection: JQuery, ): void { $subsection.find(".alert-notification").hide(); $subsection.find(".save-button").show(); const properties_elements = get_subsection_property_elements($subsection); const show_change_process_button = properties_elements.some((elem) => check_realm_default_settings_property_changed(elem), ); const $save_button_controls = $subsection.find(".subsection-header .save-button-controls"); const button_state = show_change_process_button ? "unsaved" : "discarded"; change_save_button_state($save_button_controls, button_state); } function check_maximum_valid_value( $custom_input_elem: JQuery<HTMLInputElement>, property_name: string, ): boolean { let setting_value = Number.parseInt($custom_input_elem.val()!, 10); if ( property_name === "realm_message_content_edit_limit_seconds" || property_name === "realm_message_content_delete_limit_seconds" || property_name === "realm_move_messages_between_streams_limit_seconds" || property_name === "realm_move_messages_within_stream_limit_seconds" || property_name === "email_notifications_batching_period_seconds" ) { setting_value = parse_time_limit($custom_input_elem); } return setting_value <= MAX_CUSTOM_TIME_LIMIT_SETTING_VALUE; } function should_disable_save_button_for_jitsi_server_url_setting(): boolean { if (!is_video_chat_provider_jitsi_meet()) { return false; } const $dropdown_elem = $<HTMLSelectOneElement>( "select:not([multiple])#id_realm_jitsi_server_url", ); const $custom_input_elem = $<HTMLInputElement>("input#id_realm_jitsi_server_url_custom_input"); return $dropdown_elem.val() === "custom" && !util.is_valid_url($custom_input_elem.val()!, true); } function should_disable_save_button_for_time_limit_settings( time_limit_settings: HTMLElement[], ): boolean { let disable_save_button = false; for (const setting_elem of time_limit_settings) { const $dropdown_elem = $(setting_elem).find<HTMLSelectOneElement>("select:not([multiple])"); const $custom_input_elem = $(setting_elem).find<HTMLInputElement>( "input.time-limit-custom-input", ); const custom_input_elem_val = util.check_time_input($custom_input_elem.val()!); const for_realm_default_settings = $dropdown_elem.closest(".settings-section.show").attr("id") === "realm-user-default-settings"; const property_name = extract_property_name($dropdown_elem, for_realm_default_settings); disable_save_button = $dropdown_elem.val() === "custom_period" && (custom_input_elem_val <= 0 || Number.isNaN(custom_input_elem_val) || !check_maximum_valid_value($custom_input_elem, property_name)); if ( $custom_input_elem.val() === "0" && property_name === "realm_waiting_period_threshold" ) { // 0 is a valid value for realm_waiting_period_threshold setting. We specifically // check for $custom_input_elem.val() to be "0" and not custom_input_elem_val // because it is 0 even when custom input box is empty. disable_save_button = false; } if (disable_save_button) { break; } } return disable_save_button; } function should_disable_save_button_for_group_settings(settings: string[]): boolean { for (const setting_name of settings) { let group_setting_config; if (setting_name.startsWith("realm_")) { const setting_name_without_prefix = /^realm_(.*)$/.exec(setting_name)![1]!; group_setting_config = group_permission_settings.get_group_permission_setting_config( setting_name_without_prefix, "realm", ); } else if (stream_permission_group_settings_schema.safeParse(setting_name).success) { group_setting_config = group_permission_settings.get_group_permission_setting_config( setting_name, "stream", ); } else { group_setting_config = group_permission_settings.get_group_permission_setting_config( setting_name, "group", ); } assert(group_setting_config !== undefined); const pill_widget = get_group_setting_widget(setting_name); assert(pill_widget !== null); if (pill_widget.is_pending()) { return true; } if (group_setting_config.allow_nobody_group) { continue; } const setting_value = get_group_setting_widget_value(pill_widget); const nobody_group = user_groups.get_user_group_from_name("role:nobody")!; if (setting_value === nobody_group.id) { return true; } } return false; } function should_disable_save_button_for_stream_settings(stream_id: number): boolean { // Disable save button if "Only general chat topic allowed" option is selected // for `topics_policy` and there are topics other than the general chat in the // current channel. const topics_policy_value = $("#id_topics_policy").val(); if ( topics_policy_value === settings_config.get_stream_topics_policy_values().empty_topic_only.code && stream_topic_history.stream_has_locally_available_named_topics(stream_id) ) { $("#settings-topics-already-exist-error").show(); return true; } $("#settings-topics-already-exist-error").hide(); return false; } function enable_or_disable_save_button($subsection_elem: JQuery): void { const $save_button = $subsection_elem.find(".save-button"); if ($subsection_elem.closest(".channel-permissions").length > 0) { const $settings_container = $subsection_elem.closest(".subscription_settings"); const stream_id_string = $settings_container.attr("data-stream-id"); assert(stream_id_string !== undefined); const stream_id = Number.parseInt(stream_id_string, 10); if (should_disable_save_button_for_stream_settings(stream_id)) { $save_button.prop("disabled", true); return; } } const time_limit_settings = [...$subsection_elem.find(".time-limit-setting")]; if ( time_limit_settings.length > 0 && should_disable_save_button_for_time_limit_settings(time_limit_settings) ) { if ( $subsection_elem.attr("id") === "org-message-retention" || $subsection_elem.closest(".advanced-configurations-container").length > 0 ) { ui_util.disable_element_and_add_tooltip( $save_button, $t({ defaultMessage: "Cannot save invalid message retention period.", }), ); return; } $save_button.prop("disabled", true); return; } if ( $subsection_elem.attr("id") === "org-compose-settings" && should_disable_save_button_for_jitsi_server_url_setting() ) { ui_util.disable_element_and_add_tooltip( $save_button, $t({defaultMessage: "Cannot save invalid Jitsi server URL."}), ); return; } const group_settings = [...$subsection_elem.find(".pill-container.prop-element")].map((elem) => extract_property_name($(elem)), ); if ( group_settings.length > 0 && should_disable_save_button_for_group_settings(group_settings) ) { $save_button.prop("disabled", true); return; } ui_util.enable_element_and_remove_tooltip($save_button); } export function initialize_disable_button_hint_popover( $button_wrapper: JQuery, hint_text: string | undefined, opts: Partial<tippy.Props> = {}, ): void { const tippy_opts: Partial<tippy.Props> = { animation: false, hideOnClick: false, placement: "bottom", ...opts, }; // If hint_text is undefined, we use the HTML content of a // <template> whose id is given by data-tooltip-template-id if (hint_text !== undefined) { tippy_opts.content = hint_text; } tippy.default(util.the($button_wrapper), tippy_opts); } export function enable_opening_typeahead_on_clicking_label($container: JQuery): void { const $group_setting_labels = $container.find(".group-setting-label"); $group_setting_labels.on("click", (e) => { if ($(e.target).is("a.help_link_widget, a.help_link_widget i")) { // Clicking on the "?" icon should just open the link and there is // no need to open the typeahead or focus the input, so we return. return; } // Click opens the typeahead. $(e.currentTarget).siblings(".pill-container").find(".input").expectOne().trigger("click"); // Focus puts the cursor into the input. $(e.currentTarget).siblings(".pill-container").find(".input").expectOne().trigger("focus"); }); } export function disable_opening_typeahead_on_clicking_label($container: JQuery): void { const $group_setting_labels = $container.find(".group-setting-label"); $group_setting_labels.off("click"); } export function disable_group_permission_setting($containers: JQuery, placeholder?: string): void { $containers.find(".input").prop("contenteditable", false); if (placeholder !== undefined) { $containers.find(".input").attr("data-placeholder", placeholder); } $containers.closest(".input-group").addClass("group_setting_disabled"); disable_opening_typeahead_on_clicking_label($containers.closest(".input-group")); } export function enable_group_permission_setting($containers: JQuery, placeholder?: string): void { $containers.find(".input").prop("contenteditable", true); if (placeholder !== undefined) { $containers.find(".input").attr("data-placeholder", placeholder); } $containers.closest(".input-group").removeClass("group_setting_disabled"); enable_opening_typeahead_on_clicking_label($containers.closest(".input-group")); } export const group_setting_widget_map = new Map<string, GroupSettingPillContainer | null>([ ["can_add_members_group", null], ["can_add_subscribers_group", null], ["can_administer_channel_group", null], ["can_create_topic_group", null], ["can_join_group", null], ["can_leave_group", null], ["can_manage_group", null], ["can_mention_group", null], ["can_delete_any_message_group", null], ["can_delete_own_message_group", null], ["can_move_messages_out_of_channel_group", null], ["can_move_messages_within_channel_group", null], ["can_remove_members_group", null], ["can_remove_subscribers_group", null], ["can_resolve_topics_group", null], ["can_send_message_group", null], ["realm_can_add_custom_emoji_group", null], ["realm_can_add_subscribers_group", null], ["realm_can_create_bots_group", null], ["realm_can_create_groups", null], ["realm_can_create_public_channel_group", null], ["realm_can_create_private_channel_group", null], ["realm_can_create_write_only_bots_group", null], ["realm_can_delete_any_message_group", null], ["realm_can_delete_own_message_group", null], ["realm_can_invite_users_group", null], ["realm_can_manage_all_groups", null], ["realm_can_manage_billing_group", null], ["realm_can_mention_many_users_group", null], ["realm_can_move_messages_between_channels_group", null], ["realm_can_move_messages_between_topics_group", null], ["realm_can_resolve_topics_group", null], ["realm_can_set_delete_message_policy_group", null], ["realm_can_set_topics_policy_group", null], ["realm_can_summarize_topics_group", null], ["realm_create_multiuse_invite_group", null], ["realm_direct_message_initiator_group", null], ["realm_direct_message_permission_group", null], ["realm_workplace_users_group", null], ]); export function get_group_setting_widget(setting_name: string): GroupSettingPillContainer | null { const pill_widget = group_setting_widget_map.get(setting_name); if (pill_widget === undefined) { blueslip.error("No dropdown list widget for property", {setting_name}); return null; } return pill_widget; } export function set_group_setting_widget_value( pill_widget: GroupSettingPillContainer, property_value: GroupSettingValue, ): void { pill_widget.clear(true); if (typeof property_value === "number") { const user_group = user_groups.get_user_group_from_id(property_value); if (user_group.name === "role:nobody") { return; } user_group_pill.append_user_group(user_group, pill_widget, false); } else { for (const setting_sub_group_id of property_value.direct_subgroups) { const user_group = user_groups.get_user_group_from_id(setting_sub_group_id); if (user_group.name === "role:nobody") { continue; } user_group_pill.append_user_group(user_group, pill_widget, false); } for (const setting_user_id of property_value.direct_members) { const user = people.get_user_by_id_assert_valid(setting_user_id); user_pill.append_user(user, pill_widget, false); } } } export function create_group_setting_widget({ $pill_container, setting_name, group, }: { $pill_container: JQuery; setting_name: GroupGroupSettingName; group?: UserGroup; }): GroupSettingPillContainer { const pill_widget = group_setting_pill.create_pills($pill_container, setting_name, "group"); const opts: { setting_name: string; group: UserGroup | undefined; setting_type: "group"; } = { setting_name, group, setting_type: "group", }; group_setting_pill.set_up_pill_typeahead({pill_widget, $pill_container, opts}); if (group !== undefined) { group_setting_widget_map.set(setting_name, pill_widget); } if (group !== undefined) { set_group_setting_widget_value(pill_widget, group[setting_name]); pill_widget.onTextInputHook(() => { save_discard_group_widget_status_handler($("#group_permission_settings"), group); }); pill_widget.onPillCreate(() => { save_discard_group_widget_status_handler($("#group_permission_settings"), group); }); pill_widget.onPillRemove(() => { save_discard_group_widget_status_handler($("#group_permission_settings"), group); }); } else { const default_group_name = group_permission_settings.get_group_permission_setting_config( setting_name, "group", )!.default_group_name; if (default_group_name === "group_creator") { set_group_setting_widget_value(pill_widget, { direct_members: [current_user.user_id], direct_subgroups: [], }); } else { const default_group_id = user_groups.get_user_group_from_name(default_group_name)!.id; set_group_setting_widget_value(pill_widget, default_group_id); } } return pill_widget; } export function create_realm_group_setting_widget({ $pill_container, setting_name, pill_update_callback, }: { $pill_container: JQuery; setting_name: RealmGroupSettingNameSupportingAnonymousGroups; pill_update_callback?: () => void; }): void { const pill_widget = group_setting_pill.create_pills($pill_container, setting_name, "realm"); const opts: { setting_name: string; setting_type: "realm"; } = { setting_name, setting_type: "realm", }; group_setting_pill.set_up_pill_typeahead({pill_widget, $pill_container, opts}); group_setting_widget_map.set("realm_" + setting_name, pill_widget); set_group_setting_widget_value( pill_widget, group_setting_value_schema.parse( realm[z.keyof(realm_schema).parse("realm_" + setting_name)], ), ); const $save_discard_widget_container = $(`#id_realm_${CSS.escape(setting_name)}`).closest( ".settings-subsection-parent", ); pill_widget.onTextInputHook(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_realm_settings_widget_status_handler($save_discard_widget_container); }); pill_widget.onPillCreate(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_realm_settings_widget_status_handler($save_discard_widget_container); }); pill_widget.onPillRemove(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_realm_settings_widget_status_handler($save_discard_widget_container); }); } export function create_stream_group_setting_widget({ $pill_container, setting_name, sub, pill_update_callback, }: { $pill_container: JQuery; setting_name: StreamPermissionGroupSetting; sub?: StreamSubscription; pill_update_callback?: () => void; }): GroupSettingPillContainer { const pill_widget = group_setting_pill.create_pills($pill_container, setting_name, "stream"); const opts: { setting_name: string; sub: StreamSubscription | undefined; setting_type: "stream"; } = { setting_name, sub, setting_type: "stream", }; group_setting_pill.set_up_pill_typeahead({pill_widget, $pill_container, opts}); if (sub !== undefined) { group_setting_widget_map.set(setting_name, pill_widget); } if (sub !== undefined) { set_group_setting_widget_value(pill_widget, sub[setting_name]); const $subsection = $pill_container.closest(".settings-subsection-parent"); pill_widget.onTextInputHook(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_stream_settings_widget_status_handler($subsection, sub); }); pill_widget.onPillCreate(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_stream_settings_widget_status_handler($subsection, sub); }); pill_widget.onPillRemove(() => { if (pill_update_callback !== undefined) { pill_update_callback(); } save_discard_stream_settings_widget_status_handler($subsection, sub); }); } else { const default_group_name = group_permission_settings.get_group_permission_setting_config( setting_name, "stream", )!.default_group_name; if (default_group_name === "channel_creator") { set_group_setting_widget_value(pill_widget, { direct_members: [current_user.user_id], direct_subgroups: [], }); } else { const default_group_id = user_groups.get_user_group_from_name(default_group_name)!.id; set_group_setting_widget_value(pill_widget, default_group_id); } } return pill_widget; } export function set_time_input_formatted_text( $time_select_elem: JQuery, formatted_text: string, ): void { if ($time_select_elem.val() === "custom") { $time_select_elem.parent().find(".time-input-formatted-description").hide(); $time_select_elem .parent() .find(".custom-time-input-formatted-description") .text(formatted_text); } else { $time_select_elem.parent().find(".time-input-formatted-description").show(); $time_select_elem.parent().find(".time-input-formatted-description").text(formatted_text); } } export function set_custom_time_inputs_visibility( $time_select_elem: JQuery, time_unit: string, time_value: number, ): void { if ($time_select_elem.val() === "custom") { $time_select_elem.parent().find(".custom-time-input-value").val(time_value); $time_select_elem.parent().find(".custom-time-input-unit").val(time_unit); $time_select_elem.parent().find(".custom-time-input-container").show(); } else { $time_select_elem.parent().find(".custom-time-input-container").hide(); } } export function get_group_assigned_realm_permissions(group: UserGroup): { subsection_key: string; subsection_heading: string; assigned_permissions: AssignedGroupPermission[]; }[] { const group_assigned_realm_permissions = []; for (const { subsection_heading, subsection_key, settings, } of settings_config.realm_group_permission_settings) { const assigned_permission_objects = []; for (const setting_name of settings) { if ( setting_name === "workplace_users_group" && (!page_params.development_environment || !page_params.non_workplace_pricing_eligible) ) { continue; } const setting_value = realm[z.keyof(realm_schema).parse("realm_" + setting_name)]; const can_edit = settings_config.owner_editable_realm_group_permission_settings.has( setting_name, ) ? current_user.is_owner : current_user.is_admin; const assigned_permission_object = group_permission_settings.get_assigned_permission_object( group_setting_value_schema.parse(setting_value), setting_name, group.id, can_edit, "realm", ); if (assigned_permission_object !== undefined) { assigned_permission_objects.push(assigned_permission_object); } } group_assigned_realm_permissions.push({ subsection_heading, subsection_key, assigned_permissions: assigned_permission_objects, }); } return group_assigned_realm_permissions; } export function get_group_assigned_stream_permissions(group: UserGroup): { stream: StreamSubscription; assigned_permissions: AssignedGroupPermission[]; id_prefix: string; }[] { const subs = stream_data.get_unsorted_subs(); const group_assigned_stream_permissions = []; for (const sub of subs) { const assigned_permission_objects = []; const can_edit_settings_with_metadata_access = stream_data.can_change_permissions_requiring_metadata_access(sub); const can_edit_settings_with_content_access = stream_data.can_change_permissions_requiring_content_access(sub); for (const setting_name of settings_config.stream_group_permission_settings) { const setting_value = sub[z.keyof(stream_subscription_schema).parse(setting_name)]; let can_edit_settings = can_edit_settings_with_metadata_access; if ( settings_config.stream_group_permission_settings_requiring_content_access.includes( setting_name, ) ) { can_edit_settings = can_edit_settings_with_content_access; } const assigned_permission_object = group_permission_settings.get_assigned_permission_object( group_setting_value_schema.parse(setting_value), setting_name, group.id, can_edit_settings, "stream", ); if (assigned_permission_object !== undefined) { assigned_permission_objects.push(assigned_permission_object); } } if (assigned_permission_objects.length > 0) { group_assigned_stream_permissions.push({ stream: sub, assigned_permissions: assigned_permission_objects, id_prefix: "id_group_permission_" + sub.stream_id.toString() + "_", }); } } return group_assigned_stream_permissions; } export function get_group_assigned_user_group_permissions(group: UserGroup): { group_id: number; group_name: string; assigned_permissions: AssignedGroupPermission[]; id_prefix: string; }[] { const groups = user_groups.get_realm_user_groups(); const group_assigned_user_group_permissions = []; for (const user_group of groups) { const can_edit_settings = settings_data.can_manage_user_group(user_group.id); const assigned_permission_objects = []; for (const setting_name of settings_config.group_permission_settings) { const setting_value = user_group[z.keyof(user_groups.user_group_schema).parse(setting_name)]; const assigned_permission_object = group_permission_settings.get_assigned_permission_object( group_setting_value_schema.parse(setting_value), setting_name, group.id, can_edit_settings, "group", ); if (assigned_permission_object !== undefined) { assigned_permission_objects.push(assigned_permission_object); } } if (assigned_permission_objects.length > 0) { group_assigned_user_group_permissions.push({ group_id: user_group.id, group_name: user_groups.get_display_group_name(user_group.name), id_prefix: "id_group_permission_" + user_group.id.toString() + "_", assigned_permissions: assigned_permission_objects, }); } } return group_assigned_user_group_permissions; } export function set_channel_folder_dropdown_value(sub: StreamSubscription): void { if (sub.folder_id === null) { set_dropdown_list_widget_setting_value("folder_id", settings_config.no_folder_selected); return; } set_dropdown_list_widget_setting_value("folder_id", sub.folder_id); } export function get_channel_folder_value_from_dropdown_widget($elem: JQuery): number | null { const value = get_dropdown_list_widget_setting_value($elem); assert(typeof value === "number"); if (value === settings_config.no_folder_selected) { return null; } return value; } export const language_options = (): Option[] => { const languages = get_language_list_columns(realm.realm_default_language).toSorted((a, b) => util.strcmp(a.name_with_percent, b.name_with_percent), ); return languages.map((language) => ({ name: language.name_with_percent, unique_id: language.code, })); }; export function resize_textareas_in_section($section: JQuery): void { const $subsections = $section.find(".settings-subsection-parent"); if ($subsections.length === 0) { return; } $subsections.each(function () { resize_textareas_in_subsection($(this)); }); } export let resize_textareas_in_subsection = ($subsection: JQuery): void => { const $textareas = $subsection.find("textarea"); if ($textareas.length === 0) { return; } $textareas.each(function () { const $el = $(this); const min_rows = 2; const max_rows = 5; $el.attr("rows", min_rows); const scrollheight = util.the($el).scrollHeight; const line_height = Number.parseFloat($el.css("line-height")); const needed_rows = Math.ceil(scrollheight / line_height) - 1; const new_rows = Math.min(Math.max(needed_rows, min_rows), max_rows); $el.attr("rows", new_rows); }); }; export function rewire_resize_textareas_in_subsection( value: typeof resize_textareas_in_subsection, ): void { resize_textareas_in_subsection = value; }