mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
We add a padded div to our container for the buddy list to give scrolling the illusion that we've rendered every list item, while still letting the browser do the heavy lifting instead of trying to fake it out too much.
319 lines
8.1 KiB
JavaScript
319 lines
8.1 KiB
JavaScript
/* eslint indent: "off" */
|
|
|
|
var buddy_list = (function () {
|
|
var self = {};
|
|
|
|
self.container_sel = '#user_presences';
|
|
self.scroll_container_sel = '#buddy_list_wrapper';
|
|
self.item_sel = 'li.user_sidebar_entry';
|
|
self.padding_sel = '#buddy_list_wrapper_padding';
|
|
|
|
self.items_to_html = function (opts) {
|
|
var user_info = opts.items;
|
|
var html = templates.render('user_presence_rows', {users: user_info});
|
|
return html;
|
|
};
|
|
|
|
self.item_to_html = function (opts) {
|
|
var html = templates.render('user_presence_row', opts.item);
|
|
return html;
|
|
};
|
|
|
|
self.get_li_from_key = function (opts) {
|
|
var user_id = opts.key;
|
|
var sel = self.item_sel + "[data-user-id='" + user_id + "']";
|
|
return self.container.find(sel);
|
|
};
|
|
|
|
self.get_key_from_li = function (opts) {
|
|
var user_id = opts.li.expectOne().attr('data-user-id');
|
|
return user_id;
|
|
};
|
|
|
|
self.get_data_from_keys = function (opts) {
|
|
var keys = opts.keys;
|
|
var data = buddy_data.get_items_for_users(keys);
|
|
return data;
|
|
};
|
|
|
|
self.compare_function = buddy_data.compare_function;
|
|
|
|
self.height_to_fill = function () {
|
|
// Because the buddy list gets sized dynamically, we err on the side
|
|
// of using the height of the entire viewport for deciding
|
|
// how much content to render. Even on tall monitors this should
|
|
// still be a significant optimization for orgs with thousands of
|
|
// users.
|
|
var height = message_viewport.height();
|
|
return height;
|
|
};
|
|
|
|
// Try to keep code below this line generic, so that we can
|
|
// extract a widget.
|
|
|
|
self.keys = [];
|
|
|
|
self.populate = function (opts) {
|
|
self.render_count = 0;
|
|
self.container.html('');
|
|
|
|
// We rely on our caller to give us items
|
|
// in already-sorted order.
|
|
self.keys = _.map(opts.keys, function (k) {
|
|
return k.toString();
|
|
});
|
|
|
|
self.fill_screen_with_content();
|
|
};
|
|
|
|
self.render_more = function (opts) {
|
|
var chunk_size = opts.chunk_size;
|
|
|
|
var begin = self.render_count;
|
|
var end = begin + chunk_size;
|
|
|
|
var more_keys = self.keys.slice(begin, end);
|
|
|
|
if (more_keys.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var items = self.get_data_from_keys({
|
|
keys: more_keys,
|
|
});
|
|
|
|
var html = self.items_to_html({
|
|
items: items,
|
|
});
|
|
self.container = $(self.container_sel);
|
|
self.container.append(html);
|
|
|
|
// Invariant: more_keys.length >= items.length.
|
|
// (Usually they're the same, but occasionally keys
|
|
// won't return valid items. Even though we don't
|
|
// actually render these keys, we still "count" them
|
|
// as rendered.
|
|
|
|
self.render_count += more_keys.length;
|
|
self.update_padding();
|
|
};
|
|
|
|
self.get_items = function () {
|
|
var obj = self.container.find(self.item_sel);
|
|
return obj.map(function (i, elem) {
|
|
return $(elem);
|
|
});
|
|
};
|
|
|
|
self.first_key = function () {
|
|
return self.keys[0];
|
|
};
|
|
|
|
self.prev_key = function (key) {
|
|
var i = self.keys.indexOf(key.toString());
|
|
|
|
if (i <= 0) {
|
|
return;
|
|
}
|
|
|
|
return self.keys[i - 1];
|
|
};
|
|
|
|
self.next_key = function (key) {
|
|
var i = self.keys.indexOf(key.toString());
|
|
|
|
if (i < 0) {
|
|
return;
|
|
}
|
|
|
|
return self.keys[i + 1];
|
|
};
|
|
|
|
self.maybe_remove_key = function (opts) {
|
|
var pos = self.keys.indexOf(opts.key);
|
|
|
|
if (pos < 0) {
|
|
return;
|
|
}
|
|
|
|
self.keys.splice(pos, 1);
|
|
|
|
if (pos < self.render_count) {
|
|
self.render_count -= 1;
|
|
var li = self.find_li({key: opts.key});
|
|
li.remove();
|
|
self.update_padding();
|
|
}
|
|
};
|
|
|
|
self.find_position = function (opts) {
|
|
var key = opts.key;
|
|
var i;
|
|
|
|
for (i = 0; i < self.keys.length; i += 1) {
|
|
var list_key = self.keys[i];
|
|
|
|
if (self.compare_function(key, list_key) < 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return self.keys.length;
|
|
};
|
|
|
|
self.force_render = function (opts) {
|
|
var pos = opts.pos;
|
|
|
|
// Try to render a bit optimistically here.
|
|
var cushion_size = 3;
|
|
var chunk_size = pos + cushion_size - self.render_count;
|
|
|
|
if (chunk_size <= 0) {
|
|
blueslip.error('cannot show key at this position: ' + pos);
|
|
}
|
|
|
|
self.render_more({
|
|
chunk_size: chunk_size,
|
|
});
|
|
};
|
|
|
|
self.find_li = function (opts) {
|
|
var key = opts.key.toString();
|
|
|
|
// Try direct DOM lookup first for speed.
|
|
var li = self.get_li_from_key({
|
|
key: key,
|
|
});
|
|
|
|
if (li.length === 1) {
|
|
return li;
|
|
}
|
|
|
|
if (!opts.force_render) {
|
|
// Most callers don't force us to render a list
|
|
// item that wouldn't be on-screen anyway.
|
|
return li;
|
|
}
|
|
|
|
var pos = self.keys.indexOf(key);
|
|
|
|
if (pos < 0) {
|
|
// TODO: See list_cursor.get_row() for why this is
|
|
// a bit janky now.
|
|
return [];
|
|
}
|
|
|
|
self.force_render({
|
|
pos: pos,
|
|
});
|
|
|
|
li = self.get_li_from_key({
|
|
key: key,
|
|
});
|
|
|
|
return li;
|
|
};
|
|
|
|
self.insert_new_html = function (opts) {
|
|
var other_key = opts.other_key;
|
|
var html = opts.html;
|
|
var pos = opts.pos;
|
|
|
|
if (other_key === undefined) {
|
|
if (pos === self.render_count) {
|
|
self.render_count += 1;
|
|
self.container.append(html);
|
|
self.update_padding();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (pos < self.render_count) {
|
|
self.render_count += 1;
|
|
var li = self.find_li({key: other_key});
|
|
li.before(html);
|
|
self.update_padding();
|
|
}
|
|
};
|
|
|
|
self.insert_or_move = function (opts) {
|
|
var key = opts.key.toString();
|
|
var item = opts.item;
|
|
|
|
self.maybe_remove_key({key: key});
|
|
|
|
var pos = self.find_position({
|
|
key: key,
|
|
});
|
|
|
|
// Order is important here--get the other_key
|
|
// before mutating our list. An undefined value
|
|
// corresponds to appending.
|
|
var other_key = self.keys[pos];
|
|
|
|
self.keys.splice(pos, 0, key);
|
|
|
|
var html = self.item_to_html({item: item});
|
|
self.insert_new_html({
|
|
pos: pos,
|
|
html: html,
|
|
other_key: other_key,
|
|
});
|
|
};
|
|
|
|
self.fill_screen_with_content = function () {
|
|
var height_to_fill = self.height_to_fill();
|
|
|
|
var elem = $(self.scroll_container_sel).expectOne()[0];
|
|
|
|
// Add a fudge factor.
|
|
height_to_fill += 10;
|
|
|
|
while (self.render_count < self.keys.length) {
|
|
var padding_height = $(self.padding_sel).height();
|
|
var bottom_offset = elem.scrollHeight - elem.scrollTop - padding_height;
|
|
|
|
if (bottom_offset > height_to_fill) {
|
|
break;
|
|
}
|
|
|
|
var chunk_size = 20;
|
|
|
|
self.render_more({
|
|
chunk_size: chunk_size,
|
|
});
|
|
}
|
|
};
|
|
|
|
// This is a bit of a hack to make sure we at least have
|
|
// an empty list to start, before we get the initial payload.
|
|
self.container = $(self.container_sel);
|
|
|
|
self.start_scroll_handler = function () {
|
|
// We have our caller explicitly call this to make
|
|
// sure everything's in place.
|
|
var scroll_container = $(self.scroll_container_sel);
|
|
|
|
scroll_container.scroll(function () {
|
|
self.fill_screen_with_content();
|
|
});
|
|
};
|
|
|
|
self.update_padding = function () {
|
|
padded_widget.update_padding({
|
|
shown_rows: self.render_count,
|
|
total_rows: self.keys.length,
|
|
content_sel: self.container_sel,
|
|
padding_sel: self.padding_sel,
|
|
});
|
|
};
|
|
|
|
return self;
|
|
}());
|
|
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = buddy_list;
|
|
}
|
|
|
|
window.buddy_list = buddy_list;
|