diff --git a/static/js/settings.js b/static/js/settings.js index 6c2133a362..fbbf5c9b0e 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -30,6 +30,7 @@ function setup_settings_label() { enable_login_emails: i18n.t("Send email notifications for new logins to my account"), message_content_in_email_notifications: i18n.t("Include message content in missed message emails"), realm_name_in_notifications: i18n.t("Include organization name in subject of missed message emails"), + presence_enabled: i18n.t("Display my availability to other users when online"), // display settings dense_mode: i18n.t("Dense mode"), diff --git a/static/js/settings_config.js b/static/js/settings_config.js index aa81d7d9c3..0bd9c4c6b4 100644 --- a/static/js/settings_config.js +++ b/static/js/settings_config.js @@ -216,10 +216,15 @@ const email_notification_settings = [ "realm_name_in_notifications", ]; +const presence_notification_settings = [ + "presence_enabled", +]; + const other_notification_settings = desktop_notification_settings.concat( ["desktop_icon_count_display"], mobile_notification_settings, email_notification_settings, + presence_notification_settings, ["notification_sound"] ); @@ -245,6 +250,7 @@ exports.all_notifications = () => ({ desktop_notification_settings: desktop_notification_settings, mobile_notification_settings: mobile_notification_settings, email_notification_settings: email_notification_settings, + presence_notification_settings: presence_notification_settings, }, show_push_notifications_tooltip: { push_notifications: !page_params.realm_push_notifications_enabled, diff --git a/static/templates/settings/notification_settings.hbs b/static/templates/settings/notification_settings.hbs index e8837a98e2..910e094cf3 100644 --- a/static/templates/settings/notification_settings.hbs +++ b/static/templates/settings/notification_settings.hbs @@ -107,6 +107,15 @@ label=(lookup ../settings_label this)}} {{/each}} +
{{t "Presence" }}
+ + {{#each notification_settings.presence_notification_settings}} + {{> settings_checkbox + setting_name=this + is_checked=(lookup ../page_params this) + label=(lookup ../settings_label this)}} + {{/each}} + diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 69d0393370..fd3a989d61 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -15,6 +15,8 @@ below features are supported. * `zulip_version` and `zulip_feature_level` are always returned in `POST /register`; previously they were only returned if `event_types` included `zulip_version`. +* Added new `presence_enabled` user notification setting; previously + [presence](/help/status-and-availability) was always enabled. **Feature level 2**: diff --git a/templates/zerver/help/status-and-availability.md b/templates/zerver/help/status-and-availability.md index b9f6b9ae6b..5fd561273d 100644 --- a/templates/zerver/help/status-and-availability.md +++ b/templates/zerver/help/status-and-availability.md @@ -33,17 +33,18 @@ they haven't set a status, no status will appear. There are four possible availabilities: -* **Active** (): Zulip is open and in - focus on web, desktop or mobile, or was in the last 140 seconds. +* **Active** (): Zulip is + open and in focus on web, desktop or mobile, or was in the last 140 + seconds. -* **Idle** (): Zulip is open on your - computer (either desktop or web), but you are not active. +* **Idle** (): Zulip is open on + your computer (either desktop or web), but you are not active. -* **Offline** (): Zulip is not open on - your computer. +* **Offline** (): Zulip is not + open on your computer. -* **Unavailable** (): You can always - manually set your availability to unavailable. +* **Unavailable** (): You can + always manually set your availability to unavailable. For [Group PMs](/help/private-messages), a green circle () @@ -52,8 +53,9 @@ class="indicator green">) means that some are active and some are not. A white circle () means that none are active. -You can see when someone was last active by hovering over their name in the -left or right sidebar. +You can see when someone was last recorded as active by hovering over +their name in the left or right sidebar (even if the user is marked as +unavailable). ## Set yourself as unavailable @@ -67,4 +69,25 @@ left or right sidebar. {end_tabs} -This will also obscure whether you were recently active. +## Disable updating availability + +Zulip supports the privacy option of never updating the availability +information for your account. The result is that you will always +appear to other users as **Offline** (or **Unavailable**, if you've +set an appropriate status), regardless of your activity in Zulip. + +With this setting, your "Last active" time displayed to other users in +the UI will be frozen as the time you enabled this setting. + +{start_tabs} + +{settings_tab|notifications} + +1. Under **Other notification settings**, in the **Presence** + subsection, toggle **Display my availability to other users**. + +{end_tabs} + +Note that because this setting works by making your availability to +updating, you'll still appear to other users as active for a few +minutes after disabling updates to your availability. diff --git a/version.py b/version.py index d785789755..fca77a4e64 100644 --- a/version.py +++ b/version.py @@ -29,7 +29,7 @@ DESKTOP_WARNING_VERSION = "5.0.0" # # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md. -API_FEATURE_LEVEL = 2 +API_FEATURE_LEVEL = 3 # 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/zerver/migrations/0280_userprofile_presence_enabled.py b/zerver/migrations/0280_userprofile_presence_enabled.py new file mode 100644 index 0000000000..2943ef785c --- /dev/null +++ b/zerver/migrations/0280_userprofile_presence_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-05-01 16:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0279_message_recipient_subject_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='presence_enabled', + field=models.BooleanField(default=True), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 24376f00c8..bb6caf7c76 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -916,6 +916,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin): enable_digest_emails: bool = models.BooleanField(default=True) enable_login_emails: bool = models.BooleanField(default=True) realm_name_in_notifications: bool = models.BooleanField(default=False) + presence_enabled: bool = models.BooleanField(default=True) # Used for rate-limiting certain automated messages generated by bots last_reminder: Optional[datetime.datetime] = models.DateTimeField(default=None, null=True) @@ -1048,6 +1049,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin): pm_content_in_desktop_notifications=bool, desktop_icon_count_display=int, realm_name_in_notifications=bool, + presence_enabled=bool, ) class Meta: diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 495a87fb6a..142a77ee95 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2897,6 +2897,13 @@ paths: schema: type: boolean example: true + - name: presence_enabled + in: query + description: | + Display the presence status to other users when online. + schema: + type: boolean + example: true responses: '200': description: Success. diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index be34f86ea0..d385da18bc 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -119,6 +119,7 @@ class HomeTest(ZulipTestCase): "pm_content_in_desktop_notifications", "pointer", "poll_timeout", + "presence_enabled", "presences", "prompt_for_invites", "queue_id", diff --git a/zerver/tests/test_presence.py b/zerver/tests/test_presence.py index 911570b5cd..ab31c6c2bd 100644 --- a/zerver/tests/test_presence.py +++ b/zerver/tests/test_presence.py @@ -605,3 +605,33 @@ class GetRealmStatusesTest(ZulipTestCase): self.assert_json_success(result) json = result.json() self.assertEqual(set(json['presences'].keys()), {hamlet.email, othello.email}) + + def test_presence_disabled(self) -> None: + # Disable presence status and test whether the presence + # is reported or not. + othello = self.example_user("othello") + hamlet = self.example_user("hamlet") + othello.presence_enabled = False + hamlet.presence_enabled = True + othello.save(update_fields=['presence_enabled']) + hamlet.save(update_fields=['presence_enabled']) + + result = self.api_post(othello, "/api/v1/users/me/presence", + dict(status='active'), + HTTP_USER_AGENT="ZulipAndroid/1.0") + + result = self.api_post(hamlet, "/api/v1/users/me/presence", + dict(status='idle'), + HTTP_USER_AGENT="ZulipDesktop/1.0") + self.assert_json_success(result) + json = result.json() + + # Othello's presence status is disabled so it won't be reported. + self.assertEqual(set(json['presences'].keys()), {hamlet.email}) + + result = self.api_post(hamlet, "/api/v1/users/me/presence", + dict(status='active', slim_presence='true'), + HTTP_USER_AGENT="ZulipDesktop/1.0") + self.assert_json_success(result) + json = result.json() + self.assertEqual(set(json['presences'].keys()), {str(hamlet.id)}) diff --git a/zerver/views/presence.py b/zerver/views/presence.py index 16b423fd3a..d841e75d9e 100644 --- a/zerver/views/presence.py +++ b/zerver/views/presence.py @@ -85,7 +85,7 @@ def update_active_status_backend(request: HttpRequest, user_profile: UserProfile status_val = UserPresence.status_from_string(status) if status_val is None: raise JsonableError(_("Invalid status: %s") % (status,)) - else: + elif user_profile.presence_enabled: update_user_presence(user_profile, request.client, timezone_now(), status_val, new_user_input) diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index a24967ed44..60ceed8010 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -200,7 +200,8 @@ def json_change_notify_settings( message_content_in_email_notifications: Optional[bool]=REQ(validator=check_bool, default=None), pm_content_in_desktop_notifications: Optional[bool]=REQ(validator=check_bool, default=None), desktop_icon_count_display: Optional[int]=REQ(validator=check_int, default=None), - realm_name_in_notifications: Optional[bool]=REQ(validator=check_bool, default=None) + realm_name_in_notifications: Optional[bool]=REQ(validator=check_bool, default=None), + presence_enabled: Optional[bool]=REQ(validator=check_bool, default=None), ) -> HttpResponse: result = {}