frontend: Implement 'invisible mode' feature.

Transitions the frontend of the web app to no longer use the
user status `away` field for setting a user's activity status
to be 'unavailable' (which is now a deprecated way to access
a user's `presence_enabled` setting).

Instead we now directly use and update the user's `presence_enabled`
setting for this feature.

Renames frontend code related to the feature to `invisible_mode`
vs `away`.

We lose node test coverage in `user_status.js` because we are now
using `channel.patch` to send these user setting updates to the
server.

Removes the temporary updates to `server_events_dispatch.py` (and
related tests) made in a previous commit, since we no longer have
or need the `away_user_ids` set.
This commit is contained in:
Lauryn Menard
2022-09-21 15:49:36 +02:00
committed by Tim Abbott
parent b2e0b5187d
commit d5b7551f09
18 changed files with 68 additions and 228 deletions

View File

@@ -38,7 +38,6 @@ const presence = zrequire("presence");
const people = zrequire("people");
const buddy_data = zrequire("buddy_data");
const {buddy_list} = zrequire("buddy_list");
const user_status = zrequire("user_status");
const activity = zrequire("activity");
const me = {
@@ -674,17 +673,6 @@ test("initialize", ({override, mock_template}) => {
clear();
});
test("away_status", ({override}) => {
override(pm_list, "update_private_messages", () => {});
override(buddy_list, "insert_or_move", () => {});
assert.ok(!user_status.is_away(alice.user_id));
activity.on_set_away(alice.user_id);
assert.ok(user_status.is_away(alice.user_id));
activity.on_revoke_away(alice.user_id);
assert.ok(!user_status.is_away(alice.user_id));
});
test("electron_bridge", ({override_rewire}) => {
override_rewire(activity, "send_presence_to_server", () => {});

View File

@@ -126,27 +126,35 @@ test("user_circle, level", () => {
set_presence(selma.user_id, "active");
assert.equal(buddy_data.get_user_circle_class(selma.user_id), "user_circle_green");
user_status.set_away(selma.user_id);
assert.equal(buddy_data.level(selma.user_id), 3);
assert.equal(buddy_data.level(selma.user_id), 1);
assert.equal(buddy_data.get_user_circle_class(selma.user_id), "user_circle_empty_line");
user_status.revoke_away(selma.user_id);
assert.equal(buddy_data.get_user_circle_class(selma.user_id), "user_circle_green");
set_presence(selma.user_id, "idle");
assert.equal(buddy_data.get_user_circle_class(selma.user_id), "user_circle_idle");
assert.equal(buddy_data.level(selma.user_id), 2);
set_presence(selma.user_id, "offline");
assert.equal(buddy_data.get_user_circle_class(selma.user_id), "user_circle_empty");
assert.equal(buddy_data.level(selma.user_id), 3);
set_presence(me.user_id, "active");
assert.equal(buddy_data.get_user_circle_class(me.user_id), "user_circle_green");
user_status.set_away(me.user_id);
assert.equal(buddy_data.level(me.user_id), 0);
assert.equal(buddy_data.get_user_circle_class(me.user_id), "user_circle_empty_line");
user_status.revoke_away(me.user_id);
user_settings.presence_enabled = false;
assert.equal(buddy_data.get_user_circle_class(me.user_id), "user_circle_empty");
assert.equal(buddy_data.level(me.user_id), 0);
user_settings.presence_enabled = true;
assert.equal(buddy_data.get_user_circle_class(me.user_id), "user_circle_green");
assert.equal(buddy_data.level(me.user_id), 0);
set_presence(fred.user_id, "idle");
assert.equal(buddy_data.get_user_circle_class(fred.user_id), "user_circle_idle");
assert.equal(buddy_data.level(fred.user_id), 2);
set_presence(fred.user_id, undefined);
assert.equal(buddy_data.get_user_circle_class(fred.user_id), "user_circle_empty");
assert.equal(buddy_data.level(fred.user_id), 3);
});
test("compose fade interactions (streams)", () => {
@@ -262,23 +270,6 @@ test("compose fade interactions (PMs)", () => {
assert.equal(faded(), false);
});
test("buddy_status", () => {
set_presence(selma.user_id, "active");
set_presence(me.user_id, "active");
assert.equal(buddy_data.buddy_status(selma.user_id), "active");
user_status.set_away(selma.user_id);
assert.equal(buddy_data.buddy_status(selma.user_id), "away_them");
user_status.revoke_away(selma.user_id);
assert.equal(buddy_data.buddy_status(selma.user_id), "active");
assert.equal(buddy_data.buddy_status(me.user_id), "active");
user_status.set_away(me.user_id);
assert.equal(buddy_data.buddy_status(me.user_id), "away_me");
user_status.revoke_away(me.user_id);
assert.equal(buddy_data.buddy_status(me.user_id), "active");
});
test("title_data", () => {
add_canned_users();
@@ -448,8 +439,8 @@ test("level", () => {
assert.equal(buddy_data.level(me.user_id), 0);
assert.equal(buddy_data.level(selma.user_id), 1);
user_status.set_away(me.user_id);
user_status.set_away(selma.user_id);
user_settings.presence_enabled = false;
set_presence(selma.user_id, "offline");
// Selma gets demoted to level 3, but "me"
// stays in level 0.
@@ -493,7 +484,7 @@ test("user_last_seen_time_status", ({override}) => {
test("get_items_for_users", () => {
people.add_active_user(alice);
people.add_active_user(fred);
user_status.set_away(alice.user_id);
set_presence(alice.user_id, "offline");
user_settings.emojiset = "google";
user_settings.user_list_style = 2;
const status_emoji_info = {
@@ -535,7 +526,7 @@ test("get_items_for_users", () => {
num_unread: 0,
status_emoji_info,
status_text: undefined,
user_circle_class: "user_circle_empty_line",
user_circle_class: "user_circle_empty",
user_id: 1002,
user_list_style,
},

View File

@@ -900,19 +900,16 @@ run_test("user_settings", ({override}) => {
dispatch(event);
assert_same(user_settings.enter_sends, true);
page_params.user_id = test_user.user_id;
event = event_fixtures.user_settings__presence_disabled;
user_settings.presence_enabled = true;
override(activity, "redraw_user", noop);
dispatch(event);
assert_same(user_settings.presence_enabled, false);
assert_same(user_status.is_away(test_user.user_id), true);
event = event_fixtures.user_settings__presence_enabled;
override(activity, "redraw_user", noop);
dispatch(event);
assert_same(user_settings.presence_enabled, true);
assert_same(user_status.is_away(test_user.user_id), false);
{
event = event_fixtures.user_settings__enable_stream_audible_notifications;
@@ -1019,27 +1016,7 @@ run_test("delete_message", ({override}) => {
});
run_test("user_status", ({override}) => {
let event = event_fixtures.user_status__set_away;
{
const stub = make_stub();
override(activity, "on_set_away", stub.f);
dispatch(event);
assert.equal(stub.num_calls, 1);
const args = stub.get_args("user_id");
assert_same(args.user_id, 55);
}
event = event_fixtures.user_status__revoke_away;
{
const stub = make_stub();
override(activity, "on_revoke_away", stub.f);
dispatch(event);
assert.equal(stub.num_calls, 1);
const args = stub.get_args("user_id");
assert_same(args.user_id, 63);
}
event = event_fixtures.user_status__set_status_emoji;
let event = event_fixtures.user_status__set_status_emoji;
{
const stub = make_stub();
override(activity, "redraw_user", stub.f);

View File

@@ -947,18 +947,6 @@ exports.fixtures = {
value: 2,
},
user_status__revoke_away: {
type: "user_status",
user_id: 63,
away: false,
},
user_status__set_away: {
type: "user_status",
user_id: 55,
away: true,
},
user_status__set_status_emoji: {
type: "user_status",
user_id: test_user.user_id,

View File

@@ -8,7 +8,6 @@ const {run_test} = require("../zjsunit/test");
const unread = mock_esm("../../static/js/unread");
mock_esm("../../static/js/user_status", {
is_away: () => false,
get_status_emoji: () => ({
emoji_code: 20,
}),

View File

@@ -172,8 +172,7 @@ test_ui("sender_hover", ({override, mock_template}) => {
mock_template("user_info_popover_content.hbs", false, (opts) => {
assert.deepEqual(opts, {
can_set_away: false,
can_revoke_away: false,
invisible_mode: false,
can_mute: true,
can_manage_user: false,
can_send_private_message: true,

View File

@@ -4,7 +4,6 @@ const {strict: assert} = require("assert");
const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const blueslip = require("../zjsunit/zblueslip");
const channel = mock_esm("../../static/js/channel");
@@ -37,9 +36,7 @@ emoji.initialize(emoji_params);
function initialize() {
const params = {
user_status: {
1: {away: true, status_text: "in a meeting"},
2: {away: true},
3: {away: true},
1: {status_text: "in a meeting"},
4: {emoji_name: "smiley", emoji_code: "1f603", reaction_type: "unicode_emoji"},
5: {
emoji_name: "deactivated_realm_emoji",
@@ -78,15 +75,6 @@ run_test("basics", () => {
url: "/url/for/991",
});
assert.ok(user_status.is_away(2));
assert.ok(!user_status.is_away(99));
assert.ok(!user_status.is_away(4));
user_status.set_away(4);
assert.ok(user_status.is_away(4));
user_status.revoke_away(4);
assert.ok(!user_status.is_away(4));
assert.equal(user_status.get_status_text(1), "in a meeting");
user_status.set_status_text({
@@ -137,27 +125,9 @@ run_test("server", () => {
assert.equal(sent_data, undefined);
user_status.server_set_away();
assert.deepEqual(sent_data, {
away: true,
status_text: undefined,
emoji_code: undefined,
emoji_name: undefined,
reaction_type: undefined,
});
user_status.server_revoke_away();
assert.deepEqual(sent_data, {
away: false,
status_text: undefined,
emoji_code: undefined,
emoji_name: undefined,
reaction_type: undefined,
});
let called;
user_status.server_update({
user_status.server_update_status({
status_text: "out to lunch",
success: () => {
called = true;
@@ -169,10 +139,6 @@ run_test("server", () => {
});
run_test("defensive checks", () => {
blueslip.expect("error", "need ints for user_id", 2);
user_status.set_away("string");
user_status.revoke_away("string");
assert.throws(
() =>
user_status.set_status_emoji({

View File

@@ -15,7 +15,6 @@ import * as popovers from "./popovers";
import * as presence from "./presence";
import * as ui_util from "./ui_util";
import {UserSearch} from "./user_search";
import * as user_status from "./user_status";
import * as watchdog from "./watchdog";
export let user_cursor;
@@ -259,18 +258,6 @@ export function update_presence_info(user_id, info, server_time) {
pm_list.update_private_messages();
}
export function on_set_away(user_id) {
user_status.set_away(user_id);
redraw_user(user_id);
pm_list.update_private_messages();
}
export function on_revoke_away(user_id) {
user_status.revoke_away(user_id);
redraw_user(user_id);
pm_list.update_private_messages();
}
export function redraw() {
build_user_sidebar();
user_cursor.redraw();

View File

@@ -36,16 +36,13 @@ const fade_config = {
};
export function get_user_circle_class(user_id) {
const status = buddy_status(user_id);
const status = presence.get_status(user_id);
switch (status) {
case "active":
return "user_circle_green";
case "idle":
return "user_circle_idle";
case "away_them":
case "away_me":
return "user_circle_empty_line";
default:
return "user_circle_empty";
}
@@ -57,33 +54,18 @@ export function level(user_id) {
return 0;
}
const status = buddy_status(user_id);
const status = presence.get_status(user_id);
switch (status) {
case "active":
return 1;
case "idle":
return 2;
case "away_them":
return 3;
default:
return 3;
}
}
export function buddy_status(user_id) {
if (user_status.is_away(user_id)) {
if (people.is_my_user_id(user_id)) {
return "away_me";
}
return "away_them";
}
// get active/idle/etc.
return presence.get_status(user_id);
}
export function compare_function(a, b) {
const level_a = level(a);
const level_b = level(b);

View File

@@ -51,6 +51,7 @@ import * as stream_popover from "./stream_popover";
import * as ui_report from "./ui_report";
import * as user_groups from "./user_groups";
import * as user_profile from "./user_profile";
import {user_settings} from "./user_settings";
import * as user_status from "./user_status";
import * as user_status_ui from "./user_status_ui";
import * as util from "./util";
@@ -197,15 +198,10 @@ function render_user_info_popover(
) {
const is_me = people.is_my_user_id(user.user_id);
let can_set_away = false;
let can_revoke_away = false;
let invisible_mode = false;
if (is_me) {
if (user_status.is_away(user.user_id)) {
can_revoke_away = true;
} else {
can_set_away = true;
}
invisible_mode = !user_settings.presence_enabled;
}
const muting_allowed = !is_me && !user.is_bot;
@@ -228,8 +224,7 @@ function render_user_info_popover(
.filter((f) => f.display_in_profile_summary && f.value !== undefined && f.value !== null);
const args = {
can_revoke_away,
can_set_away,
invisible_mode,
can_mute: muting_allowed && !is_muted,
can_manage_user: page_params.is_admin && !is_me,
can_send_private_message:
@@ -974,7 +969,7 @@ export function register_click_handlers() {
$("body").on("click", ".info_popover_actions .clear_status", (e) => {
e.preventDefault();
const me = elem_to_user_id($(e.target).parents("ul"));
user_status.server_update({
user_status.server_update_status({
user_id: me,
status_text: "",
emoji_name: "",
@@ -997,16 +992,16 @@ export function register_click_handlers() {
* relevant part of the Zulip UI, so we don't want preventDefault,
* but we do want to close the modal when you click them. */
$("body").on("click", ".set_away_status", (e) => {
$("body").on("click", ".invisible_mode_turn_on", (e) => {
hide_all();
user_status.server_set_away();
user_status.server_invisible_mode_on();
e.stopPropagation();
e.preventDefault();
});
$("body").on("click", ".revoke_away_status", (e) => {
$("body").on("click", ".invisible_mode_turn_off", (e) => {
hide_all();
user_status.server_revoke_away();
user_status.server_invisible_mode_off();
e.stopPropagation();
e.preventDefault();
});

View File

@@ -723,16 +723,6 @@ export function dispatch_normal_event(event) {
user_settings.presence_enabled = event.value;
$("#user_presence_enabled").prop("checked", user_settings.presence_enabled);
activity.redraw_user(page_params.user_id);
// Temporary transition code to update the set of
// user_status.away_user_ids so that the user info
// popover will update when the user updates their
// presence_enabled privacy setting.
if (event.value) {
user_status.revoke_away(page_params.user_id);
} else {
user_status.set_away(page_params.user_id);
}
break;
}
settings_display.update_page(event.property);
@@ -798,14 +788,6 @@ export function dispatch_normal_event(event) {
break;
case "user_status":
if (event.away !== undefined) {
if (event.away) {
activity.on_set_away(event.user_id);
} else {
activity.on_revoke_away(event.user_id);
}
}
if (event.status_text !== undefined) {
user_status.set_status_text({
user_id: event.user_id,

View File

@@ -45,7 +45,7 @@ function setup_settings_label() {
settings_label = {
// settings_notification
presence_enabled: $t({
defaultMessage: "Display my availability to other users when online",
defaultMessage: "Display my availability to other users (invisible mode off)",
}),
send_stream_typing_notifications: $t({
defaultMessage: "Let subscribers see when I'm typing messages in streams",

View File

@@ -554,7 +554,9 @@ export const realm_user_settings_defaults_labels = {
}),
enable_digest_emails: $t({defaultMessage: "Send digest emails when user is away"}),
realm_presence_enabled: $t({defaultMessage: "Display availability to other users when online"}),
realm_presence_enabled: $t({
defaultMessage: "Display availability to other users (invisible mode off)",
}),
realm_enter_sends: $t({defaultMessage: "Enter sends when composing a message"}),
realm_send_read_receipts: $t({defaultMessage: "Allow other users to view read receipts"}),
};

View File

@@ -1,17 +1,14 @@
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as emoji from "./emoji";
import {user_settings} from "./user_settings";
const away_user_ids = new Set();
const user_info = new Map();
const user_status_emoji_info = new Map();
export function server_update(opts) {
export function server_update_status(opts) {
channel.post({
url: "/json/users/me/status",
data: {
away: opts.away,
status_text: opts.status_text,
emoji_name: opts.emoji_name,
emoji_code: opts.emoji_code,
@@ -25,30 +22,22 @@ export function server_update(opts) {
});
}
export function server_set_away() {
server_update({away: true});
export function server_invisible_mode_on() {
channel.patch({
url: "/json/settings",
data: {
presence_enabled: false,
},
});
}
export function server_revoke_away() {
server_update({away: false});
}
export function set_away(user_id) {
if (typeof user_id !== "number") {
blueslip.error("need ints for user_id");
}
away_user_ids.add(user_id);
}
export function revoke_away(user_id) {
if (typeof user_id !== "number") {
blueslip.error("need ints for user_id");
}
away_user_ids.delete(user_id);
}
export function is_away(user_id) {
return away_user_ids.has(user_id);
export function server_invisible_mode_off() {
channel.patch({
url: "/json/settings",
data: {
presence_enabled: true,
},
});
}
export function get_status_text(user_id) {
@@ -85,7 +74,6 @@ export function set_status_emoji(opts) {
}
export function initialize(params) {
away_user_ids.clear();
user_info.clear();
for (const [str_user_id, dct] of Object.entries(params.user_status)) {
@@ -93,10 +81,6 @@ export function initialize(params) {
// convert them here.
const user_id = Number.parseInt(str_user_id, 10);
if (dct.away) {
away_user_ids.add(user_id);
}
if (dct.status_text) {
user_info.set(user_id, dct.status_text);
}

View File

@@ -62,7 +62,7 @@ export function submit_new_status() {
return;
}
user_status.server_update({
user_status.server_update_status({
status_text: new_status_text,
emoji_name: selected_emoji_info.emoji_name || "",
emoji_code: selected_emoji_info.emoji_code || "",

View File

@@ -47,12 +47,6 @@
<h3 class="inline-block">{{t "Privacy" }}</h3>
<div class="alert-notification privacy-setting-status"></div>
<div class="input-group">
{{> settings_checkbox
setting_name="presence_enabled"
is_checked=settings_object.presence_enabled
label=settings_label.presence_enabled
help_link="/help/status-and-availability"
prefix="user_"}}
{{> settings_checkbox
setting_name="send_private_typing_notifications"
is_checked=settings_object.send_private_typing_notifications
@@ -73,6 +67,12 @@
hide_tooltip=page_params.realm_enable_read_receipts
help_link="/help/read-receipts"
}}
{{> settings_checkbox
setting_name="presence_enabled"
is_checked=settings_object.presence_enabled
label=settings_label.presence_enabled
help_link="/help/status-and-availability"
prefix="user_"}}
</div>
</div>

View File

@@ -76,17 +76,16 @@
{{#if is_me}}
<hr />
{{#if can_set_away}}
{{#if invisible_mode}}
<li>
<a tabindex="0" class="set_away_status">
<i class="fa fa-minus-circle" aria-hidden="true"></i> {{#tr}}Set yourself as unavailable{{/tr}}
<a tabindex="0" class="invisible_mode_turn_off">
<i class="fa fa-circle-o" aria-hidden="true"></i> {{#tr}}Turn off invisible mode{{/tr}}
</a>
</li>
{{/if}}
{{#if can_revoke_away}}
{{else}}
<li>
<a tabindex="0" class="revoke_away_status">
<i class="fa fa-minus-circle" aria-hidden="true"></i> {{#tr}}Set yourself as available{{/tr}}
<a tabindex="0" class="invisible_mode_turn_on">
<i class="fa fa-circle-o" aria-hidden="true"></i> {{#tr}}Go invisible{{/tr}}
</a>
</li>
{{/if}}

View File

@@ -209,6 +209,7 @@ EXEMPT_FILES = make_set(
"static/js/user_groups_settings_ui.js",
"static/js/user_profile.js",
"static/js/user_settings.ts",
"static/js/user_status.js",
"static/js/user_status_ui.js",
"static/js/webpack_public_path.js",
"static/js/zcommand.js",