From 2718bd0b5d2f3e761f7ea07ea7154d4f776aa375 Mon Sep 17 00:00:00 2001 From: Steve Howell Date: Thu, 30 Mar 2017 11:04:01 -0700 Subject: [PATCH] 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. --- .eslintrc.json | 1 + frontend_tests/node_tests/activity.js | 68 ++++++++------ static/js/activity.js | 120 ++++-------------------- static/js/admin.js | 11 ++- static/js/presence.js | 127 ++++++++++++++++++++++++++ zerver/tornado/event_queue.py | 2 +- zproject/settings.py | 1 + 7 files changed, 193 insertions(+), 137 deletions(-) create mode 100644 static/js/presence.js diff --git a/.eslintrc.json b/.eslintrc.json index 616726be06..8fd87e4043 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -98,6 +98,7 @@ "floating_recipient_bar": false, "tab_bar": false, "emoji": false, + "presence": false, "activity": false, "invite": false, "colorspace": false, diff --git a/frontend_tests/node_tests/activity.js b/frontend_tests/node_tests/activity.js index 21d82cb3a5..7a7ed9496d 100644 --- a/frontend_tests/node_tests/activity.js +++ b/frontend_tests/node_tests/activity.js @@ -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 () { diff --git a/static/js/activity.js b/static/js/activity.js index 2d6c047b34..33d568e914 100644 --- a/static/js/activity.js +++ b/static/js/activity.js @@ -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,16 +149,12 @@ 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')) { - num_present += 1; - } + if (presence.is_not_offline(user_id)) { + num_present += 1; } }); @@ -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(); diff --git a/static/js/admin.js b/static/js/admin.js index b0f2b1d785..9f1829048e 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -139,12 +139,13 @@ 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 { - activity_rendered = $("").text(i18n.t("Never")); + 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 = $("").text(i18n.t("Never")); + } } row.find(".last_active").append(activity_rendered); users_table.append(row); diff --git a/static/js/presence.js b/static/js/presence.js new file mode 100644 index 0000000000..23beaf9186 --- /dev/null +++ b/static/js/presence.js @@ -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; +} diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 776a91e33f..a5da68c49d 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -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 diff --git a/zproject/settings.py b/zproject/settings.py index 40696e02da..a5c9897042 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -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',