buddy list: Add v1 progresive scrolling.

This version of progressive scrolling lazily
renders buddy list items, but it doesn't
provide the browser with any notion of upcoming
list items, so as you scroll down and the size
of the rendered list grows, the scrollbar shows
you being too close to the bottom.

This maintains 100% coverage on buddy_list.js.
This commit is contained in:
Steve Howell
2018-07-16 13:16:33 +00:00
committed by Tim Abbott
parent 407b13af25
commit 1a84af1e79
5 changed files with 397 additions and 19 deletions

View File

@@ -364,6 +364,8 @@ exports.initialize = function () {
exports.build_user_sidebar();
exports.update_huddles();
buddy_list.start_scroll_handler();
// Let the server know we're here, but pass "false" for
// want_redraw, since we just got all this info in page_params.
focus_ping(false);

View File

@@ -4,6 +4,7 @@ var buddy_list = (function () {
var self = {};
self.container_sel = '#user_presences';
self.scroll_container_sel = '#user_presences';
self.item_sel = 'li.user_sidebar_entry';
self.items_to_html = function (opts) {
@@ -17,7 +18,7 @@ var buddy_list = (function () {
return html;
};
self.find_li = function (opts) {
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);
@@ -36,24 +37,63 @@ var buddy_list = (function () {
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: self.keys,
keys: more_keys,
});
var html = self.items_to_html({
items: items,
});
var html = self.items_to_html({items: items});
self.container = $(self.container_sel);
self.container.html(html);
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.get_items = function () {
@@ -96,8 +136,11 @@ var buddy_list = (function () {
self.keys.splice(pos, 1);
var li = self.find_li({key: opts.key});
li.remove();
if (pos < self.render_count) {
self.render_count -= 1;
var li = self.find_li({key: opts.key});
li.remove();
}
};
self.find_position = function (opts) {
@@ -115,17 +158,77 @@ var buddy_list = (function () {
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) {
self.container.append(html);
if (pos === self.render_count) {
self.render_count += 1;
self.container.append(html);
}
return;
}
var li = self.find_li({key: other_key});
li.before(html);
if (pos < self.render_count) {
self.render_count += 1;
var li = self.find_li({key: other_key});
li.before(html);
}
};
self.insert_or_move = function (opts) {
@@ -147,15 +250,49 @@ var buddy_list = (function () {
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 bottom_offset = elem.scrollHeight - elem.scrollTop;
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();
});
};
return self;
}());

View File

@@ -43,7 +43,10 @@ var list_cursor = function (opts) {
return;
}
var li = opts.list.find_li({key: key});
var li = opts.list.find_li({
key: key,
force_render: true,
});
if (li.length === 0) {
return;