mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 22:43:42 +00:00
We now call activity.build_user_sidebar when we initialize the user sidebar, which avoids some janky jQuery code that was intended for partial updates. With 2000 users in dev, the amount of time to build the sidebar decreases from 1100ms to 700ms in my tests. (Times vary a bit, but it does seem consistently faster now.)
577 lines
17 KiB
JavaScript
577 lines
17 KiB
JavaScript
var activity = (function () {
|
|
var exports = {};
|
|
|
|
/*
|
|
Helpers for detecting user activity and managing user idle states
|
|
*/
|
|
|
|
/* Broadcast "idle" to server after 5 minutes of local inactivity */
|
|
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',
|
|
};
|
|
|
|
/* Keep in sync with views.py:update_active_status_backend() */
|
|
exports.ACTIVE = "active";
|
|
exports.IDLE = "idle";
|
|
|
|
// When you start Zulip, has_focus should be true, but it might not be the
|
|
// case after a server-initiated reload.
|
|
exports.has_focus = document.hasFocus && document.hasFocus();
|
|
|
|
// We initialize this to true, to count new page loads, but set it to
|
|
// false in the onload function in reload.js if this was a
|
|
// server-initiated-reload to avoid counting a server-initiated reload
|
|
// as user activity.
|
|
exports.new_user_input = true;
|
|
|
|
$("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) {
|
|
if (count === 0) {
|
|
count_span.hide();
|
|
if (count_span.parent().hasClass("user_sidebar_entry")) {
|
|
count_span.parent(".user_sidebar_entry").removeClass("user-with-count");
|
|
} else if (count_span.parent().hasClass("group-pms-sidebar-entry")) {
|
|
count_span.parent(".group-pms-sidebar-entry").removeClass("group-with-count");
|
|
}
|
|
value_span.text('');
|
|
return;
|
|
}
|
|
|
|
count_span.show();
|
|
|
|
if (count_span.parent().hasClass("user_sidebar_entry")) {
|
|
count_span.parent(".user_sidebar_entry").addClass("user-with-count");
|
|
} else if (count_span.parent().hasClass("group-pms-sidebar-entry")) {
|
|
count_span.parent(".group-pms-sidebar-entry").addClass("group-with-count");
|
|
}
|
|
value_span.text(count);
|
|
}
|
|
|
|
function get_filter_li(user_ids_string) {
|
|
if (name.indexOf(",") < 0) {
|
|
return $("li.user_sidebar_entry[data-user-id='" + user_ids_string + "']");
|
|
}
|
|
return $("li.group-pms-sidebar-entry[data-user-ids='" + user_ids_string + "']");
|
|
}
|
|
|
|
function set_count(user_ids_string, count) {
|
|
var count_span = get_filter_li(user_ids_string).find('.count');
|
|
var value_span = count_span.find('.value');
|
|
update_count_in_dom(count_span, value_span, count);
|
|
}
|
|
|
|
exports.update_dom_with_unread_counts = function (counts) {
|
|
// counts is just a data object that gets calculated elsewhere
|
|
// Our job is to update some DOM elements.
|
|
|
|
counts.pm_count.each(function (count, user_ids_string) {
|
|
// TODO: just use user_ids_string in our markup
|
|
set_count(user_ids_string, count);
|
|
});
|
|
};
|
|
|
|
exports.process_loaded_messages = function (messages) {
|
|
var need_resize = false;
|
|
|
|
_.each(messages, function (message) {
|
|
var huddle_string = people.huddle_string(message);
|
|
|
|
if (huddle_string) {
|
|
var old_timestamp = huddle_timestamps.get(huddle_string);
|
|
|
|
if (!old_timestamp || (old_timestamp < message.timestamp)) {
|
|
huddle_timestamps.set(huddle_string, message.timestamp);
|
|
need_resize = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
exports.update_huddles();
|
|
|
|
if (need_resize) {
|
|
resize.resize_page_components(); // big hammer
|
|
}
|
|
};
|
|
|
|
exports.get_huddles = function () {
|
|
var huddles = huddle_timestamps.keys();
|
|
huddles = _.sortBy(huddles, function (huddle) {
|
|
return huddle_timestamps.get(huddle);
|
|
});
|
|
return huddles.reverse();
|
|
};
|
|
|
|
exports.full_huddle_name = function (huddle) {
|
|
var user_ids = huddle.split(',');
|
|
|
|
var names = _.map(user_ids, function (user_id) {
|
|
var person = people.get_person_from_user_id(user_id);
|
|
return person.full_name;
|
|
});
|
|
|
|
return names.join(', ');
|
|
};
|
|
|
|
exports.short_huddle_name = function (huddle) {
|
|
var user_ids = huddle.split(',');
|
|
|
|
var num_to_show = 3;
|
|
var names = _.map(user_ids, function (user_id) {
|
|
var person = people.get_person_from_user_id(user_id);
|
|
return person.full_name;
|
|
});
|
|
|
|
names = _.sortBy(names, function (name) { return name.toLowerCase(); });
|
|
names = names.slice(0, num_to_show);
|
|
var others = user_ids.length - num_to_show;
|
|
|
|
if (others === 1) {
|
|
names.push("+ 1 other");
|
|
} else if (others >= 2) {
|
|
names.push("+ " + others + " others");
|
|
}
|
|
|
|
return names.join(', ');
|
|
};
|
|
|
|
exports.huddle_fraction_present = function (huddle, 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;
|
|
}
|
|
}
|
|
});
|
|
|
|
var ratio = num_present / user_ids.length;
|
|
|
|
return ratio.toFixed(2);
|
|
};
|
|
|
|
function sort_users(user_ids, presence_info) {
|
|
// TODO sort by unread count first, once we support that
|
|
user_ids.sort(function (a, b) {
|
|
if (presence_info[a].status === 'active' && presence_info[b].status !== 'active') {
|
|
return -1;
|
|
} else if (presence_info[b].status === 'active' && presence_info[a].status !== 'active') {
|
|
return 1;
|
|
}
|
|
|
|
if (presence_info[a].status === 'idle' && presence_info[b].status !== 'idle') {
|
|
return -1;
|
|
} else if (presence_info[b].status === 'idle' && presence_info[a].status !== 'idle') {
|
|
return 1;
|
|
}
|
|
|
|
// Sort equivalent PM names alphabetically
|
|
var full_name_a = a;
|
|
var full_name_b = b;
|
|
if (people.get_person_from_user_id(a)) {
|
|
full_name_a = people.get_person_from_user_id(a).full_name;
|
|
}
|
|
if (people.get_person_from_user_id(b)) {
|
|
full_name_b = people.get_person_from_user_id(b).full_name;
|
|
}
|
|
return util.strcmp(full_name_a, full_name_b);
|
|
});
|
|
|
|
return user_ids;
|
|
}
|
|
|
|
// for testing:
|
|
exports._sort_users = sort_users;
|
|
|
|
function focus_lost() {
|
|
if (!exports.has_focus) {
|
|
return false;
|
|
}
|
|
|
|
exports.has_focus = false;
|
|
}
|
|
|
|
function filter_user_ids(user_ids) {
|
|
var user_list = $(".user-list-filter");
|
|
if (user_list.length === 0) {
|
|
// We may have received an activity ping response after
|
|
// initiating a reload, in which case the user list may no
|
|
// longer be available.
|
|
// Return user list: useful for testing user list performance fix
|
|
return user_ids;
|
|
}
|
|
|
|
var search_term = user_list.expectOne().val().trim();
|
|
if (search_term === '') {
|
|
return user_ids;
|
|
}
|
|
|
|
var search_terms = search_term.toLowerCase().split(",");
|
|
search_terms = _.map(search_terms, function (s) {
|
|
return s.trim();
|
|
});
|
|
|
|
var persons = _.map(user_ids, function (user_id) {
|
|
return people.get_person_from_user_id(user_id);
|
|
});
|
|
|
|
var user_id_dict = people.filter_people_by_search_terms(persons, search_terms);
|
|
return user_id_dict.keys();
|
|
}
|
|
|
|
function filter_and_sort(users) {
|
|
var user_ids = Object.keys(users);
|
|
user_ids = filter_user_ids(user_ids);
|
|
user_ids = sort_users(user_ids, exports.presence_info);
|
|
return user_ids;
|
|
}
|
|
|
|
exports._filter_and_sort = filter_and_sort;
|
|
|
|
function get_num_unread(user_id) {
|
|
if (unread.suppress_unread_counts) {
|
|
return 0;
|
|
}
|
|
return unread.num_unread_for_person(user_id);
|
|
}
|
|
|
|
function info_for(user_id) {
|
|
var presence = exports.presence_info[user_id].status;
|
|
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,
|
|
};
|
|
}
|
|
|
|
exports.update_users = function (updated_users) {
|
|
if (page_params.presence_disabled) {
|
|
return;
|
|
}
|
|
|
|
if (!updated_users) {
|
|
blueslip.error('update_users called incorrectly');
|
|
return;
|
|
}
|
|
|
|
var all_users = filter_and_sort(exports.presence_info);
|
|
var users = filter_and_sort(updated_users);
|
|
|
|
var user_info = _.map(users, info_for);
|
|
|
|
_.each(user_info, function (user) {
|
|
// This is really brittle code. Our indexes into all_users
|
|
// are based on the assumption that since the sidebar was
|
|
// last drawn, we haven't changed the population of
|
|
// exports.presence_info or the sorting methodology.
|
|
var user_index = all_users.indexOf(user.user_id);
|
|
$('#user_presences').find('[data-user-id="' + user.user_id + '"]').remove();
|
|
var html = templates.render('user_presence_row', user);
|
|
$('#user_presences li').eq(user_index).before(html);
|
|
});
|
|
|
|
// TODO: tell compose_fade exactly which users we need to fix.
|
|
compose_fade.update_faded_users();
|
|
|
|
return user_info; // for testing
|
|
};
|
|
|
|
exports.build_user_sidebar = function () {
|
|
if (page_params.presence_disabled) {
|
|
return;
|
|
}
|
|
|
|
var users = exports.presence_info;
|
|
users = filter_and_sort(users);
|
|
|
|
var user_info = _.map(users, info_for);
|
|
var html = templates.render('user_presence_rows', {users: user_info});
|
|
$('#user_presences').html(html);
|
|
|
|
// Update user fading, if necessary.
|
|
compose_fade.update_faded_users();
|
|
|
|
return user_info; // for testing
|
|
};
|
|
|
|
function actually_update_users_for_search() {
|
|
exports.build_user_sidebar();
|
|
resize.resize_page_components();
|
|
}
|
|
|
|
var update_users_for_search = _.throttle(actually_update_users_for_search, 50);
|
|
|
|
function show_huddles() {
|
|
$('#group-pm-list').addClass("show");
|
|
}
|
|
|
|
function hide_huddles() {
|
|
$('#group-pm-list').removeClass("show");
|
|
}
|
|
|
|
exports.update_huddles = function () {
|
|
if (page_params.presence_disabled) {
|
|
return;
|
|
}
|
|
|
|
var huddles = exports.get_huddles().slice(0, 10);
|
|
|
|
if (huddles.length === 0) {
|
|
hide_huddles();
|
|
return;
|
|
}
|
|
|
|
var group_pms = _.map(huddles, function (huddle) {
|
|
return {
|
|
user_ids_string: huddle,
|
|
name: exports.full_huddle_name(huddle),
|
|
href: narrow.huddle_with_uri(huddle),
|
|
fraction_present: exports.huddle_fraction_present(huddle, exports.presence_info),
|
|
short_name: exports.short_huddle_name(huddle),
|
|
};
|
|
});
|
|
|
|
var html = templates.render('group_pms', {group_pms: group_pms});
|
|
$('#group-pms').expectOne().html(html);
|
|
|
|
_.each(huddles, function (user_ids_string) {
|
|
var count = unread.num_unread_for_person(user_ids_string);
|
|
set_count(user_ids_string, count);
|
|
});
|
|
|
|
show_huddles();
|
|
};
|
|
|
|
function status_from_timestamp(baseline_time, presence) {
|
|
var status = 'offline';
|
|
var mobileAvailable = false;
|
|
var nonmobileAvailable = false;
|
|
_.each(presence, function (device_presence, device) {
|
|
var age = baseline_time - 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 };
|
|
}
|
|
|
|
// For testing
|
|
exports._status_from_timestamp = status_from_timestamp;
|
|
|
|
function focus_ping() {
|
|
channel.post({
|
|
url: '/json/users/me/presence',
|
|
data: {status: (exports.has_focus) ? exports.ACTIVE : exports.IDLE,
|
|
new_user_input: exports.new_user_input},
|
|
idempotent: true,
|
|
success: function (data) {
|
|
|
|
// Update Zephyr mirror activity warning
|
|
if (data.zephyr_mirror_active === false) {
|
|
$('#zephyr-mirror-error').show();
|
|
} else {
|
|
$('#zephyr-mirror-error').hide();
|
|
}
|
|
|
|
exports.new_user_input = false;
|
|
|
|
exports.set_presence_info(data.presences, data.server_timestamp);
|
|
exports.build_user_sidebar();
|
|
exports.update_huddles();
|
|
},
|
|
});
|
|
}
|
|
|
|
function focus_gained() {
|
|
if (!exports.has_focus) {
|
|
exports.has_focus = true;
|
|
|
|
focus_ping();
|
|
}
|
|
}
|
|
|
|
exports.initialize = function () {
|
|
$(window).focus(focus_gained);
|
|
$(window).idle({idle: DEFAULT_IDLE_TIMEOUT_MS,
|
|
onIdle: focus_lost,
|
|
onActive: focus_gained,
|
|
keepTracking: true});
|
|
|
|
setInterval(focus_ping, ACTIVE_PING_INTERVAL_MS);
|
|
|
|
focus_ping();
|
|
|
|
activity.set_presence_info(page_params.initial_presences,
|
|
page_params.initial_servertime);
|
|
exports.build_user_sidebar();
|
|
exports.update_huddles();
|
|
};
|
|
|
|
// Set user statuses. `users` should be an object with user emails as keys
|
|
// and presence information (see `status_from_timestamp`) as values.
|
|
//
|
|
// The object does not need to include every user, only the ones
|
|
// whose presence you wish to update.
|
|
//
|
|
// This rerenders the user sidebar at the end, which can be slow if done too
|
|
// often, so try to avoid calling this repeatedly.
|
|
exports.set_user_statuses = function (users, server_time) {
|
|
var updated_users = {};
|
|
var status;
|
|
_.each(users, function (presence, email) {
|
|
if (people.is_current_user(email)) {
|
|
return;
|
|
}
|
|
status = status_from_timestamp(server_time, presence);
|
|
var user_id = people.get_user_id(email);
|
|
if (user_id) {
|
|
exports.presence_info[user_id] = status;
|
|
updated_users[user_id] = status;
|
|
} else {
|
|
blueslip.warn('unknown email: ' + email);
|
|
}
|
|
});
|
|
|
|
exports.update_users(updated_users);
|
|
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();
|
|
};
|
|
|
|
exports.searching = function () {
|
|
return $('.user-list-filter').expectOne().is(':focus');
|
|
};
|
|
|
|
exports.escape_search = function () {
|
|
var filter = $('.user-list-filter').expectOne();
|
|
if (filter.val() === '') {
|
|
filter.blur();
|
|
return;
|
|
}
|
|
filter.val('');
|
|
update_users_for_search();
|
|
};
|
|
|
|
exports.initiate_search = function () {
|
|
var filter = $('.user-list-filter').expectOne();
|
|
filter.focus();
|
|
};
|
|
|
|
exports.blur_search = function () {
|
|
$('.user-list-filter').blur();
|
|
};
|
|
|
|
function maybe_select_person(e) {
|
|
if (e.keyCode === 13) {
|
|
// Enter key was pressed
|
|
|
|
// Prevent a newline from being entered into the soon-to-be-opened composebox
|
|
e.preventDefault();
|
|
|
|
var topPerson = $('#user_presences li.user_sidebar_entry').first().attr('data-user-id');
|
|
var user_list = $(".user-list-filter");
|
|
var search_term = user_list.expectOne().val().trim();
|
|
if ((topPerson !== undefined) && (search_term !== '')) {
|
|
// undefined if there are no results
|
|
var email = people.get_person_from_user_id(topPerson).email;
|
|
compose.start('private',
|
|
{trigger: 'sidebar enter key', private_message_recipient: email});
|
|
}
|
|
// Clear the user filter
|
|
exports.escape_search();
|
|
}
|
|
}
|
|
|
|
function focus_user_filter(e) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
$(function () {
|
|
$(".user-list-filter").expectOne()
|
|
.on('click', focus_user_filter)
|
|
.on('input', update_users_for_search)
|
|
.on('keydown', maybe_select_person);
|
|
});
|
|
|
|
|
|
return exports;
|
|
|
|
}());
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = activity;
|
|
}
|