zulip/static/js/buddy_list.js

314 lines
7.8 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
import render_user_presence_row from "../templates/user_presence_row.hbs";
import render_user_presence_rows from "../templates/user_presence_rows.hbs";
import * as blueslip from "./blueslip";
import * as buddy_data from "./buddy_data";
import * as message_viewport from "./message_viewport";
import * as padded_widget from "./padded_widget";
import * as ui from "./ui";
class BuddyListConf {
container_sel = "#user_presences";
scroll_container_sel = "#buddy_list_wrapper";
item_sel = "li.user_sidebar_entry";
padding_sel = "#buddy_list_wrapper_padding";
2018-04-19 14:17:22 +02:00
items_to_html(opts) {
const user_info = opts.items;
const html = render_user_presence_rows({users: user_info});
return html;
}
item_to_html(opts) {
const html = render_user_presence_row(opts.item);
return html;
}
2018-04-19 14:17:22 +02:00
get_li_from_key(opts) {
const user_id = opts.key;
const $container = $(this.container_sel);
return $container.find(`${this.item_sel}[data-user-id='${CSS.escape(user_id)}']`);
}
2018-04-19 14:17:22 +02:00
get_key_from_li(opts) {
return Number.parseInt(opts.$li.expectOne().attr("data-user-id"), 10);
}
get_data_from_keys(opts) {
const keys = opts.keys;
const data = buddy_data.get_items_for_users(keys);
return data;
}
compare_function = buddy_data.compare_function;
height_to_fill() {
// 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.
const height = message_viewport.height();
return height;
js: Automatically convert _.each to for…of. This commit was automatically generated by the following script, followed by lint --fix and a few small manual lint-related cleanups. import * as babelParser from "recast/parsers/babel"; import * as recast from "recast"; import * as tsParser from "recast/parsers/typescript"; import { builders as b, namedTypes as n } from "ast-types"; import { Context } from "ast-types/lib/path-visitor"; import K from "ast-types/gen/kinds"; import { NodePath } from "ast-types/lib/node-path"; import assert from "assert"; import fs from "fs"; import path from "path"; import process from "process"; const checkExpression = (node: n.Node): node is K.ExpressionKind => n.Expression.check(node); const checkStatement = (node: n.Node): node is K.StatementKind => n.Statement.check(node); for (const file of process.argv.slice(2)) { console.log("Parsing", file); const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), { parser: path.extname(file) === ".ts" ? tsParser : babelParser, }); let changed = false; let inLoop = false; let replaceReturn = false; const visitLoop = (...args: string[]) => function(this: Context, path: NodePath) { for (const arg of args) { this.visit(path.get(arg)); } const old = { inLoop }; inLoop = true; this.visit(path.get("body")); inLoop = old.inLoop; return false; }; recast.visit(ast, { visitDoWhileStatement: visitLoop("test"), visitExpressionStatement(path) { const { expression, comments } = path.node; let valueOnly; if ( n.CallExpression.check(expression) && n.MemberExpression.check(expression.callee) && !expression.callee.computed && n.Identifier.check(expression.callee.object) && expression.callee.object.name === "_" && n.Identifier.check(expression.callee.property) && ["each", "forEach"].includes(expression.callee.property.name) && [2, 3].includes(expression.arguments.length) && checkExpression(expression.arguments[0]) && (n.FunctionExpression.check(expression.arguments[1]) || n.ArrowFunctionExpression.check(expression.arguments[1])) && [1, 2].includes(expression.arguments[1].params.length) && n.Identifier.check(expression.arguments[1].params[0]) && ((valueOnly = expression.arguments[1].params[1] === undefined) || n.Identifier.check(expression.arguments[1].params[1])) && (expression.arguments[2] === undefined || n.ThisExpression.check(expression.arguments[2])) ) { const old = { inLoop, replaceReturn }; inLoop = false; replaceReturn = true; this.visit( path .get("expression") .get("arguments") .get(1) .get("body") ); inLoop = old.inLoop; replaceReturn = old.replaceReturn; const [right, { body, params }] = expression.arguments; const loop = b.forOfStatement( b.variableDeclaration("let", [ b.variableDeclarator( valueOnly ? params[0] : b.arrayPattern([params[1], params[0]]) ), ]), valueOnly ? right : b.callExpression( b.memberExpression(right, b.identifier("entries")), [] ), checkStatement(body) ? body : b.expressionStatement(body) ); loop.comments = comments; path.replace(loop); changed = true; } this.traverse(path); }, visitForStatement: visitLoop("init", "test", "update"), visitForInStatement: visitLoop("left", "right"), visitForOfStatement: visitLoop("left", "right"), visitFunction(path) { this.visit(path.get("params")); const old = { replaceReturn }; replaceReturn = false; this.visit(path.get("body")); replaceReturn = old.replaceReturn; return false; }, visitReturnStatement(path) { if (replaceReturn) { assert(!inLoop); // could use labeled continue if this ever fires const { argument, comments } = path.node; if (argument === null) { const s = b.continueStatement(); s.comments = comments; path.replace(s); } else { const s = b.expressionStatement(argument); s.comments = comments; path.replace(s, b.continueStatement()); } return false; } this.traverse(path); }, visitWhileStatement: visitLoop("test"), }); if (changed) { console.log("Writing", file); fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" }); } } Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
}
}
export class BuddyList extends BuddyListConf {
keys = [];
populate(opts) {
this.render_count = 0;
this.$container.html("");
// We rely on our caller to give us items
// in already-sorted order.
this.keys = opts.keys;
this.fill_screen_with_content();
}
render_more(opts) {
const chunk_size = opts.chunk_size;
const begin = this.render_count;
const end = begin + chunk_size;
const more_keys = this.keys.slice(begin, end);
if (more_keys.length === 0) {
return;
}
const items = this.get_data_from_keys({
keys: more_keys,
});
const html = this.items_to_html({
items,
});
this.$container = $(this.container_sel);
this.$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.
this.render_count += more_keys.length;
this.update_padding();
}
get_items() {
const $obj = this.$container.find(`${this.item_sel}`);
return $obj.map((i, elem) => $(elem));
}
first_key() {
return this.keys[0];
}
prev_key(key) {
const i = this.keys.indexOf(key);
if (i <= 0) {
return undefined;
}
return this.keys[i - 1];
}
next_key(key) {
const i = this.keys.indexOf(key);
if (i < 0) {
return undefined;
}
return this.keys[i + 1];
}
maybe_remove_key(opts) {
const pos = this.keys.indexOf(opts.key);
if (pos < 0) {
return;
}
this.keys.splice(pos, 1);
if (pos < this.render_count) {
this.render_count -= 1;
const $li = this.find_li({key: opts.key});
$li.remove();
this.update_padding();
}
}
find_position(opts) {
const key = opts.key;
let i;
2018-04-19 14:17:22 +02:00
for (i = 0; i < this.keys.length; i += 1) {
const list_key = this.keys[i];
2018-04-19 14:17:22 +02:00
if (this.compare_function(key, list_key) < 0) {
return i;
}
}
return this.keys.length;
}
force_render(opts) {
const pos = opts.pos;
// Try to render a bit optimistically here.
const cushion_size = 3;
const chunk_size = pos + cushion_size - this.render_count;
if (chunk_size <= 0) {
blueslip.error("cannot show key at this position: " + pos);
}
this.render_more({
chunk_size,
});
}
find_li(opts) {
const key = opts.key;
// Try direct DOM lookup first for speed.
let $li = this.get_li_from_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;
}
const pos = this.keys.indexOf(key);
if (pos < 0) {
// TODO: See ListCursor.get_row() for why this is
// a bit janky now.
return [];
}
this.force_render({
pos,
});
$li = this.get_li_from_key({
key,
});
return $li;
}
insert_new_html(opts) {
const other_key = opts.other_key;
const html = opts.html;
const pos = opts.pos;
2018-04-19 14:17:22 +02:00
if (other_key === undefined) {
if (pos === this.render_count) {
this.render_count += 1;
this.$container.append(html);
this.update_padding();
}
return;
2018-04-19 14:17:22 +02:00
}
if (pos < this.render_count) {
this.render_count += 1;
const $li = this.find_li({key: other_key});
$li.before(html);
this.update_padding();
}
}
insert_or_move(opts) {
const key = opts.key;
const item = opts.item;
this.maybe_remove_key({key});
const pos = this.find_position({
key,
});
// Order is important here--get the other_key
// before mutating our list. An undefined value
// corresponds to appending.
const other_key = this.keys[pos];
this.keys.splice(pos, 0, key);
const html = this.item_to_html({item});
this.insert_new_html({
pos,
html,
other_key,
});
}
2018-04-19 14:17:22 +02:00
fill_screen_with_content() {
let height = this.height_to_fill();
const elem = ui.get_scroll_element($(this.scroll_container_sel)).expectOne()[0];
// Add a fudge factor.
height += 10;
while (this.render_count < this.keys.length) {
const padding_height = $(this.padding_sel).height();
const bottom_offset = elem.scrollHeight - elem.scrollTop - padding_height;
if (bottom_offset > height) {
break;
}
const chunk_size = 20;
this.render_more({
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.
$container = $(this.container_sel);
start_scroll_handler() {
// We have our caller explicitly call this to make
// sure everything's in place.
const $scroll_container = ui.get_scroll_element($(this.scroll_container_sel));
$scroll_container.on("scroll", () => {
this.fill_screen_with_content();
});
}
update_padding() {
padded_widget.update_padding({
shown_rows: this.render_count,
total_rows: this.keys.length,
content_sel: this.container_sel,
padding_sel: this.padding_sel,
});
}
}
2018-04-19 14:17:22 +02:00
export const buddy_list = new BuddyList();