mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Previously, you'd have to be offline to recieve missedmessage notifications, or maybe idle for an hour. However, I'm pretty sure the latter code didn't actually work, so we scrap that and just nofity you via email or push as soon as you're idle. Closes trac #2350 (imported from commit 899966e0514db575b9640a96865639201824b579)
		
			
				
	
	
		
			466 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			14 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_callbacks.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:json_update_active_status() */
 | 
						|
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;
 | 
						|
});
 | 
						|
 | 
						|
var presence_info = {};
 | 
						|
 | 
						|
var huddle_timestamps = new Dict();
 | 
						|
 | 
						|
 | 
						|
exports.process_loaded_messages = function (messages) {
 | 
						|
    _.each(messages, function (message) {
 | 
						|
        if (message.type === 'private') {
 | 
						|
            if (message.reply_to.indexOf(',') > 0) {
 | 
						|
                var old_timestamp = huddle_timestamps.get(message.reply_to);
 | 
						|
 | 
						|
                if (!old_timestamp || (old_timestamp < message.timestamp)) {
 | 
						|
                    huddle_timestamps.set(message.reply_to, message.timestamp);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    });
 | 
						|
};
 | 
						|
 | 
						|
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 emails = huddle.split(',');
 | 
						|
 | 
						|
    var names = _.map(emails, function (email) {
 | 
						|
        var person = people.get_by_email(email);
 | 
						|
        return person ? person.full_name : email;
 | 
						|
    });
 | 
						|
 | 
						|
    return names.join(', ');
 | 
						|
};
 | 
						|
 | 
						|
exports.short_huddle_name = function (huddle) {
 | 
						|
    var emails = huddle.split(',');
 | 
						|
 | 
						|
    var num_to_show = 3;
 | 
						|
    var names = _.map(emails.slice(0, num_to_show), function (email) {
 | 
						|
        var person = people.get_by_email(email);
 | 
						|
        return person ? person.full_name : email;
 | 
						|
    });
 | 
						|
    var others = emails.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 emails = huddle.split(',');
 | 
						|
 | 
						|
    var num_present = 0;
 | 
						|
    _.each(emails, function (email) {
 | 
						|
        if (presence_info[email]) {
 | 
						|
            var status = presence_info[email].status;
 | 
						|
            if (status && (status !== 'offline')) {
 | 
						|
                ++num_present;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    var ratio = num_present / emails.length;
 | 
						|
 | 
						|
    return ratio.toFixed(2);
 | 
						|
};
 | 
						|
 | 
						|
function sort_users(users, presence_info) {
 | 
						|
    // TODO sort by unread count first, once we support that
 | 
						|
    users.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_by_email(a)) {
 | 
						|
            full_name_a = people.get_by_email(a).full_name;
 | 
						|
        }
 | 
						|
        if (people.get_by_email(b)) {
 | 
						|
            full_name_b = people.get_by_email(b).full_name;
 | 
						|
        }
 | 
						|
        return util.strcmp(full_name_a, full_name_b);
 | 
						|
    });
 | 
						|
 | 
						|
    return users;
 | 
						|
}
 | 
						|
 | 
						|
// for testing:
 | 
						|
exports._sort_users = sort_users;
 | 
						|
 | 
						|
function focus_lost() {
 | 
						|
    if (!exports.has_focus) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    exports.has_focus = false;
 | 
						|
}
 | 
						|
 | 
						|
function filter_users_by_search(users) {
 | 
						|
    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;
 | 
						|
    }
 | 
						|
 | 
						|
    var search_term = user_list.expectOne().val().trim();
 | 
						|
 | 
						|
    if (search_term === '') {
 | 
						|
        return users;
 | 
						|
    }
 | 
						|
 | 
						|
    var search_terms = search_term.toLowerCase().split(",");
 | 
						|
    search_terms = _.map(search_terms, function (s) {
 | 
						|
        return s.trim();
 | 
						|
    });
 | 
						|
 | 
						|
    var filtered_users = _.filter(users, function (user) {
 | 
						|
        var person = people.get_by_email(user);
 | 
						|
        if (!person || !person.full_name) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        var names = person.full_name.toLowerCase().split(/\s+/);
 | 
						|
        names = _.map(names, function (s) {
 | 
						|
            return s.trim();
 | 
						|
        });
 | 
						|
        return _.any(search_terms, function (search_term) {
 | 
						|
            return _.any(names, function (name) {
 | 
						|
                return name.indexOf(search_term) === 0;
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    return filtered_users;
 | 
						|
}
 | 
						|
 | 
						|
function actually_update_users() {
 | 
						|
    if (page_params.domain === 'mit.edu') {
 | 
						|
        return;  // MIT realm doesn't have a presence list
 | 
						|
    }
 | 
						|
 | 
						|
    var users = Object.keys(presence_info);
 | 
						|
    users = filter_users_by_search(users);
 | 
						|
    users = _.filter(users, function (email) {
 | 
						|
        return people.get_by_email(email);
 | 
						|
    });
 | 
						|
 | 
						|
    users = sort_users(users, presence_info);
 | 
						|
 | 
						|
    function get_num_unread(email) {
 | 
						|
        if (unread.suppress_unread_counts) {
 | 
						|
            return 0;
 | 
						|
        }
 | 
						|
        return unread.num_unread_for_person(email);
 | 
						|
    }
 | 
						|
 | 
						|
    // Note that we do not include ourselves in the user list any more.
 | 
						|
    // If you want to figure out how to get details for "me", then revert
 | 
						|
    // the commit that added this comment.
 | 
						|
 | 
						|
    function info_for(email) {
 | 
						|
        var presence = presence_info[email].status;
 | 
						|
        return {
 | 
						|
            name: people.get_by_email(email).full_name,
 | 
						|
            email: email,
 | 
						|
            num_unread: get_num_unread(email),
 | 
						|
            type: presence,
 | 
						|
            type_desc: presence_descriptions[presence],
 | 
						|
            mobile: presence_info[email].mobile
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    var user_info = _.map(users, info_for);
 | 
						|
 | 
						|
    $('#user_presences').html(templates.render('user_presence_rows', {users: user_info}));
 | 
						|
 | 
						|
    // Update user fading, if necessary.
 | 
						|
    compose_fade.update_faded_users();
 | 
						|
}
 | 
						|
 | 
						|
// The function actually_update_users() can be pretty expensive for realms with lots
 | 
						|
// of users.  Not only is there more work to do in terms of rendering the user list, but
 | 
						|
// we also get more updates.  Large realms have reported lags while typing in the compose
 | 
						|
// box, and there's strong evidence that this is caused by user list updates.  This isn't a
 | 
						|
// perfect solution, but it should remove some pain, and there's no real harm in waiting five
 | 
						|
// seconds to update user activity.
 | 
						|
var update_users = _.throttle(actually_update_users, 5000);
 | 
						|
 | 
						|
 | 
						|
function actually_update_users_for_search() {
 | 
						|
    actually_update_users();
 | 
						|
    resize.resize_page_components();
 | 
						|
}
 | 
						|
 | 
						|
var update_users_for_search = _.throttle(actually_update_users_for_search, 50);
 | 
						|
 | 
						|
exports.update_huddles = function () {
 | 
						|
    if (page_params.domain === 'mit.edu') {
 | 
						|
        return;  // MIT realm doesn't have a presence list
 | 
						|
    }
 | 
						|
 | 
						|
    var section = $('#group-pm-list').expectOne();
 | 
						|
 | 
						|
    var huddles = exports.get_huddles().slice(0, 10);
 | 
						|
 | 
						|
    if (huddles.length === 0) {
 | 
						|
        section.hide();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    var group_pms = _.map(huddles, function (huddle) {
 | 
						|
        return {
 | 
						|
            emails: huddle,
 | 
						|
            name: exports.full_huddle_name(huddle),
 | 
						|
            fraction_present: exports.huddle_fraction_present(huddle, 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 (huddle) {
 | 
						|
        var count = unread.num_unread_for_person(huddle);
 | 
						|
        stream_list.set_presence_list_count(huddle, count);
 | 
						|
    });
 | 
						|
 | 
						|
    section.show();
 | 
						|
};
 | 
						|
 | 
						|
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/update_active_status',
 | 
						|
        data: {status: (exports.has_focus) ? exports.ACTIVE : exports.IDLE,
 | 
						|
               new_user_input: exports.new_user_input},
 | 
						|
        idempotent: true,
 | 
						|
        success: function (data) {
 | 
						|
            presence_info = {};
 | 
						|
 | 
						|
            // 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;
 | 
						|
 | 
						|
            // Ping returns the active peer list
 | 
						|
            _.each(data.presences, function (presence, this_email) {
 | 
						|
                if (page_params.email !== this_email) {
 | 
						|
                    presence_info[this_email] = status_from_timestamp(data.server_timestamp, presence);
 | 
						|
                }
 | 
						|
            });
 | 
						|
            update_users();
 | 
						|
            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();
 | 
						|
};
 | 
						|
 | 
						|
// 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) {
 | 
						|
    _.each(users, function (presence, email) {
 | 
						|
        if (email === page_params.email) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        presence_info[email] = status_from_timestamp(server_time, presence);
 | 
						|
    });
 | 
						|
 | 
						|
    update_users();
 | 
						|
    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().data('email');
 | 
						|
        if (topPerson !== undefined) {
 | 
						|
            // undefined if there are no results
 | 
						|
            compose.start('private',
 | 
						|
                    {trigger: 'sidebar enter key', "private_message_recipient": topPerson});
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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;
 | 
						|
}
 |