diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 768a0776f4..8db0118b9f 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 348** + +* [`POST /register`](/api/register-queue), [`POST /events`](/api/get-events), + `PATCH /realm`: Added `enable_guest_user_dm_warning` setting to decide + whether clients should show a warning when a user is composing to a + guest user in the organization. + **Feature level 347** * [Markdown message formatting](/api/message-formatting#links-to-channels-topics-and-messages): diff --git a/help/guest-users.md b/help/guest-users.md index e99c56d1f1..8f6ed9efd6 100644 --- a/help/guest-users.md +++ b/help/guest-users.md @@ -42,6 +42,20 @@ pricing](/help/zulip-cloud-billing#temporary-users-and-guests) for guest users. {end_tabs} +## Configure warning for direct messages to guest users + +{start_tabs} + +{tab|desktop-web} + +{settings_tab|organization-permissions} + +1. Under **Guests**, toggle **Display a warning when composing a direct message with guest user recipients**. + +{!save-changes.md!} + +{end_tabs} + ## Configure whether guests can see all other users {!cloud-plus-only.md!} diff --git a/version.py b/version.py index 2dda916b0d..1eaa91ddd9 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 347 # Last bumped for /with/ in topic links. +API_FEATURE_LEVEL = 348 # Last bumped for enable_guest_user_dm_warning. # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/web/src/admin.ts b/web/src/admin.ts index e4edad6dd9..cef449a983 100644 --- a/web/src/admin.ts +++ b/web/src/admin.ts @@ -73,6 +73,10 @@ const admin_settings_label = { realm_enable_guest_user_indicator: $t({ defaultMessage: "Display “(guest)” after names of guest users", }), + realm_enable_guest_user_dm_warning: $t({ + defaultMessage: + "Display a warning when composing a direct message with guest user recipients", + }), }; function insert_tip_box(): void { @@ -246,6 +250,7 @@ export function build_page(): void { automatically_unmute_topics_in_muted_streams_policy_values: settings_config.automatically_follow_or_unmute_topics_policy_values, realm_enable_guest_user_indicator: realm.realm_enable_guest_user_indicator, + realm_enable_guest_user_dm_warning: realm.realm_enable_guest_user_dm_warning, active_user_list_dropdown_widget_name: settings_users.active_user_list_dropdown_widget_name, deactivated_user_list_dropdown_widget_name: settings_users.deactivated_user_list_dropdown_widget_name, diff --git a/web/src/compose_actions.ts b/web/src/compose_actions.ts index 77b8a9f52c..0688b840a1 100644 --- a/web/src/compose_actions.ts +++ b/web/src/compose_actions.ts @@ -127,6 +127,7 @@ function clear_box(): void { // TODO: Better encapsulate at-mention warnings. compose_validate.clear_topic_resolved_warning(); compose_validate.clear_stream_wildcard_warnings($("#compose_banners")); + compose_validate.clear_guest_in_dm_recipient_warning(); compose_validate.set_user_acknowledged_stream_wildcard_flag(false); compose_state.set_recipient_edited_manually(false); @@ -408,6 +409,8 @@ export let start = (raw_opts: ComposeActionsStartOpts): void => { // Show a warning if topic is resolved compose_validate.warn_if_topic_resolved(true); + // Show a warning if dm recipient contains guest + compose_validate.warn_if_guest_in_dm_recipient(); // Show a warning if the user is in a search narrow when replying to a message if (opts.is_reply) { compose_validate.warn_if_in_search_view(); diff --git a/web/src/compose_banner.ts b/web/src/compose_banner.ts index f07be80424..5208b25a2b 100644 --- a/web/src/compose_banner.ts +++ b/web/src/compose_banner.ts @@ -43,6 +43,7 @@ export const CLASSNAMES = { recipient_not_subscribed: "recipient_not_subscribed", wildcard_warning: "wildcard_warning", private_stream_warning: "private_stream_warning", + guest_in_dm_recipient_warning: "guest_in_dm_recipient_warning", unscheduled_message: "unscheduled_message", search_view: "search_view", // errors diff --git a/web/src/compose_recipient.ts b/web/src/compose_recipient.ts index 38ef7061f0..90b602cc8e 100644 --- a/web/src/compose_recipient.ts +++ b/web/src/compose_recipient.ts @@ -114,6 +114,7 @@ function update_fade(): void { export function update_on_recipient_change(): void { update_fade(); update_narrow_to_recipient_visibility(); + compose_validate.warn_if_guest_in_dm_recipient(); drafts.update_compose_draft_count(); check_posting_policy_for_compose_box(); } diff --git a/web/src/compose_state.ts b/web/src/compose_state.ts index 090ca525fa..2c8e692628 100644 --- a/web/src/compose_state.ts +++ b/web/src/compose_state.ts @@ -17,6 +17,7 @@ let last_focused_compose_type_input: HTMLTextAreaElement | undefined; // the narrow and the user should still be able to see the banner once after // performing these actions let recipient_viewed_topic_resolved_banner = false; +let recipient_guest_ids_for_dm_warning: number[] = []; export function set_recipient_edited_manually(flag: boolean): void { recipient_edited_manually = flag; @@ -58,6 +59,14 @@ export function has_recipient_viewed_topic_resolved_banner(): boolean { return recipient_viewed_topic_resolved_banner; } +export function set_recipient_guest_ids_for_dm_warning(guest_ids: number[]): void { + recipient_guest_ids_for_dm_warning = guest_ids; +} + +export function get_recipient_guest_ids_for_dm_warning(): number[] { + return recipient_guest_ids_for_dm_warning; +} + export function composing(): boolean { // This is very similar to get_message_type(), but it returns // a boolean. diff --git a/web/src/compose_validate.ts b/web/src/compose_validate.ts index 20a411651b..005ffb25d4 100644 --- a/web/src/compose_validate.ts +++ b/web/src/compose_validate.ts @@ -1,7 +1,9 @@ import $ from "jquery"; +import _ from "lodash"; import * as resolved_topic from "../shared/src/resolved_topic.ts"; import render_compose_banner from "../templates/compose_banner/compose_banner.hbs"; +import render_guest_in_dm_recipient_warning from "../templates/compose_banner/guest_in_dm_recipient_warning.hbs"; import render_not_subscribed_warning from "../templates/compose_banner/not_subscribed_warning.hbs"; import render_private_stream_warning from "../templates/compose_banner/private_stream_warning.hbs"; import render_stream_wildcard_warning from "../templates/compose_banner/stream_wildcard_warning.hbs"; @@ -389,6 +391,72 @@ export function warn_if_in_search_view(): void { } } +export function clear_guest_in_dm_recipient_warning(): void { + // We don't call set_recipient_guest_ids_for_dm_warning here, so + // that reopening the same draft won't make the banner reappear. + const classname = compose_banner.CLASSNAMES.guest_in_dm_recipient_warning; + $(`#compose_banners .${CSS.escape(classname)}`).remove(); +} + +// Only called on recipient change. Adds new banner if not already +// exists or updates the existing banner or removes banner if no +// guest in the dm. +export function warn_if_guest_in_dm_recipient(): void { + if (!compose_state.composing()) { + return; + } + const recipient_ids = compose_pm_pill.get_user_ids(); + const guest_ids = people.filter_other_guest_ids(recipient_ids); + + if ( + !realm.realm_enable_guest_user_dm_warning || + compose_state.get_message_type() !== "private" || + guest_ids.length === 0 + ) { + clear_guest_in_dm_recipient_warning(); + compose_state.set_recipient_guest_ids_for_dm_warning([]); + return; + } + // If warning was shown earlier for same guests in the recipients, do nothing. + if (_.isEqual(compose_state.get_recipient_guest_ids_for_dm_warning(), guest_ids)) { + return; + } + + const guest_names = people.user_ids_to_full_names_array(guest_ids); + let banner_text: string; + + if (guest_names.length === 1) { + banner_text = $t( + {defaultMessage: "{name} is a guest in this organization."}, + {name: guest_names[0]}, + ); + } else { + const names_string = util.format_array_as_list(guest_names, "long", "conjunction"); + banner_text = $t( + {defaultMessage: "{names} are guests in this organization."}, + {names: names_string}, + ); + } + + const classname = compose_banner.CLASSNAMES.guest_in_dm_recipient_warning; + let $banner = $(`#compose_banners .${CSS.escape(classname)}`); + + compose_state.set_recipient_guest_ids_for_dm_warning(guest_ids); + // Update banner text if banner already exists. + if ($banner.length === 1) { + $banner.find(".banner_content").text(banner_text); + return; + } + + $banner = $( + render_guest_in_dm_recipient_warning({ + banner_text, + classname: compose_banner.CLASSNAMES.guest_in_dm_recipient_warning, + }), + ); + compose_banner.append_compose_banner_to_banner_list($banner, $("#compose_banners")); +} + function show_stream_wildcard_warnings(opts: StreamWildcardOptions): void { const subscriber_count = peer_data.get_subscriber_count(opts.stream_id) || 0; const stream_name = sub_store.maybe_get_stream_name(opts.stream_id); diff --git a/web/src/people.ts b/web/src/people.ts index ce807e65ed..600eb2d129 100644 --- a/web/src/people.ts +++ b/web/src/people.ts @@ -687,6 +687,17 @@ export function pm_with_operand_ids(operand: string): number[] | undefined { return user_ids; } +export function filter_other_guest_ids(user_ids: number[]): number[] { + return sort_numerically( + user_ids.filter((id) => id !== current_user.user_id && get_by_user_id(id)?.is_guest), + ); +} + +export function user_ids_to_full_names_array(user_ids: number[]): string[] { + const names = user_ids.map((user_id) => get_by_user_id(user_id).full_name).sort(util.strcmp); + return names; +} + export function emails_to_slug(emails_string: string): string | undefined { let slug = reply_to_to_user_ids_string(emails_string); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index fb75da1e0e..594bec9d7f 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -16,6 +16,7 @@ import * as compose_closed_ui from "./compose_closed_ui.ts"; import * as compose_pm_pill from "./compose_pm_pill.ts"; import * as compose_recipient from "./compose_recipient.ts"; import * as compose_state from "./compose_state.ts"; +import * as compose_validate from "./compose_validate.ts"; import {electron_bridge} from "./electron_bridge.ts"; import * as emoji from "./emoji.ts"; import * as emoji_picker from "./emoji_picker.ts"; @@ -262,6 +263,7 @@ export function dispatch_normal_event(event) { want_advertise_in_communities_directory: noop, wildcard_mention_policy: noop, enable_read_receipts: settings_account.update_send_read_receipts_tooltip, + enable_guest_user_dm_warning: compose_validate.warn_if_guest_in_dm_recipient, enable_guest_user_indicator: noop, }; switch (event.op) { diff --git a/web/src/state_data.ts b/web/src/state_data.ts index 7f5b6a4521..0732dfefc5 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -337,6 +337,7 @@ export const realm_schema = z.object({ }), ), realm_empty_topic_display_name: z.string(), + realm_enable_guest_user_dm_warning: z.boolean(), realm_enable_guest_user_indicator: z.boolean(), realm_enable_read_receipts: z.boolean(), realm_enable_spectator_access: z.boolean(), diff --git a/web/templates/compose_banner/guest_in_dm_recipient_warning.hbs b/web/templates/compose_banner/guest_in_dm_recipient_warning.hbs new file mode 100644 index 0000000000..3458ce7308 --- /dev/null +++ b/web/templates/compose_banner/guest_in_dm_recipient_warning.hbs @@ -0,0 +1,6 @@ +
+ + +
diff --git a/web/templates/settings/organization_permissions_admin.hbs b/web/templates/settings/organization_permissions_admin.hbs index 185829d2ae..d1c75323e1 100644 --- a/web/templates/settings/organization_permissions_admin.hbs +++ b/web/templates/settings/organization_permissions_admin.hbs @@ -321,6 +321,12 @@ is_checked=realm_enable_guest_user_indicator label=admin_settings_label.realm_enable_guest_user_indicator}} + {{> settings_checkbox + setting_name="realm_enable_guest_user_dm_warning" + prefix="id_" + is_checked=realm_enable_guest_user_dm_warning + label=admin_settings_label.realm_enable_guest_user_dm_warning}} + {{> ../dropdown_widget_with_label widget_name="realm_can_access_all_users_group" label=group_setting_labels.can_access_all_users_group diff --git a/web/tests/compose.test.cjs b/web/tests/compose.test.cjs index 27f8f56c00..363e555eb6 100644 --- a/web/tests/compose.test.cjs +++ b/web/tests/compose.test.cjs @@ -630,6 +630,7 @@ test_ui("update_fade", ({override, override_rewire}) => { update_narrow_to_recipient_visibility_called = true; }); override_rewire(drafts, "update_compose_draft_count", noop); + override(compose_pm_pill, "get_user_ids", () => []); compose_state.set_message_type(undefined); compose_recipient.update_on_recipient_change(); diff --git a/web/tests/compose_actions.test.cjs b/web/tests/compose_actions.test.cjs index de6d78aaa1..e0094f88b3 100644 --- a/web/tests/compose_actions.test.cjs +++ b/web/tests/compose_actions.test.cjs @@ -42,6 +42,7 @@ const compose_fade = mock_esm("../src/compose_fade", { }); const compose_pm_pill = mock_esm("../src/compose_pm_pill", { get_user_ids_string: () => "", + get_user_ids: () => [], }); const compose_ui = mock_esm("../src/compose_ui", { autosize_textarea: noop, diff --git a/web/tests/compose_validate.test.cjs b/web/tests/compose_validate.test.cjs index ea3db1f8e1..2f74ec052e 100644 --- a/web/tests/compose_validate.test.cjs +++ b/web/tests/compose_validate.test.cjs @@ -22,6 +22,11 @@ const {set_current_user, set_realm} = zrequire("state_data"); const stream_data = zrequire("stream_data"); const compose_recipient = zrequire("/compose_recipient"); const user_groups = zrequire("user_groups"); +const {initialize_user_settings} = zrequire("user_settings"); + +mock_esm("../src/ui_util", { + place_caret_at_end: noop, +}); mock_esm("../src/group_permission_settings", { get_group_permission_setting_config: () => ({ @@ -33,6 +38,8 @@ const realm = {}; set_realm(realm); const current_user = {}; set_current_user(current_user); +const user_settings = {defualt_language: "en"}; +initialize_user_settings({user_settings}); const me = { email: "me@example.com", @@ -54,6 +61,13 @@ const bob = { is_admin: true, }; +const guest = { + email: "guest@example.com", + user_id: 33, + full_name: "Guest", + is_guest: true, +}; + const social_sub = { stream_id: 101, name: "social", @@ -66,6 +80,7 @@ people.initialize_current_user(me.user_id); people.add_active_user(alice); people.add_active_user(bob); +people.add_active_user(guest); const welcome_bot = { email: "welcome-bot@example.com", @@ -116,6 +131,30 @@ function stub_message_row($textarea) { }; } +function initialize_pm_pill(mock_template) { + $.clear_all_elements(); + + $("#compose-send-button").prop("disabled", false); + $("#compose-send-button").trigger("focus"); + $("#compose-send-button .loader").hide(); + + const $pm_pill_container = $.create("fake-pm-pill-container"); + $("#private_message_recipient")[0] = {}; + $("#private_message_recipient").set_parent($pm_pill_container); + $pm_pill_container.set_find_results(".input", $("#private_message_recipient")); + $("#private_message_recipient").before = noop; + + compose_pm_pill.initialize({ + on_pill_create_or_remove: compose_recipient.update_placeholder_text, + }); + + $("#zephyr-mirror-error").is = noop; + + mock_template("input_pill.hbs", false, () => "
pill-html
"); + + mock_banners(); +} + test_ui("validate_stream_message_address_info", ({mock_template}) => { // For this test we basically only use FakeComposeBox // to set up the DOM environment. We don't assert about @@ -153,30 +192,6 @@ test_ui("validate_stream_message_address_info", ({mock_template}) => { }); test_ui("validate", ({mock_template, override}) => { - function initialize_pm_pill() { - $.clear_all_elements(); - - $("#compose-send-button").prop("disabled", false); - $("#compose-send-button").trigger("focus"); - $("#compose-send-button .loader").hide(); - - const $pm_pill_container = $.create("fake-pm-pill-container"); - $("#private_message_recipient")[0] = {}; - $("#private_message_recipient").set_parent($pm_pill_container); - $pm_pill_container.set_find_results(".input", $("#private_message_recipient")); - $("#private_message_recipient").before = noop; - - compose_pm_pill.initialize({ - on_pill_create_or_remove: compose_recipient.update_placeholder_text, - }); - - $("#zephyr-mirror-error").is = noop; - - mock_template("input_pill.hbs", false, () => "
pill-html
"); - - mock_banners(); - } - function add_content_to_compose_box() { $("textarea#compose-textarea").val("foobarfoobar"); } @@ -184,7 +199,7 @@ test_ui("validate", ({mock_template, override}) => { // test validating direct messages compose_state.set_message_type("private"); - initialize_pm_pill(); + initialize_pm_pill(mock_template); add_content_to_compose_box(); compose_state.private_message_recipient(""); let pm_recipient_error_rendered = false; @@ -238,7 +253,7 @@ test_ui("validate", ({mock_template, override}) => { assert.ok(compose_validate.validate()); override(realm, "realm_is_zephyr_mirror_realm", false); - initialize_pm_pill(); + initialize_pm_pill(mock_template); add_content_to_compose_box(); compose_state.private_message_recipient("welcome-bot@example.com"); $("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea")); @@ -258,7 +273,7 @@ test_ui("validate", ({mock_template, override}) => { } return ""; }); - initialize_pm_pill(); + initialize_pm_pill(mock_template); compose_state.private_message_recipient("welcome-bot@example.com"); $("textarea#compose-textarea").toggleClass = (classname, value) => { assert.equal(classname, "invalid"); @@ -281,7 +296,7 @@ test_ui("validate", ({mock_template, override}) => { assert.ok(zephyr_checked); assert.ok(zephyr_error_rendered); - initialize_pm_pill(); + initialize_pm_pill(mock_template); add_content_to_compose_box(); // test validating stream messages @@ -842,3 +857,68 @@ test_ui("test warn_if_topic_resolved", ({override, mock_template}) => { compose_validate.warn_if_topic_resolved(false); assert.ok(!error_shown); }); + +test_ui("test_warn_if_guest_in_dm_recipient", ({mock_template}) => { + let is_active = false; + + mock_template("compose_banner/guest_in_dm_recipient_warning.hbs", false, (data) => { + assert.equal(data.classname, compose_banner.CLASSNAMES.guest_in_dm_recipient_warning); + assert.equal( + data.banner_text, + $t({defaultMessage: "Guest is a guest in this organization."}), + ); + is_active = true; + return ""; + }); + + compose_state.set_message_type("private"); + initialize_pm_pill(mock_template); + compose_state.private_message_recipient("guest@example.com"); + const classname = compose_banner.CLASSNAMES.guest_in_dm_recipient_warning; + let $banner = $(`#compose_banners .${CSS.escape(classname)}`); + + // if setting is disabled, remove warning if exists + realm.realm_enable_guest_user_dm_warning = false; + compose_validate.warn_if_guest_in_dm_recipient(); + assert.ok(!is_active); + + // to show warning for guest emails, banner should be created + realm.realm_enable_guest_user_dm_warning = true; + $banner.length = 0; + compose_validate.warn_if_guest_in_dm_recipient(); + assert.ok(is_active); + assert.deepEqual(compose_state.get_recipient_guest_ids_for_dm_warning(), [33]); + + // don't show warning for same guests if user closed the banner. + is_active = false; + compose_validate.warn_if_guest_in_dm_recipient(); + assert.ok(!is_active); + + // on modifying the guest recipient, update banner if already shown. + is_active = true; + const new_guest = { + email: "new_guest@example.com", + user_id: 34, + full_name: "New Guest", + is_guest: true, + }; + people.add_active_user(new_guest); + + initialize_pm_pill(mock_template); + compose_state.private_message_recipient("guest@example.com, new_guest@example.com"); + $banner = $(`#compose_banners .${CSS.escape(classname)}`); + $banner.length = 1; + let is_updated = false; + $banner.set_find_results(".banner_content", { + text(content) { + assert.equal( + content, + $t({defaultMessage: "Guest and New Guest are guests in this organization."}), + ); + is_updated = true; + }, + }); + compose_validate.warn_if_guest_in_dm_recipient(); + assert.ok(is_updated); + assert.deepEqual(compose_state.get_recipient_guest_ids_for_dm_warning(), [33, 34]); +}); diff --git a/web/tests/people.test.cjs b/web/tests/people.test.cjs index f728a1fae3..c13009e550 100644 --- a/web/tests/people.test.cjs +++ b/web/tests/people.test.cjs @@ -835,6 +835,45 @@ test_people("dm_matches_search_string", () => { assert.ok(!result); }); +test_people("filter_other_guest_ids", ({override}) => { + people.add_active_user(emp401); + people.add_active_user(emp402); + people.add_active_user(guest); + + assert.equal(emp401.user_id, 401); + assert.equal(emp402.user_id, 402); + assert.equal(guest.user_id, 33); + + let ids = [401, 402]; + let guest_ids = people.filter_other_guest_ids(ids); + assert.equal(guest_ids.length, 0); + + ids = [401, 402, 33]; + guest_ids = people.filter_other_guest_ids(ids); + assert.equal(guest_ids.length, 1); + assert.equal(guest_ids[0], 33); + + override(current_user, "is_guest", true); + override(current_user, "user_id", 1); + // User ID of the current user will not be included in result. + guest_ids = people.filter_other_guest_ids([1]); + assert.equal(guest_ids.length, 0); +}); + +test_people("user_ids_to_full_names_array", () => { + people.add_active_user(emp401); + people.add_active_user(emp402); + + assert.equal(emp401.user_id, 401); + assert.equal(emp402.user_id, 402); + + const ids = [401, 402]; + const names = people.user_ids_to_full_names_array(ids); + assert.equal(names.length, 2); + assert.equal(names[0], emp401.full_name); + assert.equal(names[1], emp402.full_name); +}); + test_people("multi_user_methods", () => { people.add_active_user(emp401); people.add_active_user(emp402); diff --git a/zerver/migrations/0663_realm_enable_guest_user_dm_warning.py b/zerver/migrations/0663_realm_enable_guest_user_dm_warning.py new file mode 100644 index 0000000000..27ea6546d5 --- /dev/null +++ b/zerver/migrations/0663_realm_enable_guest_user_dm_warning.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.10 on 2025-02-06 13:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0662_clear_realm_channel_fields_if_configured_channel_deactivated"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="enable_guest_user_dm_warning", + field=models.BooleanField(default=True), + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 1b6d88a59b..3056710663 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -627,6 +627,9 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub # Whether clients should display "(guest)" after names of guest users. enable_guest_user_indicator = models.BooleanField(default=True) + # Whether to notify client when a DM has a guest recipient. + enable_guest_user_dm_warning = models.BooleanField(default=True) + # Define the types of the various automatically managed properties property_types: dict[str, type | UnionType] = dict( allow_edit_history=bool, @@ -640,6 +643,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub disallow_disposable_email_addresses=bool, email_changes_disabled=bool, emails_restricted_to_domains=bool, + enable_guest_user_dm_warning=bool, enable_guest_user_indicator=bool, enable_read_receipts=bool, enable_spectator_access=bool, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index b49a760a97..569238f7f4 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4748,6 +4748,13 @@ paths: Whether [new users joining](/help/restrict-account-creation#configuring-email-domain-restrictions) this organization are required to have an email address in one of the `realm_domains` configured for the organization. + enable_guest_user_dm_warning: + type: boolean + description: | + Whether clients should show a warning when a user is composing + a DM to a guest user in this organization. + + **Changes**: New in Zulip 10.0 (feature level 348). enable_guest_user_indicator: type: boolean description: | @@ -17609,6 +17616,15 @@ paths: - 2 = Zulip Cloud free plan (LIMITED) - 3 = Zulip Cloud Standard plan (STANDARD) - 4 = Zulip Cloud Standard plan, sponsored for free (STANDARD_FREE) + realm_enable_guest_user_dm_warning: + type: boolean + description: | + Present if `realm` is present in `fetch_event_types`. + + Whether clients should show a warning when a user is composing + a DM to a guest user in this organization. + + **Changes**: New in Zulip 10.0 (feature level 348). realm_enable_guest_user_indicator: type: boolean description: | diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index f550dbb62b..f0bea67548 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -165,6 +165,7 @@ class HomeTest(ZulipTestCase): "realm_embedded_bots", "realm_emoji", "realm_empty_topic_display_name", + "realm_enable_guest_user_dm_warning", "realm_enable_guest_user_indicator", "realm_enable_read_receipts", "realm_enable_spectator_access", diff --git a/zerver/views/realm.py b/zerver/views/realm.py index ca00ac09cc..de89067e47 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -163,6 +163,7 @@ def update_realm( Json[int | str] | None, ApiParamConfig("move_messages_between_streams_limit_seconds"), ] = None, + enable_guest_user_dm_warning: Json[bool] | None = None, enable_guest_user_indicator: Json[bool] | None = None, can_access_all_users_group: Json[GroupSettingChangeRequest] | None = None, ) -> HttpResponse: