From 68b4298d8e27ce2823ee859111519f9b3b958a84 Mon Sep 17 00:00:00 2001 From: roanster007 Date: Fri, 12 Apr 2024 23:00:29 +0530 Subject: [PATCH] settings: Add option to disable seeing typing notifications. This commit adds an option to the advanced section of Preferences settings, that would allow users to choose whether to receive typing notifications from other users. Fixes #29642 --- api_docs/changelog.md | 7 +++ version.py | 2 +- web/src/realm_user_settings_defaults.ts | 1 + web/src/server_events_dispatch.js | 7 +++ web/src/settings_config.ts | 2 + web/src/typing_data.ts | 8 +++ web/src/typing_events.ts | 5 ++ web/src/user_settings.ts | 1 + web/tests/dispatch.test.js | 11 ++++ web/tests/lib/events.js | 14 +++++ web/tests/typing_data.test.js | 16 ++++++ zerver/actions/typing.py | 12 +++-- ..._receives_typing_notifications_and_more.py | 22 ++++++++ zerver/models/users.py | 4 ++ zerver/openapi/zulip.yaml | 54 +++++++++++++++++++ zerver/tests/test_typing.py | 33 ++++++++++++ zerver/views/realm.py | 1 + zerver/views/user_settings.py | 1 + 18 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 zerver/migrations/0508_realmuserdefault_receives_typing_notifications_and_more.py 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(