2020-08-01 03:43:15 +02:00
|
|
|
"use strict";
|
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const render_user_presence_row = require("../templates/user_presence_row.hbs");
|
|
|
|
const render_user_presence_rows = require("../templates/user_presence_rows.hbs");
|
2019-07-09 21:24:00 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
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
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
items_to_html(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const user_info = opts.items;
|
|
|
|
const html = render_user_presence_rows({users: user_info});
|
2018-04-20 16:13:39 +02:00
|
|
|
return html;
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
item_to_html(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const html = render_user_presence_row(opts.item);
|
2018-04-20 16:13:39 +02:00
|
|
|
return html;
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
get_li_from_key(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const user_id = opts.key;
|
2020-07-23 01:22:25 +02:00
|
|
|
const container = $(this.container_sel);
|
|
|
|
const sel = this.item_sel + "[data-user-id='" + user_id + "']";
|
2018-08-07 16:59:10 +02:00
|
|
|
return container.find(sel);
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
get_key_from_li(opts) {
|
2020-07-15 01:29:15 +02:00
|
|
|
return parseInt(opts.li.expectOne().attr("data-user-id"), 10);
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-19 23:14:58 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
get_data_from_keys(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const keys = opts.keys;
|
|
|
|
const data = buddy_data.get_items_for_users(keys);
|
2018-07-14 14:06:30 +02:00
|
|
|
return data;
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
compare_function = buddy_data.compare_function;
|
2018-07-16 17:42:20 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
height_to_fill() {
|
2018-07-16 15:16:33 +02:00
|
|
|
// 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.
|
2019-11-02 00:06:25 +01:00
|
|
|
const height = message_viewport.height();
|
2018-07-16 15:16:33 +02:00
|
|
|
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
|
|
|
}
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
class BuddyList extends BuddyListConf {
|
|
|
|
keys = [];
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
populate(opts) {
|
|
|
|
this.render_count = 0;
|
|
|
|
this.container.html("");
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2018-07-14 14:06:30 +02:00
|
|
|
// We rely on our caller to give us items
|
|
|
|
// in already-sorted order.
|
2020-07-23 01:22:25 +02:00
|
|
|
this.keys = opts.keys;
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.fill_screen_with_content();
|
|
|
|
}
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
render_more(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const chunk_size = opts.chunk_size;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const begin = this.render_count;
|
2019-11-02 00:06:25 +01:00
|
|
|
const end = begin + chunk_size;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const more_keys = this.keys.slice(begin, end);
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
if (more_keys.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const items = this.get_data_from_keys({
|
2018-07-16 15:16:33 +02:00
|
|
|
keys: more_keys,
|
|
|
|
});
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const html = this.items_to_html({
|
2020-07-20 22:18:43 +02:00
|
|
|
items,
|
2018-07-14 14:06:30 +02:00
|
|
|
});
|
2020-07-23 01:22:25 +02:00
|
|
|
this.container = $(this.container_sel);
|
|
|
|
this.container.append(html);
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.render_count += more_keys.length;
|
|
|
|
this.update_padding();
|
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
get_items() {
|
|
|
|
const obj = this.container.find(this.item_sel);
|
2020-07-02 01:45:54 +02:00
|
|
|
return obj.map((i, elem) => $(elem));
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-22 14:12:08 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
first_key() {
|
|
|
|
return this.keys[0];
|
|
|
|
}
|
2018-04-21 14:59:03 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
prev_key(key) {
|
|
|
|
const i = this.keys.indexOf(key);
|
2018-07-14 18:08:13 +02:00
|
|
|
|
|
|
|
if (i <= 0) {
|
2018-04-21 14:59:03 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-07-14 18:08:13 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
return this.keys[i - 1];
|
|
|
|
}
|
2018-04-21 14:59:03 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
next_key(key) {
|
|
|
|
const i = this.keys.indexOf(key);
|
2018-07-14 18:08:13 +02:00
|
|
|
|
|
|
|
if (i < 0) {
|
2018-04-21 14:59:03 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-07-14 18:08:13 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
return this.keys[i + 1];
|
|
|
|
}
|
2018-04-21 14:59:03 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
maybe_remove_key(opts) {
|
|
|
|
const pos = this.keys.indexOf(opts.key);
|
2018-07-14 14:06:30 +02:00
|
|
|
|
|
|
|
if (pos < 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.keys.splice(pos, 1);
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
if (pos < this.render_count) {
|
|
|
|
this.render_count -= 1;
|
|
|
|
const li = this.find_li({key: opts.key});
|
2018-07-16 15:16:33 +02:00
|
|
|
li.remove();
|
2020-07-23 01:22:25 +02:00
|
|
|
this.update_padding();
|
2018-07-16 15:16:33 +02:00
|
|
|
}
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
find_position(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const key = opts.key;
|
|
|
|
let i;
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2020-07-23 01:22:25 +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
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
if (this.compare_function(key, list_key) < 0) {
|
2018-07-14 14:06:30 +02:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
return this.keys.length;
|
|
|
|
}
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
force_render(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const pos = opts.pos;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
// Try to render a bit optimistically here.
|
2019-11-02 00:06:25 +01:00
|
|
|
const cushion_size = 3;
|
2020-07-23 01:22:25 +02:00
|
|
|
const chunk_size = pos + cushion_size - this.render_count;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
if (chunk_size <= 0) {
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.error("cannot show key at this position: " + pos);
|
2018-07-16 15:16:33 +02:00
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.render_more({
|
2020-07-20 22:18:43 +02:00
|
|
|
chunk_size,
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
find_li(opts) {
|
2019-12-29 15:07:05 +01:00
|
|
|
const key = opts.key;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
// Try direct DOM lookup first for speed.
|
2020-07-23 01:22:25 +02:00
|
|
|
let li = this.get_li_from_key({
|
2020-07-20 22:18:43 +02:00
|
|
|
key,
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const pos = this.keys.indexOf(key);
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
if (pos < 0) {
|
2020-07-23 01:48:16 +02:00
|
|
|
// TODO: See ListCursor.get_row() for why this is
|
2018-07-16 15:16:33 +02:00
|
|
|
// a bit janky now.
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.force_render({
|
2020-07-20 22:18:43 +02:00
|
|
|
pos,
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
li = this.get_li_from_key({
|
2020-07-20 22:18:43 +02:00
|
|
|
key,
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return li;
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
insert_new_html(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const other_key = opts.other_key;
|
|
|
|
const html = opts.html;
|
|
|
|
const pos = opts.pos;
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2018-07-14 14:06:30 +02:00
|
|
|
if (other_key === undefined) {
|
2020-07-23 01:22:25 +02:00
|
|
|
if (pos === this.render_count) {
|
|
|
|
this.render_count += 1;
|
|
|
|
this.container.append(html);
|
|
|
|
this.update_padding();
|
2018-07-16 15:16:33 +02:00
|
|
|
}
|
2018-07-14 14:06:30 +02:00
|
|
|
return;
|
2018-04-19 14:17:22 +02:00
|
|
|
}
|
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
if (pos < this.render_count) {
|
|
|
|
this.render_count += 1;
|
|
|
|
const li = this.find_li({key: other_key});
|
2018-07-16 15:16:33 +02:00
|
|
|
li.before(html);
|
2020-07-23 01:22:25 +02:00
|
|
|
this.update_padding();
|
2018-07-16 15:16:33 +02:00
|
|
|
}
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
insert_or_move(opts) {
|
2019-12-29 15:07:05 +01:00
|
|
|
const key = opts.key;
|
2019-11-02 00:06:25 +01:00
|
|
|
const item = opts.item;
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.maybe_remove_key({key});
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const pos = this.find_position({
|
2020-07-20 22:18:43 +02:00
|
|
|
key,
|
2018-07-14 14:06:30 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
// Order is important here--get the other_key
|
|
|
|
// before mutating our list. An undefined value
|
|
|
|
// corresponds to appending.
|
2020-07-23 01:22:25 +02:00
|
|
|
const other_key = this.keys[pos];
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.keys.splice(pos, 0, key);
|
2018-07-14 14:06:30 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const html = this.item_to_html({item});
|
|
|
|
this.insert_new_html({
|
2020-07-20 22:18:43 +02:00
|
|
|
pos,
|
|
|
|
html,
|
|
|
|
other_key,
|
2018-07-14 14:06:30 +02:00
|
|
|
});
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
fill_screen_with_content() {
|
|
|
|
let height = this.height_to_fill();
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const elem = ui.get_scroll_element($(this.scroll_container_sel)).expectOne()[0];
|
2018-07-16 15:16:33 +02:00
|
|
|
|
|
|
|
// Add a fudge factor.
|
2018-08-04 09:07:46 +02:00
|
|
|
height += 10;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
while (this.render_count < this.keys.length) {
|
|
|
|
const padding_height = $(this.padding_sel).height();
|
2019-11-02 00:06:25 +01:00
|
|
|
const bottom_offset = elem.scrollHeight - elem.scrollTop - padding_height;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2018-08-04 09:07:46 +02:00
|
|
|
if (bottom_offset > height) {
|
2018-07-16 15:16:33 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const chunk_size = 20;
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
this.render_more({
|
2020-07-20 22:18:43 +02:00
|
|
|
chunk_size,
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
|
|
|
}
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2018-04-20 16:13:39 +02:00
|
|
|
// 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.
|
2020-07-23 01:22:25 +02:00
|
|
|
container = $(this.container_sel);
|
2018-04-20 16:13:39 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
start_scroll_handler() {
|
2018-07-16 15:16:33 +02:00
|
|
|
// We have our caller explicitly call this to make
|
|
|
|
// sure everything's in place.
|
2020-07-23 01:22:25 +02:00
|
|
|
const scroll_container = ui.get_scroll_element($(this.scroll_container_sel));
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-20 21:26:58 +02:00
|
|
|
scroll_container.on("scroll", () => {
|
2020-07-23 01:22:25 +02:00
|
|
|
this.fill_screen_with_content();
|
2018-07-16 15:16:33 +02:00
|
|
|
});
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-07-16 15:16:33 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
update_padding() {
|
2018-07-26 19:50:15 +02:00
|
|
|
padded_widget.update_padding({
|
2020-07-23 01:22:25 +02:00
|
|
|
shown_rows: this.render_count,
|
|
|
|
total_rows: this.keys.length,
|
|
|
|
content_sel: this.container_sel,
|
|
|
|
padding_sel: this.padding_sel,
|
2018-07-26 19:50:15 +02:00
|
|
|
});
|
2020-07-23 01:22:25 +02:00
|
|
|
}
|
2018-08-07 16:22:13 +02:00
|
|
|
}
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2020-07-23 01:22:25 +02:00
|
|
|
const buddy_list = new BuddyList();
|
2018-08-07 16:22:13 +02:00
|
|
|
|
2019-10-25 09:45:13 +02:00
|
|
|
module.exports = buddy_list;
|
2018-04-19 14:17:22 +02:00
|
|
|
|
2018-05-28 08:04:36 +02:00
|
|
|
window.buddy_list = buddy_list;
|