diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 2f556d42e2..957f4fef77 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 9.0 +**Feature level 253** + +* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults), + [`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings): + Added new `receives_typing_notifications` option to allow users to decide whether + to receive typing notification events from other users. + **Feature level 252** * `PATCH /realm/profile_fields/{field_id}`: `name`, `hint`, `display_in_profile_summary`, diff --git a/version.py b/version.py index 426847660d..56870ac30f 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 252 +API_FEATURE_LEVEL = 253 # 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/realm_user_settings_defaults.ts b/web/src/realm_user_settings_defaults.ts index c3c947626e..f7203de664 100644 --- a/web/src/realm_user_settings_defaults.ts +++ b/web/src/realm_user_settings_defaults.ts @@ -36,6 +36,7 @@ export type RealmDefaultSettings = { pm_content_in_desktop_notifications: boolean; presence_enabled: boolean; realm_name_in_email_notifications_policy: number; + receives_typing_notifications: boolean; send_private_typing_notifications: boolean; send_stream_typing_notifications: boolean; starred_message_counts: boolean; diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index f8aa948162..78e5260522 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -706,6 +706,7 @@ export function dispatch_normal_event(event) { "web_escape_navigates_to_home_view", "fluid_layout_width", "high_contrast_mode", + "receives_typing_notifications", "timezone", "twenty_four_hour_time", "translate_emoticons", @@ -801,6 +802,12 @@ export function dispatch_normal_event(event) { if (event.property === "starred_message_counts") { starred_messages_ui.rerender_ui(); } + if ( + event.property === "receives_typing_notifications" && + !user_settings.receives_typing_notifications + ) { + typing_events.disable_typing_notification(); + } if (event.property === "fluid_layout_width") { scroll_bar.set_layout_width(); } diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index c8b40220e5..88d1436712 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -149,6 +149,7 @@ export const get_all_preferences = (): DisplaySettings => ({ "dense_mode", "high_contrast_mode", "starred_message_counts", + "receives_typing_notifications", "fluid_layout_width", ], }, @@ -563,6 +564,7 @@ export const preferences_settings_labels = { ), fluid_layout_width: $t({defaultMessage: "Use full width on wide screens"}), high_contrast_mode: $t({defaultMessage: "High contrast mode"}), + receives_typing_notifications: $t({defaultMessage: "Show when other users are typing"}), starred_message_counts: $t({defaultMessage: "Show counts for starred messages"}), twenty_four_hour_time: $t({defaultMessage: "Time format"}), translate_emoticons: new Handlebars.SafeString( diff --git a/web/src/typing_data.ts b/web/src/typing_data.ts index 1ff81e2bdd..36fc48acf9 100644 --- a/web/src/typing_data.ts +++ b/web/src/typing_data.ts @@ -64,6 +64,14 @@ export function get_topic_typists(stream_id: number, topic: string): number[] { return muted_users.filter_muted_user_ids(typists); } +export function clear_typing_data(): void { + for (const [, timer] of inbound_timer_dict.entries()) { + clearTimeout(timer); + } + inbound_timer_dict.clear(); + typists_dict.clear(); +} + // The next functions aren't pure data, but it is easy // enough to mock the setTimeout/clearTimeout functions. export function clear_inbound_timer(key: string): void { diff --git a/web/src/typing_events.ts b/web/src/typing_events.ts index 6bc413d24a..8e2bb48170 100644 --- a/web/src/typing_events.ts +++ b/web/src/typing_events.ts @@ -146,3 +146,8 @@ export function display_notification(event: TypingEvent): void { }, ); } + +export function disable_typing_notification(): void { + typing_data.clear_typing_data(); + render_notifications_for_narrow(); +} diff --git a/web/src/user_settings.ts b/web/src/user_settings.ts index a819b40079..53f6a86111 100644 --- a/web/src/user_settings.ts +++ b/web/src/user_settings.ts @@ -48,6 +48,7 @@ export type UserSettings = (StreamNotificationSettings & pm_content_in_desktop_notifications: boolean; presence_enabled: boolean; realm_name_in_email_notifications_policy: number; + receives_typing_notifications: boolean; send_private_typing_notifications: boolean; send_read_receipts: boolean; send_stream_typing_notifications: boolean; diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index abec2f0b02..b6b30b95ef 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -1016,6 +1016,17 @@ run_test("user_settings", ({override}) => { dispatch(event); assert_same(user_settings.starred_message_counts, true); + event = event_fixtures.user_settings__receives_typing_notifications; + user_settings.receives_typing_notifications = false; + dispatch(event); + assert_same(user_settings.receives_typing_notifications, true); + + event = event_fixtures.user_settings__receives_typing_notifications_disabled; + override(typing_events, "disable_typing_notification", noop); + user_settings.receives_typing_notifications = true; + dispatch(event); + assert_same(user_settings.receives_typing_notifications, false); + override(scroll_bar, "set_layout_width", noop); event = event_fixtures.user_settings__fluid_layout_width; user_settings.fluid_layout_width = false; diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index 0c89f3e27a..0f078e276a 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -999,6 +999,20 @@ exports.fixtures = { value: true, }, + user_settings__receives_typing_notifications: { + type: "user_settings", + op: "update", + property: "receives_typing_notifications", + value: true, + }, + + user_settings__receives_typing_notifications_disabled: { + type: "user_settings", + op: "update", + property: "receives_typing_notifications", + value: false, + }, + user_settings__starred_message_counts: { type: "user_settings", op: "update", diff --git a/web/tests/typing_data.test.js b/web/tests/typing_data.test.js index f0214a4956..7deb309d59 100644 --- a/web/tests/typing_data.test.js +++ b/web/tests/typing_data.test.js @@ -82,6 +82,12 @@ test("basics", () => { // test duplicate ids in a groups typing_data.add_typist(typing_data.get_direct_message_conversation_key([20, 40, 20]), 20); assert.deepEqual(typing_data.get_group_typists([20, 40]), [20]); + + // test clearing out typing data + typing_data.clear_typing_data(); + assert.deepEqual(typing_data.get_group_typists(), []); + assert.deepEqual(typing_data.get_all_direct_message_typists(), []); + assert.deepEqual(typing_data.get_topic_typists(stream_id, topic), []); }); test("muted_typists_excluded", () => { @@ -181,6 +187,16 @@ test("timers", () => { timer_set: true, }); + // clearing out typing data + kickstart(); + typing_data.clear_typing_data(); + assert.deepEqual(events, { + f: stub_f, + timer_cleared: true, + timer_set: true, + }); + + kickstart(); // first time clearing, we clear clear(); assert.deepEqual(events, { diff --git a/zerver/actions/typing.py b/zerver/actions/typing.py index dbbd0826fa..c6a3a96f40 100644 --- a/zerver/actions/typing.py +++ b/zerver/actions/typing.py @@ -28,7 +28,11 @@ def do_send_typing_notification( ) # Only deliver the notification to active user recipients - user_ids_to_notify = [user.id for user in recipient_user_profiles if user.is_active] + user_ids_to_notify = [ + user.id + for user in recipient_user_profiles + if user.is_active and user.receives_typing_notifications + ] send_event(realm, event, user_ids_to_notify) @@ -91,9 +95,9 @@ def do_send_stream_typing_notification( return user_ids_to_notify = set( - subscriptions_query.exclude(user_profile__long_term_idle=True).values_list( - "user_profile_id", flat=True - ) + subscriptions_query.exclude(user_profile__long_term_idle=True) + .exclude(user_profile__receives_typing_notifications=False) + .values_list("user_profile_id", flat=True) ) send_event(sender.realm, event, user_ids_to_notify) diff --git a/zerver/migrations/0508_realmuserdefault_receives_typing_notifications_and_more.py b/zerver/migrations/0508_realmuserdefault_receives_typing_notifications_and_more.py new file mode 100644 index 0000000000..09bd4a7566 --- /dev/null +++ b/zerver/migrations/0508_realmuserdefault_receives_typing_notifications_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.12 on 2024-04-16 14:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0507_rework_realm_upload_quota_gb"), + ] + + operations = [ + migrations.AddField( + model_name="realmuserdefault", + name="receives_typing_notifications", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="userprofile", + name="receives_typing_notifications", + field=models.BooleanField(default=True), + ), + ] diff --git a/zerver/models/users.py b/zerver/models/users.py index 6638277751..a0abe1e80e 100644 --- a/zerver/models/users.py +++ b/zerver/models/users.py @@ -245,6 +245,9 @@ class UserBaseSettings(models.Model): send_private_typing_notifications = models.BooleanField(default=True) send_read_receipts = models.BooleanField(default=True) + # Whether the user wants to see typing notifications. + receives_typing_notifications = models.BooleanField(default=True) + # Who in the organization has access to users' actual email # addresses. Controls whether the UserProfile.email field is # the same as UserProfile.delivery_email, or is instead a fake @@ -317,6 +320,7 @@ class UserBaseSettings(models.Model): display_emoji_reaction_users=bool, email_address_visibility=int, web_escape_navigates_to_home_view=bool, + receives_typing_notifications=bool, send_private_typing_notifications=bool, send_read_receipts=bool, send_stream_typing_notifications=bool, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 5c5dd02f45..cc7e0bbbab 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -10570,6 +10570,16 @@ paths: messages](/help/star-a-message#display-the-number-of-starred-messages). type: boolean example: true + receives_typing_notifications: + description: | + Whether the user is configured to receive typing notifications from other users. + The server will only deliver typing notifications events to users who for whom this + is enabled. + + **Changes**: New in Zulip 9.0 (feature level 253). Previously, there were + only options to disable sending typing notifications. + type: boolean + example: true fluid_layout_width: description: | Whether to use the [maximum available screen width](/help/enable-full-width-display) @@ -11017,6 +11027,8 @@ paths: contentType: application/json starred_message_counts: contentType: application/json + receives_typing_notifications: + contentType: application/json fluid_layout_width: contentType: application/json high_contrast_mode: @@ -13368,6 +13380,15 @@ paths: description: | Whether clients should display the [number of starred messages](/help/star-a-message#display-the-number-of-starred-messages). + receives_typing_notifications: + type: boolean + description: | + Whether the user is configured to receive typing notifications from + other users. The server will only deliver typing notifications events + to users who for whom this is enabled. + + **Changes**: New in Zulip 9.0 (feature level 253). Previously, there were + only options to disable sending typing notifications. fluid_layout_width: type: boolean description: | @@ -14442,6 +14463,15 @@ paths: client capability and access the `user_settings` object instead. [capabilities]: /api/register-queue#parameter-client_capabilities + receives_typing_notifications: + type: boolean + description: | + Whether the user is configured to receive typing notifications from other + users. The server will only deliver typing notifications events to users who + for whom this is enabled. + + **Changes**: New in Zulip 9.0 (feature level 253). Previously, there were + only options to disable sending typing notifications. enter_sends: deprecated: true type: boolean @@ -15673,6 +15703,15 @@ paths: description: | Whether clients should display the [number of starred messages](/help/star-a-message#display-the-number-of-starred-messages). + receives_typing_notifications: + type: boolean + description: | + Whether the user is configured to receive typing notifications from + other users. The server will only deliver typing notifications events + to users who for whom this is enabled. + + **Changes**: New in Zulip 9.0 (feature level 253). Previously, there were + only options to disable sending typing notifications. fluid_layout_width: type: boolean description: | @@ -16758,6 +16797,19 @@ paths: the `PATCH /settings/display` endpoint. type: boolean example: true + receives_typing_notifications: + description: | + Whether the user is configured to receive typing notifications from other users. + The server will only deliver typing notifications events to users who for whom this + is enabled. + + By default, this is set to true, enabling user to receive typing + notifications from other users. + + **Changes**: New in Zulip 9.0 (feature level 253). Previously, there were only + options to disable sending typing notifications. + type: boolean + example: true fluid_layout_width: description: | Whether to use the [maximum available screen width](/help/enable-full-width-display) @@ -17313,6 +17365,8 @@ paths: contentType: application/json starred_message_counts: contentType: application/json + receives_typing_notifications: + contentType: application/json fluid_layout_width: contentType: application/json high_contrast_mode: diff --git a/zerver/tests/test_typing.py b/zerver/tests/test_typing.py index 71a1d8df1e..21d0021cd4 100644 --- a/zerver/tests/test_typing.py +++ b/zerver/tests/test_typing.py @@ -585,3 +585,36 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): result = self.api_post(sender, "/api/v1/typing", params) self.assert_json_error(result, "User has disabled typing notifications for stream messages") self.assertEqual(events, []) + + def test_typing_notifications_disabled(self) -> None: + sender = self.example_user("hamlet") + stream_name = self.get_streams(sender)[0] + stream_id = self.get_stream_id(stream_name) + topic_name = "Some topic" + + aaron = self.example_user("aaron") + iago = self.example_user("iago") + for user in [aaron, iago]: + self.subscribe(user, stream_name) + + aaron.receives_typing_notifications = False + aaron.save() + + params = dict( + type="stream", + op="start", + stream_id=str(stream_id), + topic=topic_name, + ) + + with self.capture_send_event_calls(expected_num_events=1) as events: + result = self.api_post(sender, "/api/v1/typing", params) + self.assert_json_success(result) + self.assert_length(events, 1) + + event_user_ids = set(events[0]["users"]) + + # Only users who have typing notifications enabled would receive + # notifications. + self.assertNotIn(aaron.id, event_user_ids) + self.assertIn(iago.id, event_user_ids) diff --git a/zerver/views/realm.py b/zerver/views/realm.py index f59feb538a..830d1bb9a2 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -521,6 +521,7 @@ def update_realm_user_settings_defaults( default=None, ), starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, default=None), + receives_typing_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None), web_stream_unreads_count_display_policy: Optional[int] = REQ( json_validator=check_int_in(UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES), default=None, diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index d5475f0552..0f8bd123ad 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -208,6 +208,7 @@ def json_change_settings( default=None, ), starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, default=None), + receives_typing_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None), fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool, default=None), high_contrast_mode: Optional[bool] = REQ(json_validator=check_bool, default=None), color_scheme: Optional[int] = REQ(