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 = {}