settings: Offer hiding presence info from other users.

For privacy-minded folks who don't want to leak the
information of whether they're online, this adds an
option to disable sending presence updates to other
users.

The new settings lies in the "Other notification
settings" section of the "Notification settings"
page, under a "Presence" subheading.

Closes #14798.
This commit is contained in:
Hashir Sarwar 2020-05-01 23:39:26 +05:00 committed by Tim Abbott
parent 3eaa71cef8
commit 55a8e7dff2
13 changed files with 114 additions and 14 deletions

View File

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

View File

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

View File

@ -107,6 +107,15 @@
label=(lookup ../settings_label this)}}
{{/each}}
<h5>{{t "Presence" }}</h5>
{{#each notification_settings.presence_notification_settings}}
{{> settings_checkbox
setting_name=this
is_checked=(lookup ../page_params this)
label=(lookup ../settings_label this)}}
{{/each}}
</div>
</form>
</div>

View File

@ -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**:

View File

@ -33,17 +33,18 @@ they haven't set a status, no status will appear.
There are four possible availabilities:
* **Active** (<span class="indicator green solid"></span>): Zulip is open and in
focus on web, desktop or mobile, or was in the last 140 seconds.
* **Active** (<span class="indicator green solid"></span>): Zulip is
open and in focus on web, desktop or mobile, or was in the last 140
seconds.
* **Idle** (<span class="indicator orange"></span>): Zulip is open on your
computer (either desktop or web), but you are not active.
* **Idle** (<span class="indicator orange"></span>): Zulip is open on
your computer (either desktop or web), but you are not active.
* **Offline** (<span class="indicator grey"></span>): Zulip is not open on
your computer.
* **Offline** (<span class="indicator grey"></span>): Zulip is not
open on your computer.
* **Unavailable** (<span class="indicator grey-line"></span>): You can always
manually set your availability to unavailable.
* **Unavailable** (<span class="indicator grey-line"></span>): You can
always manually set your availability to unavailable.
For [Group PMs](/help/private-messages), a green circle
(<span class="indicator green solid"></span>)
@ -52,8 +53,9 @@ class="indicator green"></span>) means that some are active and some are
not. A white circle (<span class="indicator grey"></span>) 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.

View File

@ -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

View File

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

View File

@ -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:

View File

@ -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.

View File

@ -119,6 +119,7 @@ class HomeTest(ZulipTestCase):
"pm_content_in_desktop_notifications",
"pointer",
"poll_timeout",
"presence_enabled",
"presences",
"prompt_for_invites",
"queue_id",

View File

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

View File

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

View File

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