Extract presence.js to track presence info.

Most of this code was simply moved from activity.js with some
minor renaming of functions like set_presence_info -> set_info.

Some functions were slightly nontrivial extractions:

    is_not_offline:
        came from activity.huddle_fraction_present

    get_status/get_mobile:
        simple getters

    set_user_status:
        partial extraction from activity.set_user_status

    last_active_date:
        pulled out of admin.js code

We also fixed activity.filter_and_sort to take user_ids.
This commit is contained in:
Steve Howell 2017-03-30 11:04:01 -07:00 committed by Tim Abbott
parent 0605a9fb0f
commit 2718bd0b5d
7 changed files with 193 additions and 137 deletions

View File

@ -98,6 +98,7 @@
"floating_recipient_bar": false,
"tab_bar": false,
"emoji": false,
"presence": false,
"activity": false,
"invite": false,
"colorspace": false,

View File

@ -22,9 +22,14 @@ add_dependencies({
hash_util: 'js/hash_util.js',
hashchange: 'js/hashchange.js',
narrow: 'js/narrow.js',
presence: 'js/presence.js',
activity: 'js/activity.js',
});
var presence = global.presence;
var OFFLINE_THRESHOLD_SECS = 140;
set_global('resize', {
resize_page_components: function () {},
});
@ -88,7 +93,7 @@ global.compile_template('user_presence_rows');
presence_info[fred.user_id] = { status: 'active' };
presence_info[jill.user_id] = { status: 'active' };
activity.presence_info = presence_info;
presence.presence_info = presence_info;
activity._sort_users(user_ids);
assert.deepEqual(user_ids, [
@ -192,7 +197,7 @@ global.compile_template('user_presence_rows');
presence_info[fred.user_id] = { status: 'idle' }; // counts as present
// jill not in list
presence_info[mark.user_id] = { status: 'offline' }; // does not count
activity.presence_info = presence_info;
presence.presence_info = presence_info;
assert.equal(
activity.huddle_fraction_present(huddle),
@ -201,54 +206,57 @@ global.compile_template('user_presence_rows');
(function test_on_mobile_property() {
// TODO: move this test to a new test module directly testing presence.js
var status_from_timestamp = presence._status_from_timestamp;
var base_time = 500;
var presence = {
var info = {
website: {
status: "active",
timestamp: base_time,
},
};
var status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS - 1, presence);
var status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS - 1, info);
assert.equal(status.mobile, false);
presence.Android = {
info.Android = {
status: "active",
timestamp: base_time + activity._OFFLINE_THRESHOLD_SECS / 2,
timestamp: base_time + OFFLINE_THRESHOLD_SECS / 2,
pushable: false,
};
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS, info);
assert.equal(status.mobile, true);
assert.equal(status.status, "active");
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS - 1, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS - 1, info);
assert.equal(status.mobile, false);
assert.equal(status.status, "active");
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS * 2, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS * 2, info);
assert.equal(status.mobile, false);
assert.equal(status.status, "offline");
presence.Android = {
info.Android = {
status: "idle",
timestamp: base_time + activity._OFFLINE_THRESHOLD_SECS / 2,
timestamp: base_time + OFFLINE_THRESHOLD_SECS / 2,
pushable: true,
};
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS, info);
assert.equal(status.mobile, true);
assert.equal(status.status, "idle");
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS - 1, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS - 1, info);
assert.equal(status.mobile, false);
assert.equal(status.status, "active");
status = activity._status_from_timestamp(
base_time + activity._OFFLINE_THRESHOLD_SECS * 2, presence);
status = status_from_timestamp(
base_time + OFFLINE_THRESHOLD_SECS * 2, info);
assert.equal(status.mobile, true);
assert.equal(status.status, "offline");
@ -272,23 +280,23 @@ global.compile_template('user_presence_rows');
},
};
activity.set_presence_info(presences, base_time);
presence.set_info(presences, base_time);
assert.deepEqual(activity.presence_info[alice.user_id],
assert.deepEqual(presence.presence_info[alice.user_id],
{ status: 'active', mobile: false, last_active: 500}
);
assert.deepEqual(activity.presence_info[fred.user_id],
assert.deepEqual(presence.presence_info[fred.user_id],
{ status: 'idle', mobile: false, last_active: 500}
);
}());
activity.presence_info = {};
activity.presence_info[alice.user_id] = { status: activity.IDLE };
activity.presence_info[fred.user_id] = { status: activity.ACTIVE };
activity.presence_info[jill.user_id] = { status: activity.ACTIVE };
activity.presence_info[mark.user_id] = { status: activity.IDLE };
activity.presence_info[norbert.user_id] = { status: activity.ACTIVE };
presence.presence_info = {};
presence.presence_info[alice.user_id] = { status: activity.IDLE };
presence.presence_info[fred.user_id] = { status: activity.ACTIVE };
presence.presence_info[jill.user_id] = { status: activity.ACTIVE };
presence.presence_info[mark.user_id] = { status: activity.IDLE };
presence.presence_info[norbert.user_id] = { status: activity.ACTIVE };
(function test_presence_list_full_update() {
global.$ = function () {

View File

@ -10,20 +10,6 @@ var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
/* Time between keep-alive pings */
var ACTIVE_PING_INTERVAL_MS = 50 * 1000;
/* Mark users as offline after 140 seconds since their last checkin,
* Keep in sync with zerver/tornado/event_queue.py:receiver_is_idle
*/
var OFFLINE_THRESHOLD_SECS = 140;
// Testing
exports._OFFLINE_THRESHOLD_SECS = OFFLINE_THRESHOLD_SECS;
var MOBILE_DEVICES = ["Android", "ZulipiOS", "ios"];
function is_mobile(device) {
return MOBILE_DEVICES.indexOf(device) !== -1;
}
var presence_descriptions = {
active: 'is active',
idle: 'is not active',
@ -47,8 +33,6 @@ $("html").on("mousemove", function () {
exports.new_user_input = true;
});
exports.presence_info = {};
var huddle_timestamps = new Dict();
function update_count_in_dom(count_span, value_span, count) {
@ -165,17 +149,13 @@ exports.short_huddle_name = function (huddle) {
};
exports.huddle_fraction_present = function (huddle) {
var presence_info = exports.presence_info;
var user_ids = huddle.split(',');
var num_present = 0;
_.each(user_ids, function (user_id) {
if (presence_info[user_id]) {
var status = presence_info[user_id].status;
if (status && (status !== 'offline')) {
if (presence.is_not_offline(user_id)) {
num_present += 1;
}
}
});
var ratio = num_present / user_ids.length;
@ -184,7 +164,6 @@ exports.huddle_fraction_present = function (huddle) {
};
function compare_function(a, b) {
var presence_info = exports.presence_info;
function level(status) {
switch (status) {
@ -197,8 +176,8 @@ function compare_function(a, b) {
}
}
var level_a = level(presence_info[a].status);
var level_b = level(presence_info[b].status);
var level_a = level(presence.get_status(a));
var level_b = level(presence.get_status(b));
var diff = level_a - level_b;
if (diff !== 0) {
return diff;
@ -264,8 +243,7 @@ function matches_filter(user_id) {
return (filter_user_ids([user_id]).length === 1);
}
function filter_and_sort(users) {
var user_ids = Object.keys(users);
function filter_and_sort(user_ids) {
user_ids = filter_user_ids(user_ids);
user_ids = sort_users(user_ids);
return user_ids;
@ -281,16 +259,16 @@ function get_num_unread(user_id) {
}
function info_for(user_id) {
var presence = exports.presence_info[user_id].status;
var status = presence.get_status(user_id);
var person = people.get_person_from_user_id(user_id);
return {
href: narrow.pm_with_uri(person.email),
name: person.full_name,
user_id: user_id,
num_unread: get_num_unread(user_id),
type: presence,
type_desc: presence_descriptions[presence],
mobile: exports.presence_info[user_id].mobile,
type: status,
type_desc: presence_descriptions[status],
mobile: presence.get_mobile(user_id),
};
}
@ -335,10 +313,9 @@ exports.build_user_sidebar = function () {
return;
}
var users = exports.presence_info;
users = filter_and_sort(users);
var user_ids = filter_and_sort(presence.get_user_ids());
var user_info = _.map(users, info_for);
var user_info = _.map(user_ids, info_for);
var html = templates.render('user_presence_rows', {users: user_info});
$('#user_presences').html(html);
@ -398,51 +375,6 @@ exports.update_huddles = function () {
show_huddles();
};
function status_from_timestamp(baseline_time, presence) {
var status = 'offline';
var last_active = 0;
var mobileAvailable = false;
var nonmobileAvailable = false;
_.each(presence, function (device_presence, device) {
var age = baseline_time - device_presence.timestamp;
if (last_active < device_presence.timestamp) {
last_active = device_presence.timestamp;
}
if (is_mobile(device)) {
mobileAvailable = device_presence.pushable || mobileAvailable;
}
if (age < OFFLINE_THRESHOLD_SECS) {
switch (device_presence.status) {
case 'active':
if (is_mobile(device)) {
mobileAvailable = true;
} else {
nonmobileAvailable = true;
}
status = device_presence.status;
break;
case 'idle':
if (status !== 'active') {
status = device_presence.status;
}
break;
case 'offline':
if (status !== 'active' && status !== 'idle') {
status = device_presence.status;
}
break;
default:
blueslip.error('Unexpected status', {presence_object: device_presence, device: device}, undefined);
}
}
});
return {status: status,
mobile: !nonmobileAvailable && mobileAvailable,
last_active: last_active };
}
// For testing
exports._status_from_timestamp = status_from_timestamp;
function focus_ping(want_redraw) {
channel.post({
@ -465,7 +397,7 @@ function focus_ping(want_redraw) {
// not send us any presences data. But avoiding the redraw
// helps.
if (want_redraw) {
exports.set_presence_info(data.presences, data.server_timestamp);
presence.set_info(data.presences, data.server_timestamp);
exports.build_user_sidebar();
exports.update_huddles();
}
@ -487,7 +419,7 @@ exports.initialize = function () {
onActive: focus_gained,
keepTracking: true});
activity.set_presence_info(page_params.initial_presences,
presence.set_info(page_params.initial_presences,
page_params.initial_servertime);
exports.build_user_sidebar();
exports.update_huddles();
@ -503,36 +435,22 @@ exports.initialize = function () {
setInterval(get_full_presence_list_update, ACTIVE_PING_INTERVAL_MS);
};
exports.set_user_status = function (email, presence, server_time) {
exports.set_user_status = function (email, info, server_time) {
if (people.is_current_user(email)) {
return;
}
var user_id = people.get_user_id(email);
if (user_id) {
var status = status_from_timestamp(server_time, presence);
exports.presence_info[user_id] = status;
exports.insert_user_into_list(user_id);
} else {
if (!user_id) {
blueslip.warn('unknown email: ' + email);
return;
}
presence.set_user_status(user_id, info, server_time);
exports.insert_user_into_list(user_id);
exports.update_huddles();
};
exports.set_presence_info = function (presences, server_timestamp) {
exports.presence_info = {};
_.each(presences, function (presence, this_email) {
if (!people.is_current_user(this_email)) {
var user_id = people.get_user_id(this_email);
if (user_id) {
var status = status_from_timestamp(server_timestamp,
presence);
exports.presence_info[user_id] = status;
}
}
});
};
exports.redraw = function () {
exports.build_user_sidebar();
exports.update_huddles();

View File

@ -139,13 +139,14 @@ function populate_users(realm_people_data) {
var row = $(templates.render("admin_user_list", {user: user}));
if (people.is_current_user(user.email)) {
activity_rendered = timerender.render_date(new XDate());
} else if (activity.presence_info[user.user_id]) {
// XDate takes number of milliseconds since UTC epoch.
var last_active = activity.presence_info[user.user_id].last_active * 1000;
activity_rendered = timerender.render_date(new XDate(last_active));
} else {
var last_active_date = presence.last_active_date(user.user_id);
if (last_active_date) {
activity_rendered = timerender.render_date(last_active_date);
} else {
activity_rendered = $("<span></span>").text(i18n.t("Never"));
}
}
row.find(".last_active").append(activity_rendered);
users_table.append(row);
});

127
static/js/presence.js Normal file
View File

@ -0,0 +1,127 @@
var presence = (function () {
var exports = {};
// This module just manages data. See activity.js for
// the UI of our buddy list.
exports.presence_info = {};
/* Mark users as offline after 140 seconds since their last checkin,
* Keep in sync with zerver/tornado/event_queue.py:receiver_is_idle
*/
var OFFLINE_THRESHOLD_SECS = 140;
var MOBILE_DEVICES = ["Android", "ZulipiOS", "ios"];
function is_mobile(device) {
return MOBILE_DEVICES.indexOf(device) !== -1;
}
exports.is_not_offline = function (user_id) {
var presence_info = exports.presence_info;
if (presence_info[user_id]) {
var status = presence_info[user_id].status;
if (status && (status !== 'offline')) {
return true;
}
}
return false;
};
exports.get_status = function (user_id) {
return exports.presence_info[user_id].status;
};
exports.get_mobile = function (user_id) {
return exports.presence_info[user_id].mobile;
};
exports.get_user_ids = function () {
var user_ids = Object.keys(exports.presence_info);
return user_ids;
};
function status_from_timestamp(baseline_time, info) {
var status = 'offline';
var last_active = 0;
var mobileAvailable = false;
var nonmobileAvailable = false;
_.each(info, function (device_presence, device) {
var age = baseline_time - device_presence.timestamp;
if (last_active < device_presence.timestamp) {
last_active = device_presence.timestamp;
}
if (is_mobile(device)) {
mobileAvailable = device_presence.pushable || mobileAvailable;
}
if (age < OFFLINE_THRESHOLD_SECS) {
switch (device_presence.status) {
case 'active':
if (is_mobile(device)) {
mobileAvailable = true;
} else {
nonmobileAvailable = true;
}
status = device_presence.status;
break;
case 'idle':
if (status !== 'active') {
status = device_presence.status;
}
break;
case 'offline':
if (status !== 'active' && status !== 'idle') {
status = device_presence.status;
}
break;
default:
blueslip.error('Unexpected status', {presence_object: device_presence, device: device}, undefined);
}
}
});
return {status: status,
mobile: !nonmobileAvailable && mobileAvailable,
last_active: last_active };
}
// For testing
exports._status_from_timestamp = status_from_timestamp;
exports.set_user_status = function (user_id, info, server_time) {
var status = status_from_timestamp(server_time, info);
exports.presence_info[user_id] = status;
};
exports.set_info = function (presences, server_timestamp) {
exports.presence_info = {};
_.each(presences, function (info, this_email) {
if (!people.is_current_user(this_email)) {
var user_id = people.get_user_id(this_email);
if (user_id) {
var status = status_from_timestamp(server_timestamp,
info);
exports.presence_info[user_id] = status;
}
}
});
};
exports.last_active_date = function (user_id) {
var info = exports.presence_info[user_id];
if (!info || !info.last_active) {
return;
}
var date = new XDate(info.last_active * 1000);
return date;
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = presence;
}

View File

@ -677,7 +677,7 @@ def receiver_is_idle(user_profile_id, realm_presences):
idle = True
else:
active_datetime = timestamp_to_datetime(latest_active_timestamp)
# 140 seconds is consistent with activity.js:OFFLINE_THRESHOLD_SECS
# 140 seconds is consistent with presence.js:OFFLINE_THRESHOLD_SECS
idle = timezone.now() - active_datetime > datetime.timedelta(seconds=140)
return off_zulip or idle

View File

@ -893,6 +893,7 @@ JS_SPECS = {
'js/message_fetch.js',
'js/server_events.js',
'js/zulip.js',
'js/presence.js',
'js/activity.js',
'js/user_events.js',
'js/colorspace.js',