zulip/static/js/buddy_data.js

355 lines
9.8 KiB
JavaScript
Raw Normal View History

"use strict";
2020-08-20 21:24:06 +02:00
const people = require("./people");
const util = require("./util");
2018-04-19 15:46:56 +02:00
/*
This is the main model code for building the buddy list.
We also rely on presence.js to compute the actual presence
for users. We glue in other "people" data and do
filtering/sorting of the data that we'll send into the view.
*/
exports.max_size_before_shrinking = 600;
const fade_config = {
get_user_id(item) {
return item.user_id;
},
fade(item) {
item.faded = true;
},
unfade(item) {
item.faded = false;
},
};
exports.get_user_circle_class = function (user_id) {
const status = exports.buddy_status(user_id);
switch (status) {
case "active":
return "user_circle_green";
case "idle":
return "user_circle_orange";
case "away_them":
case "away_me":
return "user_circle_empty_line";
default:
return "user_circle_empty";
}
};
exports.status_description = function (user_id) {
const status = exports.buddy_status(user_id);
switch (status) {
case "active":
return i18n.t("Active");
case "idle":
return i18n.t("Idle");
case "away_them":
case "away_me":
return i18n.t("Unavailable");
default:
return i18n.t("Offline");
}
};
exports.level = function (user_id) {
if (people.is_my_user_id(user_id)) {
// Always put current user at the top.
return 0;
}
const status = exports.buddy_status(user_id);
switch (status) {
case "active":
return 1;
case "idle":
return 2;
case "away_them":
return 3;
default:
return 3;
2018-04-19 15:46:56 +02:00
}
};
2018-04-19 15:46:56 +02:00
exports.buddy_status = function (user_id) {
if (user_status.is_away(user_id)) {
if (people.is_my_user_id(user_id)) {
return "away_me";
}
return "away_them";
}
// get active/idle/etc.
return presence.get_status(user_id);
};
exports.compare_function = function (a, b) {
const level_a = exports.level(a);
const level_b = exports.level(b);
const diff = level_a - level_b;
2018-04-19 15:46:56 +02:00
if (diff !== 0) {
return diff;
}
// Sort equivalent PM names alphabetically
const person_a = people.get_by_user_id(a);
const person_b = people.get_by_user_id(b);
2018-04-19 15:46:56 +02:00
const full_name_a = person_a ? person_a.full_name : "";
const full_name_b = person_b ? person_b.full_name : "";
2018-04-19 15:46:56 +02:00
return util.strcmp(full_name_a, full_name_b);
};
exports.sort_users = function (user_ids) {
// TODO sort by unread count first, once we support that
user_ids.sort(exports.compare_function);
return user_ids;
};
function filter_user_ids(user_filter_text, user_ids) {
if (user_filter_text === "") {
2018-04-19 15:46:56 +02:00
return user_ids;
}
user_ids = user_ids.filter((user_id) => !people.is_my_user_id(user_id));
let search_terms = user_filter_text.toLowerCase().split(/[|,]+/);
search_terms = search_terms.map((s) => s.trim());
2018-04-19 15:46:56 +02:00
const persons = user_ids.map((user_id) => people.get_by_user_id(user_id));
2018-04-19 15:46:56 +02:00
const user_id_dict = people.filter_people_by_search_terms(persons, search_terms);
return Array.from(user_id_dict.keys());
2018-04-19 15:46:56 +02:00
}
exports.matches_filter = function (user_filter_text, user_id) {
2018-04-19 15:46:56 +02:00
// This is a roundabout way of checking a user if you look
// too hard at it, but it should be fine for now.
return filter_user_ids(user_filter_text, [user_id]).length === 1;
2018-04-19 15:46:56 +02:00
};
function get_num_unread(user_id) {
return unread.num_unread_for_person(user_id.toString());
2018-04-19 15:46:56 +02:00
}
exports.my_user_status = function (user_id) {
if (!people.is_my_user_id(user_id)) {
return;
}
if (user_status.is_away(user_id)) {
return i18n.t("(unavailable)");
}
return i18n.t("(you)");
};
exports.user_last_seen_time_status = function (user_id) {
const status = presence.get_status(user_id);
if (status === "active") {
return i18n.t("Active now");
}
if (page_params.realm_is_zephyr_mirror_realm) {
// We don't send presence data to clients in Zephyr mirroring realms
return i18n.t("Unknown");
}
// There are situations where the client has incomplete presence
// history on a user. This can happen when users are deactivated,
// or when they just haven't been present in a long time (and we
// may have queries on presence that go back only N weeks).
//
// We give the somewhat vague status of "Unknown" for these users.
const last_active_date = presence.last_active_date(user_id);
if (last_active_date === undefined) {
return i18n.t("More than 2 weeks ago");
}
return timerender.last_seen_status_from_date(last_active_date.clone());
};
2018-04-19 15:46:56 +02:00
exports.info_for = function (user_id) {
const user_circle_class = exports.get_user_circle_class(user_id);
const person = people.get_by_user_id(user_id);
const my_user_status = exports.my_user_status(user_id);
const user_circle_status = exports.status_description(user_id);
2018-04-19 15:46:56 +02:00
return {
href: hash_util.pm_with_uri(person.email),
2018-04-19 15:46:56 +02:00
name: person.full_name,
user_id,
my_user_status,
is_current_user: people.is_my_user_id(user_id),
2018-04-19 15:46:56 +02:00
num_unread: get_num_unread(user_id),
user_circle_class,
user_circle_status,
};
};
function get_last_seen(active_status, last_seen) {
if (active_status === "active") {
return last_seen;
}
const last_seen_text = i18n.t("Last active: __last_seen__", {last_seen});
return last_seen_text;
}
exports.get_title_data = function (user_ids_string, is_group) {
if (is_group === true) {
// For groups, just return a string with recipient names.
return {
first_line: people.get_recipients(user_ids_string),
second_line: "",
third_line: "",
};
}
// Since it's not a group, user_ids_string is a single user ID.
const user_id = parseInt(user_ids_string, 10);
const person = people.get_by_user_id(user_id);
if (person.is_bot) {
const bot_owner = people.get_bot_owner_user(person);
if (bot_owner) {
const bot_owner_name = i18n.t("Owner: __name__", {name: bot_owner.full_name});
return {
first_line: person.full_name,
second_line: bot_owner_name,
third_line: "",
};
}
// Bot does not have an owner.
return {
first_line: person.full_name,
second_line: "",
third_line: "",
};
}
// For buddy list and individual PMS. Since is_group=False, it's
// a single, human, user.
const active_status = presence.get_status(user_id);
const last_seen = exports.user_last_seen_time_status(user_id);
// Users has a status.
if (user_status.get_status_text(user_id)) {
return {
first_line: person.full_name,
second_line: user_status.get_status_text(user_id),
third_line: get_last_seen(active_status, last_seen),
};
}
// Users does not have a status.
return {
first_line: person.full_name,
second_line: get_last_seen(active_status, last_seen),
third_line: "",
2018-04-19 15:46:56 +02:00
};
};
exports.get_item = function (user_id) {
const info = exports.info_for(user_id);
compose_fade.update_user_info([info], fade_config);
return info;
};
function user_is_recently_active(user_id) {
// return true if the user has a green/orange circle
return exports.level(user_id) <= 2;
}
function maybe_shrink_list(user_ids, user_filter_text) {
if (user_ids.length <= exports.max_size_before_shrinking) {
return user_ids;
}
if (user_filter_text) {
// If the user types something, we want to show all
// users matching the text, even if they have not been
// online recently.
// For super common letters like "s", we may
// eventually want to filter down to only users that
// are in presence.get_user_ids().
return user_ids;
}
user_ids = user_ids.filter(user_is_recently_active);
return user_ids;
}
function get_user_id_list(user_filter_text) {
let user_ids;
2018-04-19 15:46:56 +02:00
if (user_filter_text) {
2018-04-19 15:46:56 +02:00
// If there's a filter, select from all users, not just those
// recently active.
user_ids = filter_user_ids(user_filter_text, people.get_active_user_ids());
2018-04-19 15:46:56 +02:00
} else {
// From large realms, the user_ids in presence may exclude
// users who have been idle more than three weeks. When the
// filter text is blank, we show only those recently active users.
user_ids = presence.get_user_ids();
}
user_ids = user_ids.filter((user_id) => {
const person = people.get_by_user_id(user_id);
if (!person) {
blueslip.warn("Got user_id in presence but not people: " + user_id);
return false;
}
// if the user is bot, do not show in presence data.
return !person.is_bot;
});
return user_ids;
}
exports.get_filtered_and_sorted_user_ids = function (user_filter_text) {
let user_ids;
user_ids = get_user_id_list(user_filter_text);
user_ids = maybe_shrink_list(user_ids, user_filter_text);
return exports.sort_users(user_ids);
};
exports.get_items_for_users = function (user_ids) {
js: Convert _.map(a, …) to a.map(…). And convert the corresponding function expressions to arrow style while we’re here. 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 K from "ast-types/gen/kinds"; 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); 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; recast.visit(ast, { visitCallExpression(path) { const { callee, arguments: args } = path.node; if ( n.MemberExpression.check(callee) && !callee.computed && n.Identifier.check(callee.object) && callee.object.name === "_" && n.Identifier.check(callee.property) && callee.property.name === "map" && args.length === 2 && checkExpression(args[0]) && checkExpression(args[1]) ) { const [arr, fn] = args; path.replace( b.callExpression(b.memberExpression(arr, b.identifier("map")), [ n.FunctionExpression.check(fn) || n.ArrowFunctionExpression.check(fn) ? b.arrowFunctionExpression( fn.params, n.BlockStatement.check(fn.body) && fn.body.body.length === 1 && n.ReturnStatement.check(fn.body.body[0]) ? fn.body.body[0].argument || b.identifier("undefined") : fn.body ) : fn, ]) ); changed = true; } this.traverse(path); }, }); 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-08 02:43:49 +01:00
const user_info = user_ids.map(exports.info_for);
compose_fade.update_user_info(user_info, fade_config);
2018-04-19 15:46:56 +02:00
return user_info;
};
exports.huddle_fraction_present = function (huddle) {
const user_ids = huddle.split(",").map((s) => parseInt(s, 10));
let num_present = 0;
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
for (const user_id of user_ids) {
if (presence.is_active(user_id)) {
num_present += 1;
}
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
}
if (num_present === user_ids.length) {
return 1;
} else if (num_present !== 0) {
return 0.5;
}
return;
};
window.buddy_data = exports;