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 people = zrequire("people");
const buddy_data = zrequire("buddy_data"); const buddy_data = zrequire("buddy_data");
const {buddy_list} = zrequire("buddy_list"); const {buddy_list} = zrequire("buddy_list");
const user_status = zrequire("user_status");
const activity = zrequire("activity"); const activity = zrequire("activity");
const me = { const me = {
@@ -674,17 +673,6 @@ test("initialize", ({override, mock_template}) => {
clear(); 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}) => { test("electron_bridge", ({override_rewire}) => {
override_rewire(activity, "send_presence_to_server", () => {}); override_rewire(activity, "send_presence_to_server", () => {});

View File

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

View File

@@ -900,19 +900,16 @@ run_test("user_settings", ({override}) => {
dispatch(event); dispatch(event);
assert_same(user_settings.enter_sends, true); assert_same(user_settings.enter_sends, true);
page_params.user_id = test_user.user_id;
event = event_fixtures.user_settings__presence_disabled; event = event_fixtures.user_settings__presence_disabled;
user_settings.presence_enabled = true; user_settings.presence_enabled = true;
override(activity, "redraw_user", noop); override(activity, "redraw_user", noop);
dispatch(event); dispatch(event);
assert_same(user_settings.presence_enabled, false); 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; event = event_fixtures.user_settings__presence_enabled;
override(activity, "redraw_user", noop); override(activity, "redraw_user", noop);
dispatch(event); dispatch(event);
assert_same(user_settings.presence_enabled, true); 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; event = event_fixtures.user_settings__enable_stream_audible_notifications;
@@ -1019,27 +1016,7 @@ run_test("delete_message", ({override}) => {
}); });
run_test("user_status", ({override}) => { run_test("user_status", ({override}) => {
let event = event_fixtures.user_status__set_away; let event = event_fixtures.user_status__set_status_emoji;
{
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;
{ {
const stub = make_stub(); const stub = make_stub();
override(activity, "redraw_user", stub.f); override(activity, "redraw_user", stub.f);

View File

@@ -947,18 +947,6 @@ exports.fixtures = {
value: 2, 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: { user_status__set_status_emoji: {
type: "user_status", type: "user_status",
user_id: test_user.user_id, 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"); const unread = mock_esm("../../static/js/unread");
mock_esm("../../static/js/user_status", { mock_esm("../../static/js/user_status", {
is_away: () => false,
get_status_emoji: () => ({ get_status_emoji: () => ({
emoji_code: 20, 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) => { mock_template("user_info_popover_content.hbs", false, (opts) => {
assert.deepEqual(opts, { assert.deepEqual(opts, {
can_set_away: false, invisible_mode: false,
can_revoke_away: false,
can_mute: true, can_mute: true,
can_manage_user: false, can_manage_user: false,
can_send_private_message: true, can_send_private_message: true,

View File

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

View File

@@ -15,7 +15,6 @@ import * as popovers from "./popovers";
import * as presence from "./presence"; import * as presence from "./presence";
import * as ui_util from "./ui_util"; import * as ui_util from "./ui_util";
import {UserSearch} from "./user_search"; import {UserSearch} from "./user_search";
import * as user_status from "./user_status";
import * as watchdog from "./watchdog"; import * as watchdog from "./watchdog";
export let user_cursor; export let user_cursor;
@@ -259,18 +258,6 @@ export function update_presence_info(user_id, info, server_time) {
pm_list.update_private_messages(); 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() { export function redraw() {
build_user_sidebar(); build_user_sidebar();
user_cursor.redraw(); user_cursor.redraw();

View File

@@ -36,16 +36,13 @@ const fade_config = {
}; };
export function get_user_circle_class(user_id) { export function get_user_circle_class(user_id) {
const status = buddy_status(user_id); const status = presence.get_status(user_id);
switch (status) { switch (status) {
case "active": case "active":
return "user_circle_green"; return "user_circle_green";
case "idle": case "idle":
return "user_circle_idle"; return "user_circle_idle";
case "away_them":
case "away_me":
return "user_circle_empty_line";
default: default:
return "user_circle_empty"; return "user_circle_empty";
} }
@@ -57,33 +54,18 @@ export function level(user_id) {
return 0; return 0;
} }
const status = buddy_status(user_id); const status = presence.get_status(user_id);
switch (status) { switch (status) {
case "active": case "active":
return 1; return 1;
case "idle": case "idle":
return 2; return 2;
case "away_them":
return 3;
default: default:
return 3; 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) { export function compare_function(a, b) {
const level_a = level(a); const level_a = level(a);
const level_b = level(b); 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 ui_report from "./ui_report";
import * as user_groups from "./user_groups"; import * as user_groups from "./user_groups";
import * as user_profile from "./user_profile"; import * as user_profile from "./user_profile";
import {user_settings} from "./user_settings";
import * as user_status from "./user_status"; import * as user_status from "./user_status";
import * as user_status_ui from "./user_status_ui"; import * as user_status_ui from "./user_status_ui";
import * as util from "./util"; 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); const is_me = people.is_my_user_id(user.user_id);
let can_set_away = false; let invisible_mode = false;
let can_revoke_away = false;
if (is_me) { if (is_me) {
if (user_status.is_away(user.user_id)) { invisible_mode = !user_settings.presence_enabled;
can_revoke_away = true;
} else {
can_set_away = true;
}
} }
const muting_allowed = !is_me && !user.is_bot; 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); .filter((f) => f.display_in_profile_summary && f.value !== undefined && f.value !== null);
const args = { const args = {
can_revoke_away, invisible_mode,
can_set_away,
can_mute: muting_allowed && !is_muted, can_mute: muting_allowed && !is_muted,
can_manage_user: page_params.is_admin && !is_me, can_manage_user: page_params.is_admin && !is_me,
can_send_private_message: can_send_private_message:
@@ -974,7 +969,7 @@ export function register_click_handlers() {
$("body").on("click", ".info_popover_actions .clear_status", (e) => { $("body").on("click", ".info_popover_actions .clear_status", (e) => {
e.preventDefault(); e.preventDefault();
const me = elem_to_user_id($(e.target).parents("ul")); const me = elem_to_user_id($(e.target).parents("ul"));
user_status.server_update({ user_status.server_update_status({
user_id: me, user_id: me,
status_text: "", status_text: "",
emoji_name: "", emoji_name: "",
@@ -997,16 +992,16 @@ export function register_click_handlers() {
* relevant part of the Zulip UI, so we don't want preventDefault, * relevant part of the Zulip UI, so we don't want preventDefault,
* but we do want to close the modal when you click them. */ * 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(); hide_all();
user_status.server_set_away(); user_status.server_invisible_mode_on();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}); });
$("body").on("click", ".revoke_away_status", (e) => { $("body").on("click", ".invisible_mode_turn_off", (e) => {
hide_all(); hide_all();
user_status.server_revoke_away(); user_status.server_invisible_mode_off();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}); });

View File

@@ -723,16 +723,6 @@ export function dispatch_normal_event(event) {
user_settings.presence_enabled = event.value; user_settings.presence_enabled = event.value;
$("#user_presence_enabled").prop("checked", user_settings.presence_enabled); $("#user_presence_enabled").prop("checked", user_settings.presence_enabled);
activity.redraw_user(page_params.user_id); 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; break;
} }
settings_display.update_page(event.property); settings_display.update_page(event.property);
@@ -798,14 +788,6 @@ export function dispatch_normal_event(event) {
break; break;
case "user_status": 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) { if (event.status_text !== undefined) {
user_status.set_status_text({ user_status.set_status_text({
user_id: event.user_id, user_id: event.user_id,

View File

@@ -45,7 +45,7 @@ function setup_settings_label() {
settings_label = { settings_label = {
// settings_notification // settings_notification
presence_enabled: $t({ 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({ send_stream_typing_notifications: $t({
defaultMessage: "Let subscribers see when I'm typing messages in streams", 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"}), 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_enter_sends: $t({defaultMessage: "Enter sends when composing a message"}),
realm_send_read_receipts: $t({defaultMessage: "Allow other users to view read receipts"}), 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 channel from "./channel";
import * as emoji from "./emoji"; import * as emoji from "./emoji";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
const away_user_ids = new Set();
const user_info = new Map(); const user_info = new Map();
const user_status_emoji_info = new Map(); const user_status_emoji_info = new Map();
export function server_update(opts) { export function server_update_status(opts) {
channel.post({ channel.post({
url: "/json/users/me/status", url: "/json/users/me/status",
data: { data: {
away: opts.away,
status_text: opts.status_text, status_text: opts.status_text,
emoji_name: opts.emoji_name, emoji_name: opts.emoji_name,
emoji_code: opts.emoji_code, emoji_code: opts.emoji_code,
@@ -25,30 +22,22 @@ export function server_update(opts) {
}); });
} }
export function server_set_away() { export function server_invisible_mode_on() {
server_update({away: true}); channel.patch({
url: "/json/settings",
data: {
presence_enabled: false,
},
});
} }
export function server_revoke_away() { export function server_invisible_mode_off() {
server_update({away: false}); channel.patch({
} url: "/json/settings",
data: {
export function set_away(user_id) { presence_enabled: true,
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 get_status_text(user_id) { export function get_status_text(user_id) {
@@ -85,7 +74,6 @@ export function set_status_emoji(opts) {
} }
export function initialize(params) { export function initialize(params) {
away_user_ids.clear();
user_info.clear(); user_info.clear();
for (const [str_user_id, dct] of Object.entries(params.user_status)) { for (const [str_user_id, dct] of Object.entries(params.user_status)) {
@@ -93,10 +81,6 @@ export function initialize(params) {
// convert them here. // convert them here.
const user_id = Number.parseInt(str_user_id, 10); const user_id = Number.parseInt(str_user_id, 10);
if (dct.away) {
away_user_ids.add(user_id);
}
if (dct.status_text) { if (dct.status_text) {
user_info.set(user_id, dct.status_text); user_info.set(user_id, dct.status_text);
} }

View File

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

View File

@@ -47,12 +47,6 @@
<h3 class="inline-block">{{t "Privacy" }}</h3> <h3 class="inline-block">{{t "Privacy" }}</h3>
<div class="alert-notification privacy-setting-status"></div> <div class="alert-notification privacy-setting-status"></div>
<div class="input-group"> <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 {{> settings_checkbox
setting_name="send_private_typing_notifications" setting_name="send_private_typing_notifications"
is_checked=settings_object.send_private_typing_notifications is_checked=settings_object.send_private_typing_notifications
@@ -73,6 +67,12 @@
hide_tooltip=page_params.realm_enable_read_receipts hide_tooltip=page_params.realm_enable_read_receipts
help_link="/help/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>
</div> </div>

View File

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

View File

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