mirror of https://github.com/zulip/zulip.git
typeahead_helper: Convert module to typescript.
This commit is contained in:
parent
fdb6f212d0
commit
7154539e80
|
@ -8,28 +8,35 @@ import * as buddy_data from "./buddy_data";
|
|||
import * as compose_state from "./compose_state";
|
||||
import {page_params} from "./page_params";
|
||||
import * as people from "./people";
|
||||
import type {PseudoMentionUser, User} from "./people";
|
||||
import * as pm_conversations from "./pm_conversations";
|
||||
import * as pygments_data from "./pygments_data";
|
||||
import * as recent_senders from "./recent_senders";
|
||||
import * as stream_data from "./stream_data";
|
||||
import * as stream_list_sort from "./stream_list_sort";
|
||||
import type {StreamSubscription} from "./sub_store";
|
||||
import * as user_groups from "./user_groups";
|
||||
import type {UserGroup} from "./user_groups";
|
||||
import * as user_status from "./user_status";
|
||||
import type {UserStatusEmojiInfo} from "./user_status";
|
||||
import * as util from "./util";
|
||||
|
||||
export type UserOrMention = PseudoMentionUser | (User & {is_broadcast: undefined});
|
||||
|
||||
// Returns an array of direct message recipients, removing empty elements.
|
||||
// For example, "a,,b, " => ["a", "b"]
|
||||
export function get_cleaned_pm_recipients(query_string) {
|
||||
export function get_cleaned_pm_recipients(query_string: string): string[] {
|
||||
let recipients = util.extract_pm_recipients(query_string);
|
||||
recipients = recipients.filter((elem) => elem.match(/\S/));
|
||||
return recipients;
|
||||
}
|
||||
|
||||
export function build_highlight_regex(query) {
|
||||
export function build_highlight_regex(query: string): RegExp {
|
||||
const regex = new RegExp("(" + _.escapeRegExp(query) + ")", "ig");
|
||||
return regex;
|
||||
}
|
||||
|
||||
export function highlight_with_escaping_and_regex(regex, item) {
|
||||
export function highlight_with_escaping_and_regex(regex: RegExp, item: string): string {
|
||||
// if regex is empty return entire item highlighted and escaped
|
||||
if (regex.source === "()") {
|
||||
return "<strong>" + Handlebars.Utils.escapeExpression(item) + "</strong>";
|
||||
|
@ -57,7 +64,7 @@ export function highlight_with_escaping_and_regex(regex, item) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function make_query_highlighter(query) {
|
||||
export function make_query_highlighter(query: string): (phrase: string) => string {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const regex = build_highlight_regex(query);
|
||||
|
@ -67,22 +74,48 @@ export function make_query_highlighter(query) {
|
|||
};
|
||||
}
|
||||
|
||||
export function render_typeahead_item(args) {
|
||||
args.has_image = args.img_src !== undefined;
|
||||
args.has_status = args.status_emoji_info !== undefined;
|
||||
args.has_secondary = args.secondary !== undefined;
|
||||
args.has_pronouns = args.pronouns !== undefined;
|
||||
return render_typeahead_list_item(args);
|
||||
type StreamData = {
|
||||
invite_only: boolean;
|
||||
is_web_public: boolean;
|
||||
color: string;
|
||||
name: string;
|
||||
description: string;
|
||||
subscribed: boolean;
|
||||
};
|
||||
|
||||
export function render_typeahead_item(args: {
|
||||
primary?: string;
|
||||
is_person?: boolean;
|
||||
img_src?: string;
|
||||
status_emoji_info?: UserStatusEmojiInfo;
|
||||
secondary?: string | null;
|
||||
pronouns?: string;
|
||||
is_user_group?: boolean;
|
||||
stream?: StreamData;
|
||||
is_unsubscribed?: boolean;
|
||||
emoji_code?: string;
|
||||
}): string {
|
||||
const has_image = args.img_src !== undefined;
|
||||
const has_status = args.status_emoji_info !== undefined;
|
||||
const has_secondary = args.secondary !== undefined;
|
||||
const has_pronouns = args.pronouns !== undefined;
|
||||
return render_typeahead_list_item({
|
||||
...args,
|
||||
has_image,
|
||||
has_status,
|
||||
has_secondary,
|
||||
has_pronouns,
|
||||
});
|
||||
}
|
||||
|
||||
export function render_person(person) {
|
||||
const user_circle_class = buddy_data.get_user_circle_class(person.user_id);
|
||||
if (person.special_item_text) {
|
||||
export function render_person(person: UserOrMention): string {
|
||||
if (person.is_broadcast) {
|
||||
return render_typeahead_item({
|
||||
primary: person.special_item_text,
|
||||
is_person: true,
|
||||
});
|
||||
}
|
||||
const user_circle_class = buddy_data.get_user_circle_class(person.user_id);
|
||||
|
||||
const avatar_url = people.small_avatar_url_for_person(person);
|
||||
|
||||
|
@ -101,13 +134,13 @@ export function render_person(person) {
|
|||
status_emoji_info,
|
||||
should_add_guest_user_indicator: people.should_add_guest_user_indicator(person.user_id),
|
||||
pronouns,
|
||||
secondary: person.delivery_email,
|
||||
};
|
||||
|
||||
typeahead_arguments.secondary = person.delivery_email;
|
||||
return render_typeahead_item(typeahead_arguments);
|
||||
}
|
||||
|
||||
export function render_user_group(user_group) {
|
||||
export function render_user_group(user_group: {name: string; description: string}): string {
|
||||
return render_typeahead_item({
|
||||
primary: user_group.name,
|
||||
secondary: user_group.description,
|
||||
|
@ -115,7 +148,9 @@ export function render_user_group(user_group) {
|
|||
});
|
||||
}
|
||||
|
||||
export function render_person_or_user_group(item) {
|
||||
export function render_person_or_user_group(
|
||||
item: UserGroup | (UserOrMention & {members: undefined}),
|
||||
): string {
|
||||
if (user_groups.is_user_group(item)) {
|
||||
return render_user_group(item);
|
||||
}
|
||||
|
@ -123,7 +158,7 @@ export function render_person_or_user_group(item) {
|
|||
return render_person(item);
|
||||
}
|
||||
|
||||
export function render_stream(stream) {
|
||||
export function render_stream(stream: StreamData): string {
|
||||
let desc = stream.description;
|
||||
const short_desc = desc.slice(0, 35);
|
||||
|
||||
|
@ -138,27 +173,34 @@ export function render_stream(stream) {
|
|||
});
|
||||
}
|
||||
|
||||
export function render_emoji(item) {
|
||||
export function render_emoji(item: {
|
||||
emoji_name: string;
|
||||
emoji_url: string;
|
||||
emoji_code: string;
|
||||
}): string {
|
||||
const args = {
|
||||
is_emoji: true,
|
||||
primary: item.emoji_name.replaceAll("_", " "),
|
||||
};
|
||||
|
||||
if (item.emoji_url) {
|
||||
args.img_src = item.emoji_url;
|
||||
} else {
|
||||
args.emoji_code = item.emoji_code;
|
||||
return render_typeahead_item({
|
||||
...args,
|
||||
img_src: item.emoji_url,
|
||||
});
|
||||
}
|
||||
|
||||
return render_typeahead_item(args);
|
||||
return render_typeahead_item({
|
||||
...args,
|
||||
emoji_code: item.emoji_code,
|
||||
});
|
||||
}
|
||||
|
||||
export function sorter(query, objs, get_item) {
|
||||
export function sorter<T>(query: string, objs: T[], get_item: (x: T) => string): T[] {
|
||||
const results = typeahead.triage(query, objs, get_item);
|
||||
return [...results.matches, ...results.rest];
|
||||
}
|
||||
|
||||
export function compare_by_pms(user_a, user_b) {
|
||||
export function compare_by_pms(user_a: User, user_b: User): number {
|
||||
const count_a = people.get_recipient_count(user_a);
|
||||
const count_b = people.get_recipient_count(user_b);
|
||||
|
||||
|
@ -185,11 +227,11 @@ export function compare_by_pms(user_a, user_b) {
|
|||
}
|
||||
|
||||
export function compare_people_for_relevance(
|
||||
person_a,
|
||||
person_b,
|
||||
tertiary_compare,
|
||||
current_stream_id,
|
||||
) {
|
||||
person_a: UserOrMention,
|
||||
person_b: UserOrMention,
|
||||
tertiary_compare: (user_a: User, user_b: User) => number,
|
||||
current_stream_id?: number,
|
||||
): number {
|
||||
// give preference to "all", "everyone" or "stream"
|
||||
// We use is_broadcast for a quick check. It will
|
||||
// true for all/everyone/stream and undefined (falsy)
|
||||
|
@ -241,7 +283,11 @@ export function compare_people_for_relevance(
|
|||
return tertiary_compare(person_a, person_b);
|
||||
}
|
||||
|
||||
export function sort_people_for_relevance(objs, current_stream_id, current_topic) {
|
||||
export function sort_people_for_relevance(
|
||||
objs: UserOrMention[],
|
||||
current_stream_id: number,
|
||||
current_topic: string,
|
||||
): UserOrMention[] {
|
||||
// If sorting for recipientbox typeahead and not viewing a stream / topic, then current_stream = ""
|
||||
let current_stream = null;
|
||||
if (current_stream_id) {
|
||||
|
@ -271,7 +317,7 @@ export function sort_people_for_relevance(objs, current_stream_id, current_topic
|
|||
return objs;
|
||||
}
|
||||
|
||||
function compare_language_by_popularity(lang_a, lang_b) {
|
||||
function compare_language_by_popularity(lang_a: string, lang_b: string): number {
|
||||
const lang_a_data = pygments_data.langs[lang_a];
|
||||
const lang_b_data = pygments_data.langs[lang_b];
|
||||
|
||||
|
@ -320,7 +366,7 @@ function compare_language_by_popularity(lang_a, lang_b) {
|
|||
|
||||
// This function compares two languages first by their popularity, then if
|
||||
// there is a tie on popularity, then compare alphabetically to break the tie.
|
||||
export function compare_language(lang_a, lang_b) {
|
||||
export function compare_language(lang_a: string, lang_b: string): number {
|
||||
let diff = compare_language_by_popularity(lang_a, lang_b);
|
||||
|
||||
// Check to see if there is a tie. If there is, then use alphabetical order
|
||||
|
@ -332,7 +378,7 @@ export function compare_language(lang_a, lang_b) {
|
|||
return diff;
|
||||
}
|
||||
|
||||
function retain_unique_language_aliases(matches) {
|
||||
function retain_unique_language_aliases(matches: string[]): string[] {
|
||||
// We make the typeahead a little more nicer but only showing one alias per language.
|
||||
// For example if the user searches for prefix "j", then the typeahead list should contain
|
||||
// "javascript" only, and not "js" and "javascript".
|
||||
|
@ -350,7 +396,7 @@ function retain_unique_language_aliases(matches) {
|
|||
return unique_aliases;
|
||||
}
|
||||
|
||||
export function sort_languages(matches, query) {
|
||||
export function sort_languages(matches: string[], query: string): string[] {
|
||||
const results = typeahead.triage(query, matches, (x) => x, compare_language);
|
||||
|
||||
return retain_unique_language_aliases([...results.matches, ...results.rest]);
|
||||
|
@ -363,8 +409,15 @@ export function sort_recipients({
|
|||
current_topic,
|
||||
groups = [],
|
||||
max_num_items = 20,
|
||||
}) {
|
||||
function sort_relevance(items) {
|
||||
}: {
|
||||
users: UserOrMention[];
|
||||
query: string;
|
||||
current_stream_id: number;
|
||||
current_topic: string;
|
||||
groups: UserGroup[];
|
||||
max_num_items: number;
|
||||
}): (UserOrMention | UserGroup)[] {
|
||||
function sort_relevance(items: UserOrMention[]): UserOrMention[] {
|
||||
return sort_people_for_relevance(items, current_stream_id, current_topic);
|
||||
}
|
||||
|
||||
|
@ -374,44 +427,85 @@ export function sort_recipients({
|
|||
|
||||
const groups_results = typeahead.triage(query, groups, (g) => g.name);
|
||||
|
||||
const best_users = () => sort_relevance(users_name_results.matches);
|
||||
const best_groups = () => groups_results.matches;
|
||||
const ok_users = () => sort_relevance(email_results.matches);
|
||||
const worst_users = () => sort_relevance(email_results.rest);
|
||||
const worst_groups = () => groups_results.rest;
|
||||
const best_users = (): UserOrMention[] => sort_relevance(users_name_results.matches);
|
||||
const best_groups = (): UserGroup[] => groups_results.matches;
|
||||
const ok_users = (): UserOrMention[] => sort_relevance(email_results.matches);
|
||||
const worst_users = (): UserOrMention[] => sort_relevance(email_results.rest);
|
||||
const worst_groups = (): UserGroup[] => groups_results.rest;
|
||||
|
||||
const getters = [best_users, best_groups, ok_users, worst_users, worst_groups];
|
||||
|
||||
/*
|
||||
The following optimization is important for large realms.
|
||||
If we know we're only showing 5 suggestions, and we
|
||||
get 5 matches from `best_users`, then we want to avoid
|
||||
calling the expensive sorts for `ok_users` and `worst_users`,
|
||||
since they just get dropped.
|
||||
*/
|
||||
|
||||
let items = [];
|
||||
|
||||
for (const getter of getters) {
|
||||
if (items.length < max_num_items) {
|
||||
items = [...items, ...getter()];
|
||||
}
|
||||
}
|
||||
const getters: (
|
||||
| {
|
||||
getter: () => UserOrMention[];
|
||||
type: "users";
|
||||
}
|
||||
| {
|
||||
getter: () => UserGroup[];
|
||||
type: "groups";
|
||||
}
|
||||
)[] = [
|
||||
{
|
||||
getter: best_users,
|
||||
type: "users",
|
||||
},
|
||||
{
|
||||
getter: best_groups,
|
||||
type: "groups",
|
||||
},
|
||||
{
|
||||
getter: ok_users,
|
||||
type: "users",
|
||||
},
|
||||
{
|
||||
getter: worst_users,
|
||||
type: "users",
|
||||
},
|
||||
{
|
||||
getter: worst_groups,
|
||||
type: "groups",
|
||||
},
|
||||
];
|
||||
|
||||
// We suggest only the first matching stream wildcard mention,
|
||||
// irrespective of how many equivalent stream wildcard mentions match.
|
||||
const recipients = [];
|
||||
const recipients: (UserOrMention | UserGroup)[] = [];
|
||||
let stream_wildcard_mention_included = false;
|
||||
for (const item of items) {
|
||||
const topic_wildcard_mention = item.email === "topic";
|
||||
if (!item.is_broadcast || topic_wildcard_mention || !stream_wildcard_mention_included) {
|
||||
recipients.push(item);
|
||||
if (item.is_broadcast && !topic_wildcard_mention) {
|
||||
stream_wildcard_mention_included = true;
|
||||
|
||||
function add_user_recipients(items: UserOrMention[]): void {
|
||||
for (const item of items) {
|
||||
const topic_wildcard_mention = item.email === "topic";
|
||||
if (!item.is_broadcast || topic_wildcard_mention || !stream_wildcard_mention_included) {
|
||||
recipients.push(item);
|
||||
if (item.is_broadcast && !topic_wildcard_mention) {
|
||||
stream_wildcard_mention_included = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add_group_recipients(items: UserGroup[]): void {
|
||||
for (const item of items) {
|
||||
recipients.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (const getter of getters) {
|
||||
/*
|
||||
The following optimization is important for large realms.
|
||||
If we know we're only showing 5 suggestions, and we
|
||||
get 5 matches from `best_users`, then we want to avoid
|
||||
calling the expensive sorts for `ok_users` and `worst_users`,
|
||||
since they just get dropped.
|
||||
*/
|
||||
if (recipients.length >= max_num_items) {
|
||||
break;
|
||||
}
|
||||
if (getter.type === "users") {
|
||||
add_user_recipients(getter.getter());
|
||||
} else {
|
||||
add_group_recipients(getter.getter());
|
||||
}
|
||||
}
|
||||
|
||||
// We don't push exact matches to the top, like we do with other
|
||||
// typeaheads, because in open organizations, it's not uncommon to
|
||||
// have a bunch of inactive users with display names that are just
|
||||
|
@ -420,7 +514,14 @@ export function sort_recipients({
|
|||
return recipients.slice(0, max_num_items);
|
||||
}
|
||||
|
||||
function slash_command_comparator(slash_command_a, slash_command_b) {
|
||||
type SlashCommand = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function slash_command_comparator(
|
||||
slash_command_a: SlashCommand,
|
||||
slash_command_b: SlashCommand,
|
||||
): number {
|
||||
if (slash_command_a.name < slash_command_b.name) {
|
||||
return -1;
|
||||
} else if (slash_command_a.name > slash_command_b.name) {
|
||||
|
@ -430,7 +531,7 @@ function slash_command_comparator(slash_command_a, slash_command_b) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
export function sort_slash_commands(matches, query) {
|
||||
export function sort_slash_commands(matches: SlashCommand[], query: string): SlashCommand[] {
|
||||
// We will likely want to in the future make this sort the
|
||||
// just-`/` commands by something approximating usefulness.
|
||||
const results = typeahead.triage(query, matches, (x) => x.name, slash_command_comparator);
|
||||
|
@ -439,7 +540,7 @@ export function sort_slash_commands(matches, query) {
|
|||
}
|
||||
|
||||
// Gives stream a score from 0 to 3 based on its activity
|
||||
function activity_score(sub) {
|
||||
function activity_score(sub: StreamSubscription): number {
|
||||
let stream_score = 0;
|
||||
if (!sub.subscribed) {
|
||||
stream_score = -1;
|
||||
|
@ -458,19 +559,22 @@ function activity_score(sub) {
|
|||
// Sort streams by ranking them by activity. If activity is equal,
|
||||
// as defined bv activity_score, decide based on our weekly traffic
|
||||
// stats.
|
||||
export function compare_by_activity(stream_a, stream_b) {
|
||||
export function compare_by_activity(
|
||||
stream_a: StreamSubscription,
|
||||
stream_b: StreamSubscription,
|
||||
): number {
|
||||
let diff = activity_score(stream_b) - activity_score(stream_a);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
diff = (stream_b.stream_weekly_traffic || 0) - (stream_a.stream_weekly_traffic || 0);
|
||||
diff = (stream_b.stream_weekly_traffic ?? 0) - (stream_a.stream_weekly_traffic ?? 0);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
return util.strcmp(stream_a.name, stream_b.name);
|
||||
}
|
||||
|
||||
export function sort_streams(matches, query) {
|
||||
export function sort_streams(matches: StreamSubscription[], query: string): StreamSubscription[] {
|
||||
const name_results = typeahead.triage(query, matches, (x) => x.name, compare_by_activity);
|
||||
const desc_results = typeahead.triage(
|
||||
query,
|
|
@ -2,8 +2,8 @@ import * as blueslip from "./blueslip";
|
|||
import {FoldDict} from "./fold_dict";
|
||||
import * as group_permission_settings from "./group_permission_settings";
|
||||
import {page_params} from "./page_params";
|
||||
import type {User} from "./people";
|
||||
import * as settings_config from "./settings_config";
|
||||
import type {UserOrMention} from "./typeahead_helper";
|
||||
import type {UserGroupUpdateEvent} from "./types";
|
||||
|
||||
export type UserGroup = {
|
||||
|
@ -167,7 +167,9 @@ export function initialize(params: {realm_user_groups: UserGroupRaw[]}): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function is_user_group(item: User | UserGroup): item is UserGroup {
|
||||
export function is_user_group(
|
||||
item: (UserOrMention & {members: undefined}) | UserGroup,
|
||||
): item is UserGroup {
|
||||
return item.members !== undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -722,6 +722,7 @@ test("render_person special_item_text", ({mock_template}) => {
|
|||
is_bot: false,
|
||||
user_id: 7,
|
||||
special_item_text: "special_text",
|
||||
is_broadcast: true,
|
||||
};
|
||||
|
||||
rendered = false;
|
||||
|
|
Loading…
Reference in New Issue