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
This commit is contained in:
roanster007 2024-04-12 23:00:29 +05:30 committed by Tim Abbott
parent c7e2e048cb
commit 68b4298d8e
18 changed files with 196 additions and 5 deletions

View File

@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0 ## 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** **Feature level 252**
* `PATCH /realm/profile_fields/{field_id}`: `name`, `hint`, `display_in_profile_summary`, * `PATCH /realm/profile_fields/{field_id}`: `name`, `hint`, `display_in_profile_summary`,

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# 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 = 252 API_FEATURE_LEVEL = 253
# 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

View File

@ -36,6 +36,7 @@ export type RealmDefaultSettings = {
pm_content_in_desktop_notifications: boolean; pm_content_in_desktop_notifications: boolean;
presence_enabled: boolean; presence_enabled: boolean;
realm_name_in_email_notifications_policy: number; realm_name_in_email_notifications_policy: number;
receives_typing_notifications: boolean;
send_private_typing_notifications: boolean; send_private_typing_notifications: boolean;
send_stream_typing_notifications: boolean; send_stream_typing_notifications: boolean;
starred_message_counts: boolean; starred_message_counts: boolean;

View File

@ -706,6 +706,7 @@ export function dispatch_normal_event(event) {
"web_escape_navigates_to_home_view", "web_escape_navigates_to_home_view",
"fluid_layout_width", "fluid_layout_width",
"high_contrast_mode", "high_contrast_mode",
"receives_typing_notifications",
"timezone", "timezone",
"twenty_four_hour_time", "twenty_four_hour_time",
"translate_emoticons", "translate_emoticons",
@ -801,6 +802,12 @@ export function dispatch_normal_event(event) {
if (event.property === "starred_message_counts") { if (event.property === "starred_message_counts") {
starred_messages_ui.rerender_ui(); 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") { if (event.property === "fluid_layout_width") {
scroll_bar.set_layout_width(); scroll_bar.set_layout_width();
} }

View File

@ -149,6 +149,7 @@ export const get_all_preferences = (): DisplaySettings => ({
"dense_mode", "dense_mode",
"high_contrast_mode", "high_contrast_mode",
"starred_message_counts", "starred_message_counts",
"receives_typing_notifications",
"fluid_layout_width", "fluid_layout_width",
], ],
}, },
@ -563,6 +564,7 @@ export const preferences_settings_labels = {
), ),
fluid_layout_width: $t({defaultMessage: "Use full width on wide screens"}), fluid_layout_width: $t({defaultMessage: "Use full width on wide screens"}),
high_contrast_mode: $t({defaultMessage: "High contrast mode"}), 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"}), starred_message_counts: $t({defaultMessage: "Show counts for starred messages"}),
twenty_four_hour_time: $t({defaultMessage: "Time format"}), twenty_four_hour_time: $t({defaultMessage: "Time format"}),
translate_emoticons: new Handlebars.SafeString( translate_emoticons: new Handlebars.SafeString(

View File

@ -64,6 +64,14 @@ export function get_topic_typists(stream_id: number, topic: string): number[] {
return muted_users.filter_muted_user_ids(typists); 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 // The next functions aren't pure data, but it is easy
// enough to mock the setTimeout/clearTimeout functions. // enough to mock the setTimeout/clearTimeout functions.
export function clear_inbound_timer(key: string): void { export function clear_inbound_timer(key: string): void {

View File

@ -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();
}

View File

@ -48,6 +48,7 @@ export type UserSettings = (StreamNotificationSettings &
pm_content_in_desktop_notifications: boolean; pm_content_in_desktop_notifications: boolean;
presence_enabled: boolean; presence_enabled: boolean;
realm_name_in_email_notifications_policy: number; realm_name_in_email_notifications_policy: number;
receives_typing_notifications: boolean;
send_private_typing_notifications: boolean; send_private_typing_notifications: boolean;
send_read_receipts: boolean; send_read_receipts: boolean;
send_stream_typing_notifications: boolean; send_stream_typing_notifications: boolean;

View File

@ -1016,6 +1016,17 @@ run_test("user_settings", ({override}) => {
dispatch(event); dispatch(event);
assert_same(user_settings.starred_message_counts, true); 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); override(scroll_bar, "set_layout_width", noop);
event = event_fixtures.user_settings__fluid_layout_width; event = event_fixtures.user_settings__fluid_layout_width;
user_settings.fluid_layout_width = false; user_settings.fluid_layout_width = false;

View File

@ -999,6 +999,20 @@ exports.fixtures = {
value: true, 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: { user_settings__starred_message_counts: {
type: "user_settings", type: "user_settings",
op: "update", op: "update",

View File

@ -82,6 +82,12 @@ test("basics", () => {
// test duplicate ids in a groups // test duplicate ids in a groups
typing_data.add_typist(typing_data.get_direct_message_conversation_key([20, 40, 20]), 20); 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]); 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", () => { test("muted_typists_excluded", () => {
@ -181,6 +187,16 @@ test("timers", () => {
timer_set: true, 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 // first time clearing, we clear
clear(); clear();
assert.deepEqual(events, { assert.deepEqual(events, {

View File

@ -28,7 +28,11 @@ def do_send_typing_notification(
) )
# Only deliver the notification to active user recipients # 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) send_event(realm, event, user_ids_to_notify)
@ -91,9 +95,9 @@ def do_send_stream_typing_notification(
return return
user_ids_to_notify = set( user_ids_to_notify = set(
subscriptions_query.exclude(user_profile__long_term_idle=True).values_list( subscriptions_query.exclude(user_profile__long_term_idle=True)
"user_profile_id", flat=True .exclude(user_profile__receives_typing_notifications=False)
) .values_list("user_profile_id", flat=True)
) )
send_event(sender.realm, event, user_ids_to_notify) send_event(sender.realm, event, user_ids_to_notify)

View File

@ -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),
),
]

View File

@ -245,6 +245,9 @@ class UserBaseSettings(models.Model):
send_private_typing_notifications = models.BooleanField(default=True) send_private_typing_notifications = models.BooleanField(default=True)
send_read_receipts = 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 # Who in the organization has access to users' actual email
# addresses. Controls whether the UserProfile.email field is # addresses. Controls whether the UserProfile.email field is
# the same as UserProfile.delivery_email, or is instead a fake # the same as UserProfile.delivery_email, or is instead a fake
@ -317,6 +320,7 @@ class UserBaseSettings(models.Model):
display_emoji_reaction_users=bool, display_emoji_reaction_users=bool,
email_address_visibility=int, email_address_visibility=int,
web_escape_navigates_to_home_view=bool, web_escape_navigates_to_home_view=bool,
receives_typing_notifications=bool,
send_private_typing_notifications=bool, send_private_typing_notifications=bool,
send_read_receipts=bool, send_read_receipts=bool,
send_stream_typing_notifications=bool, send_stream_typing_notifications=bool,

View File

@ -10570,6 +10570,16 @@ paths:
messages](/help/star-a-message#display-the-number-of-starred-messages). messages](/help/star-a-message#display-the-number-of-starred-messages).
type: boolean type: boolean
example: true 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: fluid_layout_width:
description: | description: |
Whether to use the [maximum available screen width](/help/enable-full-width-display) Whether to use the [maximum available screen width](/help/enable-full-width-display)
@ -11017,6 +11027,8 @@ paths:
contentType: application/json contentType: application/json
starred_message_counts: starred_message_counts:
contentType: application/json contentType: application/json
receives_typing_notifications:
contentType: application/json
fluid_layout_width: fluid_layout_width:
contentType: application/json contentType: application/json
high_contrast_mode: high_contrast_mode:
@ -13368,6 +13380,15 @@ paths:
description: | description: |
Whether clients should display the [number of starred Whether clients should display the [number of starred
messages](/help/star-a-message#display-the-number-of-starred-messages). 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: fluid_layout_width:
type: boolean type: boolean
description: | description: |
@ -14442,6 +14463,15 @@ paths:
client capability and access the `user_settings` object instead. client capability and access the `user_settings` object instead.
[capabilities]: /api/register-queue#parameter-client_capabilities [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: enter_sends:
deprecated: true deprecated: true
type: boolean type: boolean
@ -15673,6 +15703,15 @@ paths:
description: | description: |
Whether clients should display the [number of starred Whether clients should display the [number of starred
messages](/help/star-a-message#display-the-number-of-starred-messages). 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: fluid_layout_width:
type: boolean type: boolean
description: | description: |
@ -16758,6 +16797,19 @@ paths:
the `PATCH /settings/display` endpoint. the `PATCH /settings/display` endpoint.
type: boolean type: boolean
example: true 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: fluid_layout_width:
description: | description: |
Whether to use the [maximum available screen width](/help/enable-full-width-display) Whether to use the [maximum available screen width](/help/enable-full-width-display)
@ -17313,6 +17365,8 @@ paths:
contentType: application/json contentType: application/json
starred_message_counts: starred_message_counts:
contentType: application/json contentType: application/json
receives_typing_notifications:
contentType: application/json
fluid_layout_width: fluid_layout_width:
contentType: application/json contentType: application/json
high_contrast_mode: high_contrast_mode:

View File

@ -585,3 +585,36 @@ class TestSendTypingNotificationsSettings(ZulipTestCase):
result = self.api_post(sender, "/api/v1/typing", params) result = self.api_post(sender, "/api/v1/typing", params)
self.assert_json_error(result, "User has disabled typing notifications for stream messages") self.assert_json_error(result, "User has disabled typing notifications for stream messages")
self.assertEqual(events, []) 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)

View File

@ -521,6 +521,7 @@ def update_realm_user_settings_defaults(
default=None, default=None,
), ),
starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, 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( web_stream_unreads_count_display_policy: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES), json_validator=check_int_in(UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES),
default=None, default=None,

View File

@ -208,6 +208,7 @@ def json_change_settings(
default=None, default=None,
), ),
starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, 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), fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool, default=None),
high_contrast_mode: 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( color_scheme: Optional[int] = REQ(