mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +00:00 
			
		
		
		
	realm: Add setting to notify user on DMing guest.
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. Fixes #30078. Co-authored-by: adnan-td <generaladnan139@gmail.com>
This commit is contained in:
		| @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. | |||||||
|  |  | ||||||
| ## Changes in Zulip 10.0 | ## 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** | **Feature level 347** | ||||||
|  |  | ||||||
| * [Markdown message formatting](/api/message-formatting#links-to-channels-topics-and-messages): | * [Markdown message formatting](/api/message-formatting#links-to-channels-topics-and-messages): | ||||||
|   | |||||||
| @@ -42,6 +42,20 @@ pricing](/help/zulip-cloud-billing#temporary-users-and-guests) for guest users. | |||||||
|  |  | ||||||
| {end_tabs} | {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 | ## Configure whether guests can see all other users | ||||||
|  |  | ||||||
| {!cloud-plus-only.md!} | {!cloud-plus-only.md!} | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" | |||||||
| # new level means in api_docs/changelog.md, as well as "**Changes**" | # new level means in api_docs/changelog.md, as well as "**Changes**" | ||||||
| # entries in the endpoint's documentation in `zulip.yaml`. | # 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 | # 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 | # only when going from an old version of the code to a newer version. Bump | ||||||
|   | |||||||
| @@ -73,6 +73,10 @@ const admin_settings_label = { | |||||||
|     realm_enable_guest_user_indicator: $t({ |     realm_enable_guest_user_indicator: $t({ | ||||||
|         defaultMessage: "Display “(guest)” after names of guest users", |         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 { | function insert_tip_box(): void { | ||||||
| @@ -246,6 +250,7 @@ export function build_page(): void { | |||||||
|         automatically_unmute_topics_in_muted_streams_policy_values: |         automatically_unmute_topics_in_muted_streams_policy_values: | ||||||
|             settings_config.automatically_follow_or_unmute_topics_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_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, |         active_user_list_dropdown_widget_name: settings_users.active_user_list_dropdown_widget_name, | ||||||
|         deactivated_user_list_dropdown_widget_name: |         deactivated_user_list_dropdown_widget_name: | ||||||
|             settings_users.deactivated_user_list_dropdown_widget_name, |             settings_users.deactivated_user_list_dropdown_widget_name, | ||||||
|   | |||||||
| @@ -127,6 +127,7 @@ function clear_box(): void { | |||||||
|     // TODO: Better encapsulate at-mention warnings. |     // TODO: Better encapsulate at-mention warnings. | ||||||
|     compose_validate.clear_topic_resolved_warning(); |     compose_validate.clear_topic_resolved_warning(); | ||||||
|     compose_validate.clear_stream_wildcard_warnings($("#compose_banners")); |     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_validate.set_user_acknowledged_stream_wildcard_flag(false); | ||||||
|  |  | ||||||
|     compose_state.set_recipient_edited_manually(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 |     // Show a warning if topic is resolved | ||||||
|     compose_validate.warn_if_topic_resolved(true); |     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 |     // Show a warning if the user is in a search narrow when replying to a message | ||||||
|     if (opts.is_reply) { |     if (opts.is_reply) { | ||||||
|         compose_validate.warn_if_in_search_view(); |         compose_validate.warn_if_in_search_view(); | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ export const CLASSNAMES = { | |||||||
|     recipient_not_subscribed: "recipient_not_subscribed", |     recipient_not_subscribed: "recipient_not_subscribed", | ||||||
|     wildcard_warning: "wildcard_warning", |     wildcard_warning: "wildcard_warning", | ||||||
|     private_stream_warning: "private_stream_warning", |     private_stream_warning: "private_stream_warning", | ||||||
|  |     guest_in_dm_recipient_warning: "guest_in_dm_recipient_warning", | ||||||
|     unscheduled_message: "unscheduled_message", |     unscheduled_message: "unscheduled_message", | ||||||
|     search_view: "search_view", |     search_view: "search_view", | ||||||
|     // errors |     // errors | ||||||
|   | |||||||
| @@ -114,6 +114,7 @@ function update_fade(): void { | |||||||
| export function update_on_recipient_change(): void { | export function update_on_recipient_change(): void { | ||||||
|     update_fade(); |     update_fade(); | ||||||
|     update_narrow_to_recipient_visibility(); |     update_narrow_to_recipient_visibility(); | ||||||
|  |     compose_validate.warn_if_guest_in_dm_recipient(); | ||||||
|     drafts.update_compose_draft_count(); |     drafts.update_compose_draft_count(); | ||||||
|     check_posting_policy_for_compose_box(); |     check_posting_policy_for_compose_box(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | // the narrow and the user should still be able to see the banner once after | ||||||
| // performing these actions | // performing these actions | ||||||
| let recipient_viewed_topic_resolved_banner = false; | let recipient_viewed_topic_resolved_banner = false; | ||||||
|  | let recipient_guest_ids_for_dm_warning: number[] = []; | ||||||
|  |  | ||||||
| export function set_recipient_edited_manually(flag: boolean): void { | export function set_recipient_edited_manually(flag: boolean): void { | ||||||
|     recipient_edited_manually = flag; |     recipient_edited_manually = flag; | ||||||
| @@ -58,6 +59,14 @@ export function has_recipient_viewed_topic_resolved_banner(): boolean { | |||||||
|     return recipient_viewed_topic_resolved_banner; |     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 { | export function composing(): boolean { | ||||||
|     // This is very similar to get_message_type(), but it returns |     // This is very similar to get_message_type(), but it returns | ||||||
|     // a boolean. |     // a boolean. | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| import $ from "jquery"; | import $ from "jquery"; | ||||||
|  | import _ from "lodash"; | ||||||
|  |  | ||||||
| import * as resolved_topic from "../shared/src/resolved_topic.ts"; | import * as resolved_topic from "../shared/src/resolved_topic.ts"; | ||||||
| import render_compose_banner from "../templates/compose_banner/compose_banner.hbs"; | 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_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_private_stream_warning from "../templates/compose_banner/private_stream_warning.hbs"; | ||||||
| import render_stream_wildcard_warning from "../templates/compose_banner/stream_wildcard_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 { | function show_stream_wildcard_warnings(opts: StreamWildcardOptions): void { | ||||||
|     const subscriber_count = peer_data.get_subscriber_count(opts.stream_id) || 0; |     const subscriber_count = peer_data.get_subscriber_count(opts.stream_id) || 0; | ||||||
|     const stream_name = sub_store.maybe_get_stream_name(opts.stream_id); |     const stream_name = sub_store.maybe_get_stream_name(opts.stream_id); | ||||||
|   | |||||||
| @@ -687,6 +687,17 @@ export function pm_with_operand_ids(operand: string): number[] | undefined { | |||||||
|     return user_ids; |     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 { | export function emails_to_slug(emails_string: string): string | undefined { | ||||||
|     let slug = reply_to_to_user_ids_string(emails_string); |     let slug = reply_to_to_user_ids_string(emails_string); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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_pm_pill from "./compose_pm_pill.ts"; | ||||||
| import * as compose_recipient from "./compose_recipient.ts"; | import * as compose_recipient from "./compose_recipient.ts"; | ||||||
| import * as compose_state from "./compose_state.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 {electron_bridge} from "./electron_bridge.ts"; | ||||||
| import * as emoji from "./emoji.ts"; | import * as emoji from "./emoji.ts"; | ||||||
| import * as emoji_picker from "./emoji_picker.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, |                 want_advertise_in_communities_directory: noop, | ||||||
|                 wildcard_mention_policy: noop, |                 wildcard_mention_policy: noop, | ||||||
|                 enable_read_receipts: settings_account.update_send_read_receipts_tooltip, |                 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, |                 enable_guest_user_indicator: noop, | ||||||
|             }; |             }; | ||||||
|             switch (event.op) { |             switch (event.op) { | ||||||
|   | |||||||
| @@ -337,6 +337,7 @@ export const realm_schema = z.object({ | |||||||
|         }), |         }), | ||||||
|     ), |     ), | ||||||
|     realm_empty_topic_display_name: z.string(), |     realm_empty_topic_display_name: z.string(), | ||||||
|  |     realm_enable_guest_user_dm_warning: z.boolean(), | ||||||
|     realm_enable_guest_user_indicator: z.boolean(), |     realm_enable_guest_user_indicator: z.boolean(), | ||||||
|     realm_enable_read_receipts: z.boolean(), |     realm_enable_read_receipts: z.boolean(), | ||||||
|     realm_enable_spectator_access: z.boolean(), |     realm_enable_spectator_access: z.boolean(), | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <div class="above_compose_banner main-view-banner warning-style {{classname}}"> | ||||||
|  |     <p class="banner_content"> | ||||||
|  |         {{banner_text}} | ||||||
|  |     </p> | ||||||
|  |     <a role="button" class="zulip-icon zulip-icon-close main-view-banner-close-button"></a> | ||||||
|  | </div> | ||||||
| @@ -321,6 +321,12 @@ | |||||||
|                   is_checked=realm_enable_guest_user_indicator |                   is_checked=realm_enable_guest_user_indicator | ||||||
|                   label=admin_settings_label.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 |                 {{> ../dropdown_widget_with_label | ||||||
|                   widget_name="realm_can_access_all_users_group" |                   widget_name="realm_can_access_all_users_group" | ||||||
|                   label=group_setting_labels.can_access_all_users_group |                   label=group_setting_labels.can_access_all_users_group | ||||||
|   | |||||||
| @@ -630,6 +630,7 @@ test_ui("update_fade", ({override, override_rewire}) => { | |||||||
|         update_narrow_to_recipient_visibility_called = true; |         update_narrow_to_recipient_visibility_called = true; | ||||||
|     }); |     }); | ||||||
|     override_rewire(drafts, "update_compose_draft_count", noop); |     override_rewire(drafts, "update_compose_draft_count", noop); | ||||||
|  |     override(compose_pm_pill, "get_user_ids", () => []); | ||||||
|  |  | ||||||
|     compose_state.set_message_type(undefined); |     compose_state.set_message_type(undefined); | ||||||
|     compose_recipient.update_on_recipient_change(); |     compose_recipient.update_on_recipient_change(); | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ const compose_fade = mock_esm("../src/compose_fade", { | |||||||
| }); | }); | ||||||
| const compose_pm_pill = mock_esm("../src/compose_pm_pill", { | const compose_pm_pill = mock_esm("../src/compose_pm_pill", { | ||||||
|     get_user_ids_string: () => "", |     get_user_ids_string: () => "", | ||||||
|  |     get_user_ids: () => [], | ||||||
| }); | }); | ||||||
| const compose_ui = mock_esm("../src/compose_ui", { | const compose_ui = mock_esm("../src/compose_ui", { | ||||||
|     autosize_textarea: noop, |     autosize_textarea: noop, | ||||||
|   | |||||||
| @@ -22,6 +22,11 @@ const {set_current_user, set_realm} = zrequire("state_data"); | |||||||
| const stream_data = zrequire("stream_data"); | const stream_data = zrequire("stream_data"); | ||||||
| const compose_recipient = zrequire("/compose_recipient"); | const compose_recipient = zrequire("/compose_recipient"); | ||||||
| const user_groups = zrequire("user_groups"); | 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", { | mock_esm("../src/group_permission_settings", { | ||||||
|     get_group_permission_setting_config: () => ({ |     get_group_permission_setting_config: () => ({ | ||||||
| @@ -33,6 +38,8 @@ const realm = {}; | |||||||
| set_realm(realm); | set_realm(realm); | ||||||
| const current_user = {}; | const current_user = {}; | ||||||
| set_current_user(current_user); | set_current_user(current_user); | ||||||
|  | const user_settings = {defualt_language: "en"}; | ||||||
|  | initialize_user_settings({user_settings}); | ||||||
|  |  | ||||||
| const me = { | const me = { | ||||||
|     email: "me@example.com", |     email: "me@example.com", | ||||||
| @@ -54,6 +61,13 @@ const bob = { | |||||||
|     is_admin: true, |     is_admin: true, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const guest = { | ||||||
|  |     email: "guest@example.com", | ||||||
|  |     user_id: 33, | ||||||
|  |     full_name: "Guest", | ||||||
|  |     is_guest: true, | ||||||
|  | }; | ||||||
|  |  | ||||||
| const social_sub = { | const social_sub = { | ||||||
|     stream_id: 101, |     stream_id: 101, | ||||||
|     name: "social", |     name: "social", | ||||||
| @@ -66,6 +80,7 @@ people.initialize_current_user(me.user_id); | |||||||
|  |  | ||||||
| people.add_active_user(alice); | people.add_active_user(alice); | ||||||
| people.add_active_user(bob); | people.add_active_user(bob); | ||||||
|  | people.add_active_user(guest); | ||||||
|  |  | ||||||
| const welcome_bot = { | const welcome_bot = { | ||||||
|     email: "welcome-bot@example.com", |     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, () => "<div>pill-html</div>"); | ||||||
|  |  | ||||||
|  |     mock_banners(); | ||||||
|  | } | ||||||
|  |  | ||||||
| test_ui("validate_stream_message_address_info", ({mock_template}) => { | test_ui("validate_stream_message_address_info", ({mock_template}) => { | ||||||
|     // For this test we basically only use FakeComposeBox |     // For this test we basically only use FakeComposeBox | ||||||
|     // to set up the DOM environment. We don't assert about |     // 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}) => { | 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, () => "<div>pill-html</div>"); |  | ||||||
|  |  | ||||||
|         mock_banners(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function add_content_to_compose_box() { |     function add_content_to_compose_box() { | ||||||
|         $("textarea#compose-textarea").val("foobarfoobar"); |         $("textarea#compose-textarea").val("foobarfoobar"); | ||||||
|     } |     } | ||||||
| @@ -184,7 +199,7 @@ test_ui("validate", ({mock_template, override}) => { | |||||||
|     // test validating direct messages |     // test validating direct messages | ||||||
|     compose_state.set_message_type("private"); |     compose_state.set_message_type("private"); | ||||||
|  |  | ||||||
|     initialize_pm_pill(); |     initialize_pm_pill(mock_template); | ||||||
|     add_content_to_compose_box(); |     add_content_to_compose_box(); | ||||||
|     compose_state.private_message_recipient(""); |     compose_state.private_message_recipient(""); | ||||||
|     let pm_recipient_error_rendered = false; |     let pm_recipient_error_rendered = false; | ||||||
| @@ -238,7 +253,7 @@ test_ui("validate", ({mock_template, override}) => { | |||||||
|     assert.ok(compose_validate.validate()); |     assert.ok(compose_validate.validate()); | ||||||
|     override(realm, "realm_is_zephyr_mirror_realm", false); |     override(realm, "realm_is_zephyr_mirror_realm", false); | ||||||
|  |  | ||||||
|     initialize_pm_pill(); |     initialize_pm_pill(mock_template); | ||||||
|     add_content_to_compose_box(); |     add_content_to_compose_box(); | ||||||
|     compose_state.private_message_recipient("welcome-bot@example.com"); |     compose_state.private_message_recipient("welcome-bot@example.com"); | ||||||
|     $("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea")); |     $("#send_message_form").set_find_results(".message-textarea", $("textarea#compose-textarea")); | ||||||
| @@ -258,7 +273,7 @@ test_ui("validate", ({mock_template, override}) => { | |||||||
|         } |         } | ||||||
|         return "<banner-stub>"; |         return "<banner-stub>"; | ||||||
|     }); |     }); | ||||||
|     initialize_pm_pill(); |     initialize_pm_pill(mock_template); | ||||||
|     compose_state.private_message_recipient("welcome-bot@example.com"); |     compose_state.private_message_recipient("welcome-bot@example.com"); | ||||||
|     $("textarea#compose-textarea").toggleClass = (classname, value) => { |     $("textarea#compose-textarea").toggleClass = (classname, value) => { | ||||||
|         assert.equal(classname, "invalid"); |         assert.equal(classname, "invalid"); | ||||||
| @@ -281,7 +296,7 @@ test_ui("validate", ({mock_template, override}) => { | |||||||
|     assert.ok(zephyr_checked); |     assert.ok(zephyr_checked); | ||||||
|     assert.ok(zephyr_error_rendered); |     assert.ok(zephyr_error_rendered); | ||||||
|  |  | ||||||
|     initialize_pm_pill(); |     initialize_pm_pill(mock_template); | ||||||
|     add_content_to_compose_box(); |     add_content_to_compose_box(); | ||||||
|  |  | ||||||
|     // test validating stream messages |     // 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); |     compose_validate.warn_if_topic_resolved(false); | ||||||
|     assert.ok(!error_shown); |     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 "<banner-stub>"; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     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]); | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -835,6 +835,45 @@ test_people("dm_matches_search_string", () => { | |||||||
|     assert.ok(!result); |     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", () => { | test_people("multi_user_methods", () => { | ||||||
|     people.add_active_user(emp401); |     people.add_active_user(emp401); | ||||||
|     people.add_active_user(emp402); |     people.add_active_user(emp402); | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								zerver/migrations/0663_realm_enable_guest_user_dm_warning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								zerver/migrations/0663_realm_enable_guest_user_dm_warning.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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. |     # Whether clients should display "(guest)" after names of guest users. | ||||||
|     enable_guest_user_indicator = models.BooleanField(default=True) |     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 |     # Define the types of the various automatically managed properties | ||||||
|     property_types: dict[str, type | UnionType] = dict( |     property_types: dict[str, type | UnionType] = dict( | ||||||
|         allow_edit_history=bool, |         allow_edit_history=bool, | ||||||
| @@ -640,6 +643,7 @@ class Realm(models.Model):  # type: ignore[django-manager-missing] # django-stub | |||||||
|         disallow_disposable_email_addresses=bool, |         disallow_disposable_email_addresses=bool, | ||||||
|         email_changes_disabled=bool, |         email_changes_disabled=bool, | ||||||
|         emails_restricted_to_domains=bool, |         emails_restricted_to_domains=bool, | ||||||
|  |         enable_guest_user_dm_warning=bool, | ||||||
|         enable_guest_user_indicator=bool, |         enable_guest_user_indicator=bool, | ||||||
|         enable_read_receipts=bool, |         enable_read_receipts=bool, | ||||||
|         enable_spectator_access=bool, |         enable_spectator_access=bool, | ||||||
|   | |||||||
| @@ -4748,6 +4748,13 @@ paths: | |||||||
|                                         Whether [new users joining](/help/restrict-account-creation#configuring-email-domain-restrictions) |                                         Whether [new users joining](/help/restrict-account-creation#configuring-email-domain-restrictions) | ||||||
|                                         this organization are required to have an email |                                         this organization are required to have an email | ||||||
|                                         address in one of the `realm_domains` configured for the organization. |                                         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: |                                     enable_guest_user_indicator: | ||||||
|                                       type: boolean |                                       type: boolean | ||||||
|                                       description: | |                                       description: | | ||||||
| @@ -17609,6 +17616,15 @@ paths: | |||||||
|                           - 2 = Zulip Cloud free plan (LIMITED) |                           - 2 = Zulip Cloud free plan (LIMITED) | ||||||
|                           - 3 = Zulip Cloud Standard plan (STANDARD) |                           - 3 = Zulip Cloud Standard plan (STANDARD) | ||||||
|                           - 4 = Zulip Cloud Standard plan, sponsored for free (STANDARD_FREE) |                           - 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: |                       realm_enable_guest_user_indicator: | ||||||
|                         type: boolean |                         type: boolean | ||||||
|                         description: | |                         description: | | ||||||
|   | |||||||
| @@ -165,6 +165,7 @@ class HomeTest(ZulipTestCase): | |||||||
|         "realm_embedded_bots", |         "realm_embedded_bots", | ||||||
|         "realm_emoji", |         "realm_emoji", | ||||||
|         "realm_empty_topic_display_name", |         "realm_empty_topic_display_name", | ||||||
|  |         "realm_enable_guest_user_dm_warning", | ||||||
|         "realm_enable_guest_user_indicator", |         "realm_enable_guest_user_indicator", | ||||||
|         "realm_enable_read_receipts", |         "realm_enable_read_receipts", | ||||||
|         "realm_enable_spectator_access", |         "realm_enable_spectator_access", | ||||||
|   | |||||||
| @@ -163,6 +163,7 @@ def update_realm( | |||||||
|         Json[int | str] | None, |         Json[int | str] | None, | ||||||
|         ApiParamConfig("move_messages_between_streams_limit_seconds"), |         ApiParamConfig("move_messages_between_streams_limit_seconds"), | ||||||
|     ] = None, |     ] = None, | ||||||
|  |     enable_guest_user_dm_warning: Json[bool] | None = None, | ||||||
|     enable_guest_user_indicator: Json[bool] | None = None, |     enable_guest_user_indicator: Json[bool] | None = None, | ||||||
|     can_access_all_users_group: Json[GroupSettingChangeRequest] | None = None, |     can_access_all_users_group: Json[GroupSettingChangeRequest] | None = None, | ||||||
| ) -> HttpResponse: | ) -> HttpResponse: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user