mirror of https://github.com/zulip/zulip.git
composebox_typeahead: Convert module to typescript.
This commit is contained in:
parent
7f9361a865
commit
96c9950115
|
@ -104,7 +104,7 @@ places:
|
|||
- The frontend Markdown processor (`web/src/markdown.ts` and sometimes
|
||||
`web/third/marked/lib/marked.js`), or `markdown.contains_backend_only_syntax` if
|
||||
your changes won't be supported in the frontend processor.
|
||||
- If desired, the typeahead logic in `web/src/composebox_typeahead.js`.
|
||||
- If desired, the typeahead logic in `web/src/composebox_typeahead.ts`.
|
||||
- The test suite, probably via adding entries to `zerver/tests/fixtures/markdown_test_cases.json`.
|
||||
- The in-app Markdown documentation (`markdown_help_rows` in `web/src/info_overlay.ts`).
|
||||
- The list of changes to Markdown at the end of this document.
|
||||
|
|
|
@ -52,4 +52,4 @@ send a message with the raw content.
|
|||
## Typeahead
|
||||
|
||||
Typeahead for both slash commands (and widgets) is implemented
|
||||
via the `slash_commands` object in `web/src/composebox_typeahead.js`.
|
||||
via the `slash_commands` object in `web/src/composebox_typeahead.ts`.
|
||||
|
|
|
@ -82,7 +82,7 @@ EXEMPT_FILES = make_set(
|
|||
"web/src/compose_tooltips.ts",
|
||||
"web/src/compose_ui.ts",
|
||||
"web/src/compose_validate.ts",
|
||||
"web/src/composebox_typeahead.js",
|
||||
"web/src/composebox_typeahead.ts",
|
||||
"web/src/condense.ts",
|
||||
"web/src/confirm_dialog.ts",
|
||||
"web/src/copied_tooltip.ts",
|
||||
|
|
|
@ -30,7 +30,7 @@ export const popular_emojis = [
|
|||
|
||||
const unicode_marks = /\p{M}/gu;
|
||||
|
||||
type Emoji =
|
||||
export type Emoji =
|
||||
| {
|
||||
emoji_name: string;
|
||||
reaction_type: "realm_emoji" | "zulip_extra_emoji";
|
||||
|
|
|
@ -96,7 +96,7 @@ export function create_message_object(message_content = compose_state.message_co
|
|||
message.topic = "";
|
||||
|
||||
if (message.type === "private") {
|
||||
// TODO: this should be collapsed with the code in composebox_typeahead.js
|
||||
// TODO: this should be collapsed with the code in composebox_typeahead.ts
|
||||
const recipient = compose_state.private_message_recipient();
|
||||
const emails = util.extract_pm_recipients(recipient);
|
||||
message.to = emails;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import type {Typeahead} from "./bootstrap_typeahead";
|
||||
import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util";
|
||||
import * as common from "./common";
|
||||
import type {TypeaheadSuggestion} from "./composebox_typeahead";
|
||||
import {$t} from "./i18n";
|
||||
import * as loading from "./loading";
|
||||
import * as markdown from "./markdown";
|
||||
|
@ -59,10 +60,10 @@ type SelectedLinesSections = {
|
|||
export let compose_spinner_visible = false;
|
||||
export let shift_pressed = false; // true or false
|
||||
export let code_formatting_button_triggered = false; // true or false
|
||||
export let compose_textarea_typeahead: Typeahead<object> | undefined;
|
||||
export let compose_textarea_typeahead: Typeahead<TypeaheadSuggestion> | undefined;
|
||||
let full_size_status = false; // true or false
|
||||
|
||||
export function set_compose_textarea_typeahead(typeahead: Typeahead<object>): void {
|
||||
export function set_compose_textarea_typeahead(typeahead: Typeahead<TypeaheadSuggestion>): void {
|
||||
compose_textarea_typeahead = typeahead;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import $ from "jquery";
|
||||
import _ from "lodash";
|
||||
import assert from "minimalistic-assert";
|
||||
|
||||
import * as typeahead from "../shared/src/typeahead";
|
||||
import type {Emoji, EmojiSuggestion} from "../shared/src/typeahead";
|
||||
import render_topic_typeahead_hint from "../templates/topic_typeahead_hint.hbs";
|
||||
|
||||
import {Typeahead} from "./bootstrap_typeahead";
|
||||
import type {TypeaheadInputElement} from "./bootstrap_typeahead";
|
||||
import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util";
|
||||
import * as compose_pm_pill from "./compose_pm_pill";
|
||||
import * as compose_state from "./compose_state";
|
||||
import * as compose_ui from "./compose_ui";
|
||||
import * as compose_validate from "./compose_validate";
|
||||
import * as emoji from "./emoji";
|
||||
import type {EmojiDict} from "./emoji";
|
||||
import * as flatpickr from "./flatpickr";
|
||||
import {$t} from "./i18n";
|
||||
import * as keydown_util from "./keydown_util";
|
||||
|
@ -18,17 +22,23 @@ import * as message_store from "./message_store";
|
|||
import * as muted_users from "./muted_users";
|
||||
import {page_params} from "./page_params";
|
||||
import * as people from "./people";
|
||||
import type {PseudoMentionUser, User} from "./people";
|
||||
import * as realm_playground from "./realm_playground";
|
||||
import * as rows from "./rows";
|
||||
import * as settings_data from "./settings_data";
|
||||
import {realm} from "./state_data";
|
||||
import * as stream_data from "./stream_data";
|
||||
import type {StreamPillData} from "./stream_pill";
|
||||
import * as stream_topic_history from "./stream_topic_history";
|
||||
import * as stream_topic_history_util from "./stream_topic_history_util";
|
||||
import * as timerender from "./timerender";
|
||||
import * as typeahead_helper from "./typeahead_helper";
|
||||
import type {UserOrMention, UserOrMentionPillData} from "./typeahead_helper";
|
||||
import type {UserGroupPillData} from "./user_group_pill";
|
||||
import * as user_groups from "./user_groups";
|
||||
import type {UserGroup} from "./user_groups";
|
||||
import * as user_pill from "./user_pill";
|
||||
import type {UserPillData} from "./user_pill";
|
||||
import {user_settings} from "./user_settings";
|
||||
|
||||
// **********************************
|
||||
|
@ -42,41 +52,98 @@ import {user_settings} from "./user_settings";
|
|||
// highlighter that escapes (i.e. one that calls
|
||||
// typeahead_helper.highlight_with_escaping).
|
||||
|
||||
// ---------------- TYPE DECLARATIONS ----------------
|
||||
// There are many types of suggestions that can show
|
||||
// up in the composebox typeahead, but they are never
|
||||
// mixed together. So a user can see a list of stream
|
||||
// suggestions in one situation, and a list of user
|
||||
// suggestions in another, but never both at the same
|
||||
// time.
|
||||
// We use types with a "type" field to keep track
|
||||
// of and differentiate each kind of suggestion,
|
||||
// because we handle multiple kinds of suggestions
|
||||
// within shared code blocks.
|
||||
type SlashCommand = {
|
||||
text: string;
|
||||
name: string;
|
||||
aliases: NamedCurve;
|
||||
placeholder?: string;
|
||||
};
|
||||
export type SlashCommandSuggestion = SlashCommand & {type: "slash"};
|
||||
|
||||
export type LanguageSuggestion = {
|
||||
language: string;
|
||||
type: "syntax";
|
||||
};
|
||||
|
||||
type TopicSuggestion = {
|
||||
topic: string;
|
||||
type: "topic_list";
|
||||
};
|
||||
|
||||
type TimeJumpSuggestion = {
|
||||
message: string;
|
||||
type: "time_jump";
|
||||
};
|
||||
|
||||
type TopicJumpSuggestion = {
|
||||
message: string;
|
||||
type: "topic_jump";
|
||||
};
|
||||
|
||||
export type TypeaheadSuggestion =
|
||||
| UserGroupPillData
|
||||
| UserOrMentionPillData
|
||||
| StreamPillData
|
||||
| TopicJumpSuggestion
|
||||
| TimeJumpSuggestion
|
||||
| LanguageSuggestion
|
||||
| TopicSuggestion
|
||||
| EmojiSuggestion
|
||||
| SlashCommandSuggestion;
|
||||
|
||||
// This is what we use for direct message/compose typeaheads.
|
||||
// We export it to allow tests to mock it.
|
||||
export const max_num_items = 8;
|
||||
|
||||
export let emoji_collection = [];
|
||||
export let emoji_collection: Emoji[] = [];
|
||||
|
||||
let completing;
|
||||
let token;
|
||||
// This has mostly been replaced with `type` fields on
|
||||
// the typeahead items, but is still used for the stream>topic
|
||||
// flow and for `get_header_html`. It would be great if we could
|
||||
// get rid of it altogether.
|
||||
let completing: string | null;
|
||||
let token: string;
|
||||
|
||||
export function get_or_set_token_for_testing(val) {
|
||||
export function get_or_set_token_for_testing(val?: string): string {
|
||||
if (val !== undefined) {
|
||||
token = val;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export function get_or_set_completing_for_tests(val) {
|
||||
export function get_or_set_completing_for_tests(val?: string): string | null {
|
||||
if (val !== undefined) {
|
||||
completing = val;
|
||||
}
|
||||
return completing;
|
||||
}
|
||||
|
||||
export function update_emoji_data(initial_emojis) {
|
||||
export function update_emoji_data(initial_emojis: EmojiDict[]): void {
|
||||
emoji_collection = [];
|
||||
for (const emoji_dict of initial_emojis) {
|
||||
const {reaction_type} = emoji.get_emoji_details_by_name(emoji_dict.name);
|
||||
if (emoji_dict.is_realm_emoji === true) {
|
||||
if (emoji_dict.is_realm_emoji) {
|
||||
assert(reaction_type !== "unicode_emoji");
|
||||
emoji_collection.push({
|
||||
reaction_type,
|
||||
emoji_name: emoji_dict.name,
|
||||
emoji_url: emoji_dict.url,
|
||||
is_realm_emoji: true,
|
||||
emoji_code: undefined,
|
||||
});
|
||||
} else {
|
||||
assert(reaction_type === "unicode_emoji");
|
||||
for (const alias of emoji_dict.aliases) {
|
||||
emoji_collection.push({
|
||||
reaction_type,
|
||||
|
@ -89,36 +156,43 @@ export function update_emoji_data(initial_emojis) {
|
|||
}
|
||||
}
|
||||
|
||||
export function topics_seen_for(stream_id) {
|
||||
export function topics_seen_for(stream_id?: number): string[] {
|
||||
if (!stream_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch topic history from the server, in case we will need it soon.
|
||||
stream_topic_history_util.get_server_history(stream_id, () => {});
|
||||
stream_topic_history_util.get_server_history(stream_id, () => {
|
||||
// We'll use the extended results in the next keypress, but we
|
||||
// don't try to live-update what's already shown, because the
|
||||
// click target moving can be annoying if onewas about to
|
||||
// select/click an option.
|
||||
});
|
||||
return stream_topic_history.get_recent_topic_names(stream_id);
|
||||
}
|
||||
|
||||
export function get_language_matcher(query) {
|
||||
export function get_language_matcher(query: string): (language: string) => boolean {
|
||||
query = query.toLowerCase();
|
||||
return function (lang) {
|
||||
return lang.includes(query);
|
||||
return function (language: string): boolean {
|
||||
return language.includes(query);
|
||||
};
|
||||
}
|
||||
|
||||
export function get_stream_or_user_group_matcher(query) {
|
||||
export function get_stream_or_user_group_matcher(
|
||||
query: string,
|
||||
): (user_group_or_stream: UserGroupPillData | StreamPillData) => boolean {
|
||||
// Case-insensitive.
|
||||
query = typeahead.clean_query_lowercase(query);
|
||||
|
||||
return function (user_group_or_stream) {
|
||||
return function (user_group_or_stream: UserGroupPillData | StreamPillData) {
|
||||
return typeahead_helper.query_matches_name(query, user_group_or_stream);
|
||||
};
|
||||
}
|
||||
|
||||
export function get_slash_matcher(query) {
|
||||
export function get_slash_matcher(query: string): (item: SlashCommand) => boolean {
|
||||
query = typeahead.clean_query_lowercase(query);
|
||||
|
||||
return function (item) {
|
||||
return function (item: SlashCommand) {
|
||||
return (
|
||||
typeahead.query_matches_string_in_order(query, item.name, " ") ||
|
||||
typeahead.query_matches_string_in_order(query, item.aliases, " ")
|
||||
|
@ -126,19 +200,15 @@ export function get_slash_matcher(query) {
|
|||
};
|
||||
}
|
||||
|
||||
function get_topic_matcher(query) {
|
||||
function get_topic_matcher(query: string): (topic: string) => boolean {
|
||||
query = typeahead.clean_query_lowercase(query);
|
||||
|
||||
return function (topic) {
|
||||
const obj = {
|
||||
topic,
|
||||
};
|
||||
|
||||
return typeahead.query_matches_string_in_order(query, obj.topic, " ");
|
||||
return function (topic: string): boolean {
|
||||
return typeahead.query_matches_string_in_order(query, topic, " ");
|
||||
};
|
||||
}
|
||||
|
||||
export function should_enter_send(e) {
|
||||
export function should_enter_send(e: JQuery.KeyDownEvent): boolean {
|
||||
const has_non_shift_modifier_key = e.ctrlKey || e.metaKey || e.altKey;
|
||||
const has_modifier_key = e.shiftKey || has_non_shift_modifier_key;
|
||||
let this_enter_sends;
|
||||
|
@ -160,13 +230,18 @@ export function should_enter_send(e) {
|
|||
return this_enter_sends;
|
||||
}
|
||||
|
||||
function handle_bulleting_or_numbering($textarea, e) {
|
||||
function handle_bulleting_or_numbering(
|
||||
$textarea: JQuery<HTMLTextAreaElement>,
|
||||
e: JQuery.KeyDownEvent,
|
||||
): void {
|
||||
// We only want this functionality if the cursor is not in a code block
|
||||
if (compose_ui.cursor_inside_code_block($textarea)) {
|
||||
return;
|
||||
}
|
||||
// handles automatic insertion or removal of bulleting or numbering
|
||||
const before_text = split_at_cursor($textarea.val(), $textarea)[0];
|
||||
const val = $textarea.val();
|
||||
assert(val !== undefined);
|
||||
const before_text = split_at_cursor(val, $textarea)[0];
|
||||
const previous_line = bulleted_numbered_list_util.get_last_line(before_text);
|
||||
let to_append = "";
|
||||
// if previous line was bulleted, automatically add a bullet to the new line
|
||||
|
@ -207,7 +282,7 @@ function handle_bulleting_or_numbering($textarea, e) {
|
|||
e.preventDefault();
|
||||
}
|
||||
|
||||
export function handle_enter($textarea, e) {
|
||||
export function handle_enter($textarea: JQuery<HTMLTextAreaElement>, e: JQuery.KeyDownEvent): void {
|
||||
// Used only if Enter doesn't send. We need to emulate the
|
||||
// browser's native "Enter" behavior because this code path
|
||||
// includes `Ctrl+Enter` and other modifier key variants that
|
||||
|
@ -240,17 +315,20 @@ export function handle_enter($textarea, e) {
|
|||
// We can't focus at the time of keydown because we need to wait for typeahead.
|
||||
// And we can't compute where to focus at the time of keyup because only the keydown
|
||||
// has reliable information about whether it was a Tab or a Shift+Tab.
|
||||
let $nextFocus = false;
|
||||
let $nextFocus: JQuery | undefined;
|
||||
|
||||
function handle_keydown(e, {on_enter_send}) {
|
||||
function handle_keydown(
|
||||
e: JQuery.KeyDownEvent,
|
||||
on_enter_send: (scheduling_message?: boolean) => boolean | undefined,
|
||||
): void {
|
||||
const key = e.key;
|
||||
|
||||
if (keydown_util.is_enter_event(e) || (key === "Tab" && !e.shiftKey)) {
|
||||
// Enter key or Tab key
|
||||
let target_sel;
|
||||
|
||||
if (e.target.id) {
|
||||
target_sel = `#${CSS.escape(e.target.id)}`;
|
||||
const target_id = $(e.target).attr("id");
|
||||
if (target_id) {
|
||||
target_sel = `#${CSS.escape(target_id)}`;
|
||||
}
|
||||
|
||||
const on_topic = target_sel === "input#stream_message_recipient_topic";
|
||||
|
@ -297,26 +375,26 @@ function handle_keydown(e, {on_enter_send}) {
|
|||
}
|
||||
}
|
||||
|
||||
function handle_keyup(e) {
|
||||
function handle_keyup(e: JQuery.KeyUpEvent): void {
|
||||
if (
|
||||
// Enter key or Tab key
|
||||
(keydown_util.is_enter_event(e) || (e.key === "Tab" && !e.shiftKey)) &&
|
||||
$nextFocus
|
||||
$nextFocus !== undefined
|
||||
) {
|
||||
$nextFocus.trigger("focus");
|
||||
$nextFocus = false;
|
||||
$nextFocus = undefined;
|
||||
|
||||
// Prevent the form from submitting
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
export function split_at_cursor(query, $input) {
|
||||
export function split_at_cursor(query: string, $input: JQuery): [string, string] {
|
||||
const cursor = $input.caret();
|
||||
return [query.slice(0, cursor), query.slice(cursor)];
|
||||
}
|
||||
|
||||
export function tokenize_compose_str(s) {
|
||||
export function tokenize_compose_str(s: string): string {
|
||||
// This basically finds a token like "@alic" or
|
||||
// "#Veron" as close to the end of the string as it
|
||||
// can find it. It wants to find white space or
|
||||
|
@ -384,7 +462,7 @@ export function tokenize_compose_str(s) {
|
|||
return "";
|
||||
}
|
||||
|
||||
function get_wildcard_string(mention) {
|
||||
function get_wildcard_string(mention: string): string {
|
||||
if (compose_state.get_message_type() === "private") {
|
||||
return $t({defaultMessage: "Notify recipients"});
|
||||
}
|
||||
|
@ -394,8 +472,8 @@ function get_wildcard_string(mention) {
|
|||
return $t({defaultMessage: "Notify channel"});
|
||||
}
|
||||
|
||||
export function broadcast_mentions() {
|
||||
let wildcard_mention_array = [];
|
||||
export function broadcast_mentions(): PseudoMentionUser[] {
|
||||
let wildcard_mention_array: string[] = [];
|
||||
if (compose_state.get_message_type() === "private") {
|
||||
wildcard_mention_array = ["all", "everyone"];
|
||||
} else if (compose_validate.stream_wildcard_mention_allowed()) {
|
||||
|
@ -422,7 +500,7 @@ export function broadcast_mentions() {
|
|||
}));
|
||||
}
|
||||
|
||||
function filter_mention_name(current_token) {
|
||||
function filter_mention_name(current_token: string): string | undefined {
|
||||
if (current_token.startsWith("**")) {
|
||||
current_token = current_token.slice(2);
|
||||
} else if (current_token.startsWith("*")) {
|
||||
|
@ -439,7 +517,7 @@ function filter_mention_name(current_token) {
|
|||
return current_token;
|
||||
}
|
||||
|
||||
function should_show_custom_query(query, items) {
|
||||
function should_show_custom_query(query: string, items: string[]): boolean {
|
||||
// returns true if the custom query doesn't match one of the
|
||||
// choices in the items list.
|
||||
if (!query) {
|
||||
|
@ -483,19 +561,28 @@ export const slash_commands = [
|
|||
},
|
||||
];
|
||||
|
||||
export const all_slash_commands = [...dev_only_slash_commands, ...slash_commands];
|
||||
export const all_slash_commands: SlashCommand[] = [...dev_only_slash_commands, ...slash_commands];
|
||||
|
||||
export function filter_and_sort_mentions(is_silent, query, opts) {
|
||||
opts = {
|
||||
export function filter_and_sort_mentions(
|
||||
is_silent: boolean,
|
||||
query: string,
|
||||
opts: {
|
||||
stream_id: number | undefined;
|
||||
topic: string;
|
||||
},
|
||||
): (UserGroupPillData | UserOrMentionPillData)[] {
|
||||
return get_person_suggestions(query, {
|
||||
want_broadcast: !is_silent,
|
||||
filter_pills: false,
|
||||
filter_groups_for_mention: !is_silent,
|
||||
...opts,
|
||||
};
|
||||
return get_person_suggestions(query, opts);
|
||||
}).map((item) => ({
|
||||
...item,
|
||||
is_silent,
|
||||
}));
|
||||
}
|
||||
|
||||
export function get_pm_people(query) {
|
||||
export function get_pm_people(query: string): (UserGroupPillData | UserPillData)[] {
|
||||
const opts = {
|
||||
want_broadcast: false,
|
||||
filter_pills: true,
|
||||
|
@ -503,13 +590,41 @@ export function get_pm_people(query) {
|
|||
topic: compose_state.topic(),
|
||||
filter_groups_for_guests: true,
|
||||
};
|
||||
return get_person_suggestions(query, opts);
|
||||
const suggestions = get_person_suggestions(query, opts);
|
||||
// We know these aren't mentions because `want_broadcast` was `false`.
|
||||
// TODO: In the future we should separate user and mention so we don't have
|
||||
// to do this.
|
||||
const user_suggestions: (UserGroupPillData | UserPillData)[] = [];
|
||||
for (const suggestion of suggestions) {
|
||||
if (suggestion.type === "user_or_mention") {
|
||||
assert(suggestion.is_broadcast === undefined);
|
||||
user_suggestions.push({
|
||||
...suggestion,
|
||||
type: "user",
|
||||
});
|
||||
} else {
|
||||
user_suggestions.push(suggestion);
|
||||
}
|
||||
}
|
||||
return user_suggestions;
|
||||
}
|
||||
|
||||
export function get_person_suggestions(query, opts) {
|
||||
type PersonSuggestionOpts = {
|
||||
want_broadcast: boolean;
|
||||
filter_pills: boolean;
|
||||
stream_id: number | undefined;
|
||||
topic: string;
|
||||
filter_groups_for_guests?: boolean;
|
||||
filter_groups_for_mention?: boolean;
|
||||
};
|
||||
|
||||
export function get_person_suggestions(
|
||||
query: string,
|
||||
opts: PersonSuggestionOpts,
|
||||
): (UserOrMentionPillData | UserGroupPillData)[] {
|
||||
query = typeahead.clean_query_lowercase(query);
|
||||
|
||||
function filter_persons(all_persons) {
|
||||
function filter_persons(all_persons: User[]): UserOrMentionPillData[] {
|
||||
let persons;
|
||||
|
||||
if (opts.filter_pills) {
|
||||
|
@ -519,15 +634,15 @@ export function get_person_suggestions(query, opts) {
|
|||
}
|
||||
// Exclude muted users from typeaheads.
|
||||
persons = muted_users.filter_muted_users(persons);
|
||||
let user_or_mention_list: UserOrMention[] = persons.map((person) => ({
|
||||
...person,
|
||||
is_broadcast: undefined,
|
||||
}));
|
||||
|
||||
if (opts.want_broadcast) {
|
||||
persons = [...persons, ...broadcast_mentions()];
|
||||
user_or_mention_list = [...user_or_mention_list, ...broadcast_mentions()];
|
||||
}
|
||||
// `sort_recipients` and other functions like `user_pill.get_user_ids`
|
||||
// are shared with the pill typeahead which has only users, and we
|
||||
// need a way to differentiate these mentons-or-users from just users,
|
||||
// to help with typing.
|
||||
const person_items = persons.map((person) => ({
|
||||
const person_items: UserOrMentionPillData[] = user_or_mention_list.map((person) => ({
|
||||
...person,
|
||||
type: "user_or_mention",
|
||||
}));
|
||||
|
@ -535,7 +650,7 @@ export function get_person_suggestions(query, opts) {
|
|||
return person_items.filter((item) => typeahead_helper.query_matches_person(query, item));
|
||||
}
|
||||
|
||||
let groups;
|
||||
let groups: UserGroup[];
|
||||
if (opts.filter_groups_for_mention) {
|
||||
groups = user_groups.get_user_groups_allowed_to_mention();
|
||||
} else if (opts.filter_groups_for_guests && !settings_data.user_can_access_all_other_users()) {
|
||||
|
@ -553,7 +668,12 @@ export function get_person_suggestions(query, opts) {
|
|||
groups = user_groups.get_realm_user_groups();
|
||||
}
|
||||
|
||||
const filtered_groups = groups.filter((item) =>
|
||||
const group_pill_data: UserGroupPillData[] = groups.map((group) => ({
|
||||
...group,
|
||||
type: "user_group",
|
||||
}));
|
||||
|
||||
const filtered_groups = group_pill_data.filter((item) =>
|
||||
typeahead_helper.query_matches_name(query, item),
|
||||
);
|
||||
|
||||
|
@ -583,7 +703,7 @@ export function get_person_suggestions(query, opts) {
|
|||
|
||||
const filtered_message_persons = filter_persons(people.get_active_message_people());
|
||||
|
||||
let filtered_persons;
|
||||
let filtered_persons: UserOrMentionPillData[];
|
||||
|
||||
if (filtered_message_persons.length >= cutoff_length) {
|
||||
filtered_persons = filtered_message_persons;
|
||||
|
@ -601,21 +721,30 @@ export function get_person_suggestions(query, opts) {
|
|||
});
|
||||
}
|
||||
|
||||
export function get_stream_topic_data(input_element) {
|
||||
const opts = {};
|
||||
function get_stream_topic_data(input_element: TypeaheadInputElement): {
|
||||
stream_id: number | undefined;
|
||||
topic: string;
|
||||
} {
|
||||
let stream_id;
|
||||
let topic;
|
||||
const $message_row = input_element.$element.closest(".message_row");
|
||||
if ($message_row.length === 1) {
|
||||
// we are editing a message so we try to use its keys.
|
||||
const msg = message_store.get(rows.id($message_row));
|
||||
assert(msg !== undefined);
|
||||
if (msg.type === "stream") {
|
||||
opts.stream_id = msg.stream_id;
|
||||
opts.topic = msg.topic;
|
||||
stream_id = msg.stream_id;
|
||||
topic = msg.topic;
|
||||
}
|
||||
} else {
|
||||
opts.stream_id = compose_state.stream_id();
|
||||
opts.topic = compose_state.topic();
|
||||
stream_id = compose_state.stream_id();
|
||||
topic = compose_state.topic();
|
||||
}
|
||||
return opts;
|
||||
assert(topic !== undefined);
|
||||
return {
|
||||
stream_id,
|
||||
topic,
|
||||
};
|
||||
}
|
||||
|
||||
const ALLOWED_MARKDOWN_FEATURES = {
|
||||
|
@ -629,9 +758,12 @@ const ALLOWED_MARKDOWN_FEATURES = {
|
|||
timestamp: true,
|
||||
};
|
||||
|
||||
export function get_candidates(query, input_element) {
|
||||
export function get_candidates(
|
||||
query: string,
|
||||
input_element: TypeaheadInputElement,
|
||||
): TypeaheadSuggestion[] {
|
||||
const split = split_at_cursor(query, input_element.$element);
|
||||
let current_token = tokenize_compose_str(split[0]);
|
||||
let current_token: string | boolean = tokenize_compose_str(split[0]);
|
||||
if (current_token === "") {
|
||||
return [];
|
||||
}
|
||||
|
@ -678,7 +810,11 @@ export function get_candidates(query, input_element) {
|
|||
compose_ui.set_code_formatting_button_triggered(false);
|
||||
const matcher = get_language_matcher(token);
|
||||
const matches = language_list.filter((item) => matcher(item));
|
||||
return typeahead_helper.sort_languages(matches, token);
|
||||
const matches_list: LanguageSuggestion[] = matches.map((language) => ({
|
||||
language,
|
||||
type: "syntax",
|
||||
}));
|
||||
return typeahead_helper.sort_languages(matches_list, token);
|
||||
}
|
||||
|
||||
// Only start the emoji autocompleter if : is directly after one
|
||||
|
@ -697,8 +833,12 @@ export function get_candidates(query, input_element) {
|
|||
}
|
||||
completing = "emoji";
|
||||
token = current_token.slice(1);
|
||||
const candidate_list: EmojiSuggestion[] = emoji_collection.map((emoji_dict) => ({
|
||||
...emoji_dict,
|
||||
type: "emoji",
|
||||
}));
|
||||
const matcher = typeahead.get_emoji_matcher(token);
|
||||
const matches = emoji_collection.filter((item) => matcher(item));
|
||||
const matches = candidate_list.filter((item) => matcher(item));
|
||||
return typeahead.sort_emojis(matches, token);
|
||||
}
|
||||
|
||||
|
@ -712,17 +852,18 @@ export function get_candidates(query, input_element) {
|
|||
is_silent = true;
|
||||
current_token = current_token.slice(1);
|
||||
}
|
||||
current_token = filter_mention_name(current_token);
|
||||
if (current_token === undefined) {
|
||||
const filtered_current_token = filter_mention_name(current_token);
|
||||
if (filtered_current_token === undefined) {
|
||||
completing = null;
|
||||
return [];
|
||||
}
|
||||
token = current_token;
|
||||
token = filtered_current_token;
|
||||
|
||||
const opts = get_stream_topic_data(input_element);
|
||||
return filter_and_sort_mentions(is_silent, token, opts);
|
||||
}
|
||||
|
||||
function get_slash_commands_data() {
|
||||
function get_slash_commands_data(): SlashCommand[] {
|
||||
const commands = page_params.development_environment ? all_slash_commands : slash_commands;
|
||||
return commands;
|
||||
}
|
||||
|
@ -732,10 +873,13 @@ export function get_candidates(query, input_element) {
|
|||
|
||||
completing = "slash";
|
||||
token = current_token;
|
||||
const slash_commands = get_slash_commands_data();
|
||||
const matcher = get_slash_matcher(token);
|
||||
const matches = slash_commands.filter((item) => matcher(item));
|
||||
return typeahead_helper.sort_slash_commands(matches, token);
|
||||
const matches = get_slash_commands_data().filter((item) => matcher(item));
|
||||
const matches_list: SlashCommandSuggestion[] = matches.map((slash_command) => ({
|
||||
...slash_command,
|
||||
type: "slash",
|
||||
}));
|
||||
return typeahead_helper.sort_slash_commands(matches_list, token);
|
||||
}
|
||||
|
||||
if (ALLOWED_MARKDOWN_FEATURES.stream && current_token.startsWith("#")) {
|
||||
|
@ -755,9 +899,12 @@ export function get_candidates(query, input_element) {
|
|||
|
||||
completing = "stream";
|
||||
token = current_token;
|
||||
const subs = stream_data.get_unsorted_subs();
|
||||
const candidate_list: StreamPillData[] = stream_data.get_unsorted_subs().map((sub) => ({
|
||||
...sub,
|
||||
type: "stream",
|
||||
}));
|
||||
const matcher = get_stream_or_user_group_matcher(token);
|
||||
const matches = subs.filter((item) => matcher(item));
|
||||
const matches = candidate_list.filter((item) => matcher(item));
|
||||
return typeahead_helper.sort_streams(matches, token);
|
||||
}
|
||||
|
||||
|
@ -770,7 +917,12 @@ export function get_candidates(query, input_element) {
|
|||
completing = "topic_jump";
|
||||
token = ">";
|
||||
// We return something so that the typeahead is shown, but ultimately
|
||||
return [""];
|
||||
return [
|
||||
{
|
||||
message: "",
|
||||
type: "topic_jump",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Matches '#**stream name>some text' at the end of a split.
|
||||
|
@ -779,6 +931,7 @@ export function get_candidates(query, input_element) {
|
|||
if (should_begin_typeahead) {
|
||||
completing = "topic_list";
|
||||
const tokens = stream_topic_regex.exec(split[0]);
|
||||
assert(tokens !== null);
|
||||
if (tokens[1]) {
|
||||
const stream_name = tokens[1];
|
||||
token = tokens[2] || "";
|
||||
|
@ -795,7 +948,11 @@ export function get_candidates(query, input_element) {
|
|||
}
|
||||
const matcher = get_topic_matcher(token);
|
||||
const matches = topic_list.filter((item) => matcher(item));
|
||||
return typeahead_helper.sorter(token, matches, (x) => x);
|
||||
const matches_list: TopicSuggestion[] = matches.map((topic) => ({
|
||||
topic,
|
||||
type: "topic_list",
|
||||
}));
|
||||
return typeahead_helper.sorter(token, matches_list, (x) => x.topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -803,19 +960,23 @@ export function get_candidates(query, input_element) {
|
|||
const time_jump_regex = /<time(:([^>]*?)>?)?$/;
|
||||
if (time_jump_regex.test(split[0])) {
|
||||
completing = "time_jump";
|
||||
return [$t({defaultMessage: "Mention a time-zone-aware time"})];
|
||||
return [
|
||||
{
|
||||
message: $t({defaultMessage: "Mention a time-zone-aware time"}),
|
||||
type: "time_jump",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function content_highlighter_html(item) {
|
||||
switch (completing) {
|
||||
export function content_highlighter_html(item: TypeaheadSuggestion): string | undefined {
|
||||
switch (item.type) {
|
||||
case "emoji":
|
||||
return typeahead_helper.render_emoji(item);
|
||||
case "mention":
|
||||
return typeahead_helper.render_person_or_user_group(item);
|
||||
case "silent_mention":
|
||||
case "user_group":
|
||||
case "user_or_mention":
|
||||
return typeahead_helper.render_person_or_user_group(item);
|
||||
case "slash":
|
||||
return typeahead_helper.render_typeahead_item({
|
||||
|
@ -824,30 +985,49 @@ export function content_highlighter_html(item) {
|
|||
case "stream":
|
||||
return typeahead_helper.render_stream(item);
|
||||
case "syntax":
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
return typeahead_helper.render_typeahead_item({primary: item.language});
|
||||
case "topic_jump":
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
return typeahead_helper.render_typeahead_item({primary: item.message});
|
||||
case "topic_list":
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
return typeahead_helper.render_typeahead_item({primary: item.topic});
|
||||
case "time_jump":
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
return typeahead_helper.render_typeahead_item({primary: item.message});
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function content_typeahead_selected(item, query, input_element, event) {
|
||||
export function content_typeahead_selected(
|
||||
item: TypeaheadSuggestion,
|
||||
query: string,
|
||||
input_element: TypeaheadInputElement,
|
||||
event?: JQuery.ClickEvent | JQuery.KeyUpEvent | JQuery.KeyDownEvent,
|
||||
): string {
|
||||
const pieces = split_at_cursor(query, input_element.$element);
|
||||
let beginning = pieces[0];
|
||||
let rest = pieces[1];
|
||||
const $textbox = input_element.$element;
|
||||
assert(input_element.type === "textarea");
|
||||
const $textbox: JQuery<HTMLTextAreaElement> = input_element.$element;
|
||||
// Accepting some typeahead selections, like polls, will generate
|
||||
// placeholder text that is selected, in order to clarify for the
|
||||
// user what a given parameter is for. This object stores the
|
||||
// highlight offsets for that purpose.
|
||||
const highlight = {};
|
||||
|
||||
switch (completing) {
|
||||
const highlight: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
} = {};
|
||||
// `topic_jump` means that the user just typed a stream name
|
||||
// and then `>` to start typing a topic. But the `item` coming
|
||||
// from the typeahead is still the stream item, so here we do
|
||||
// a reassignment so that the switch statement does the right
|
||||
// thing.
|
||||
if (completing === "topic_jump") {
|
||||
item = {
|
||||
type: "topic_jump",
|
||||
message: "",
|
||||
};
|
||||
}
|
||||
switch (item.type) {
|
||||
case "emoji":
|
||||
// leading and trailing spaces are required for emoji,
|
||||
// except if it begins a message or a new line.
|
||||
|
@ -861,9 +1041,9 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
beginning = beginning.slice(0, -token.length - 1) + " :" + item.emoji_name + ": ";
|
||||
}
|
||||
break;
|
||||
case "silent_mention":
|
||||
case "mention": {
|
||||
const is_silent = completing === "silent_mention";
|
||||
case "user_group":
|
||||
case "user_or_mention": {
|
||||
const is_silent = item.is_silent;
|
||||
beginning = beginning.slice(0, -token.length - 1);
|
||||
if (beginning.endsWith("@_*")) {
|
||||
beginning = beginning.slice(0, -3);
|
||||
|
@ -872,7 +1052,7 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
} else if (beginning.endsWith("@")) {
|
||||
beginning = beginning.slice(0, -1);
|
||||
}
|
||||
if (user_groups.is_user_group(item)) {
|
||||
if (item.type === "user_group") {
|
||||
let user_group_mention_text = is_silent ? "@_*" : "@*";
|
||||
user_group_mention_text += item.name + "* ";
|
||||
beginning += user_group_mention_text;
|
||||
|
@ -882,11 +1062,8 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
// that functionality yet, and we haven't gotten much
|
||||
// feedback on this being an actual pitfall.
|
||||
} else {
|
||||
let mention_text = people.get_mention_syntax(
|
||||
item.full_name,
|
||||
item.user_id,
|
||||
is_silent,
|
||||
);
|
||||
const user_id = item.is_broadcast ? undefined : item.user_id;
|
||||
let mention_text = people.get_mention_syntax(item.full_name, user_id, is_silent);
|
||||
if (!is_silent && !item.is_broadcast) {
|
||||
compose_validate.warn_if_mentioning_unsubscribed_user(item, $textbox);
|
||||
mention_text = compose_validate.convert_mentions_to_silent_in_direct_messages(
|
||||
|
@ -927,8 +1104,8 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
// Isolate the end index of the triple backticks/tildes, including
|
||||
// possibly a space afterward
|
||||
const backticks = beginning.length - token.length;
|
||||
beginning = beginning.slice(0, backticks) + item;
|
||||
if (item === "spoiler") {
|
||||
beginning = beginning.slice(0, backticks) + item.language;
|
||||
if (item.language === "spoiler") {
|
||||
// to add in and highlight placeholder "Header"
|
||||
const placeholder = $t({defaultMessage: "Header"});
|
||||
highlight.start = beginning.length + 1;
|
||||
|
@ -960,7 +1137,7 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
// with the topic and the final **. Note that token.length can be 0
|
||||
// if we are completing from `**streamname>`.
|
||||
const start = beginning.length - token.length;
|
||||
beginning = beginning.slice(0, start) + item + "** ";
|
||||
beginning = beginning.slice(0, start) + item.topic + "** ";
|
||||
break;
|
||||
}
|
||||
case "time_jump": {
|
||||
|
@ -970,7 +1147,7 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
}
|
||||
const timestamp = timerender.get_timestamp_for_flatpickr(timestring);
|
||||
|
||||
const on_timestamp_selection = (val) => {
|
||||
const on_timestamp_selection = (val: string): void => {
|
||||
const datestr = val;
|
||||
beginning =
|
||||
beginning.slice(0, Math.max(0, beginning.lastIndexOf("<time"))) +
|
||||
|
@ -1003,7 +1180,7 @@ export function content_typeahead_selected(item, query, input_element, event) {
|
|||
return beginning + rest;
|
||||
}
|
||||
|
||||
export function compose_automated_selection() {
|
||||
export function compose_automated_selection(): boolean {
|
||||
if (completing === "topic_jump") {
|
||||
// automatically jump inside stream mention on typing > just after
|
||||
// a stream mention, to begin stream+topic mention typeahead (topic_list).
|
||||
|
@ -1012,7 +1189,7 @@ export function compose_automated_selection() {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function compose_trigger_selection(event) {
|
||||
function compose_trigger_selection(event: JQuery.KeyDownEvent): boolean {
|
||||
if (completing === "stream" && event.key === ">") {
|
||||
// complete stream typeahead partially to immediately start the topic_list typeahead.
|
||||
return true;
|
||||
|
@ -1020,24 +1197,28 @@ export function compose_trigger_selection(event) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function initialize_topic_edit_typeahead(form_field, stream_name, dropup) {
|
||||
const bootstrap_typeahead_input = {
|
||||
export function initialize_topic_edit_typeahead(
|
||||
form_field: JQuery<HTMLInputElement>,
|
||||
stream_name: string,
|
||||
dropup: boolean,
|
||||
): Typeahead<string> {
|
||||
const bootstrap_typeahead_input: TypeaheadInputElement = {
|
||||
$element: form_field,
|
||||
type: "input",
|
||||
};
|
||||
return new Typeahead(bootstrap_typeahead_input, {
|
||||
dropup,
|
||||
highlighter_html(item) {
|
||||
highlighter_html(item: string): string {
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
},
|
||||
sorter(items, query) {
|
||||
sorter(items: string[], query: string): string[] {
|
||||
const sorted = typeahead_helper.sorter(query, items, (x) => x);
|
||||
if (sorted.length > 0 && !sorted.includes(query)) {
|
||||
sorted.unshift(query);
|
||||
}
|
||||
return sorted;
|
||||
},
|
||||
source() {
|
||||
source(): string[] {
|
||||
const stream_id = stream_data.get_stream_id(stream_name);
|
||||
return topics_seen_for(stream_id);
|
||||
},
|
||||
|
@ -1045,7 +1226,7 @@ export function initialize_topic_edit_typeahead(form_field, stream_name, dropup)
|
|||
});
|
||||
}
|
||||
|
||||
function get_header_html() {
|
||||
function get_header_html(): string | false {
|
||||
let tip_text = "";
|
||||
switch (completing) {
|
||||
case "stream":
|
||||
|
@ -1069,8 +1250,8 @@ function get_header_html() {
|
|||
return `<em>${_.escape(tip_text)}</em>`;
|
||||
}
|
||||
|
||||
export function initialize_compose_typeahead(selector) {
|
||||
const bootstrap_typeahead_input = {
|
||||
export function initialize_compose_typeahead(selector: string): void {
|
||||
const bootstrap_typeahead_input: TypeaheadInputElement = {
|
||||
$element: $(selector),
|
||||
type: "textarea",
|
||||
};
|
||||
|
@ -1100,31 +1281,37 @@ export function initialize_compose_typeahead(selector) {
|
|||
);
|
||||
}
|
||||
|
||||
export function initialize({on_enter_send}) {
|
||||
export function initialize({
|
||||
on_enter_send,
|
||||
}: {
|
||||
on_enter_send: (scheduling_message?: boolean) => boolean | undefined;
|
||||
}): void {
|
||||
// These handlers are at the "form" level so that they are called after typeahead
|
||||
$("form#send_message_form").on("keydown", (e) => handle_keydown(e, {on_enter_send}));
|
||||
$("form#send_message_form").on("keydown", (e) => {
|
||||
handle_keydown(e, on_enter_send);
|
||||
});
|
||||
$("form#send_message_form").on("keyup", handle_keyup);
|
||||
|
||||
const stream_message_typeahead_input = {
|
||||
const stream_message_typeahead_input: TypeaheadInputElement = {
|
||||
$element: $("input#stream_message_recipient_topic"),
|
||||
type: "input",
|
||||
};
|
||||
new Typeahead(stream_message_typeahead_input, {
|
||||
source() {
|
||||
source(): string[] {
|
||||
return topics_seen_for(compose_state.stream_id());
|
||||
},
|
||||
items: 3,
|
||||
highlighter_html(item) {
|
||||
highlighter_html(item: string): string {
|
||||
return typeahead_helper.render_typeahead_item({primary: item});
|
||||
},
|
||||
sorter(items, query) {
|
||||
sorter(items: string[], query: string): string[] {
|
||||
const sorted = typeahead_helper.sorter(query, items, (x) => x);
|
||||
if (sorted.length > 0 && !sorted.includes(query)) {
|
||||
sorted.unshift(query);
|
||||
}
|
||||
return sorted;
|
||||
},
|
||||
option_label(matching_items, item) {
|
||||
option_label(matching_items: string[], item: string): string | false {
|
||||
if (!matching_items.includes(item)) {
|
||||
return `<em>${$t({defaultMessage: "New"})}</em>`;
|
||||
}
|
||||
|
@ -1133,7 +1320,7 @@ export function initialize({on_enter_send}) {
|
|||
header_html: render_topic_typeahead_hint,
|
||||
});
|
||||
|
||||
const private_message_typeahead_input = {
|
||||
const private_message_typeahead_input: TypeaheadInputElement = {
|
||||
$element: $("#private_message_recipient"),
|
||||
type: "contenteditable",
|
||||
};
|
||||
|
@ -1141,17 +1328,17 @@ export function initialize({on_enter_send}) {
|
|||
source: get_pm_people,
|
||||
items: max_num_items,
|
||||
dropup: true,
|
||||
highlighter_html(item) {
|
||||
highlighter_html(item: UserGroupPillData | UserPillData) {
|
||||
return typeahead_helper.render_person_or_user_group(item);
|
||||
},
|
||||
matcher() {
|
||||
matcher(): boolean {
|
||||
return true;
|
||||
},
|
||||
sorter(items) {
|
||||
sorter(items: (UserGroupPillData | UserPillData)[]): (UserGroupPillData | UserPillData)[] {
|
||||
return items;
|
||||
},
|
||||
updater(item) {
|
||||
if (user_groups.is_user_group(item)) {
|
||||
updater(item: UserGroupPillData | UserPillData): undefined {
|
||||
if (item.type === "user_group") {
|
||||
for (const user_id of item.members) {
|
||||
const user = people.get_by_user_id(user_id);
|
||||
// filter out inactive users, inserted users and current user
|
|
@ -46,7 +46,7 @@ type RealmEmoji = {
|
|||
};
|
||||
|
||||
// Data structure which every widget(like Emoji Picker) in the web app is supposed to use for displaying emojis.
|
||||
type EmojiDict = {
|
||||
export type EmojiDict = {
|
||||
name: string;
|
||||
display_name: string;
|
||||
aliases: string[];
|
||||
|
|
|
@ -11,17 +11,16 @@ import * as typeahead_helper from "./typeahead_helper";
|
|||
import type {CombinedPillContainer} from "./typeahead_helper";
|
||||
import * as user_group_pill from "./user_group_pill";
|
||||
import type {UserGroupPillData} from "./user_group_pill";
|
||||
import type {UserGroup} from "./user_groups";
|
||||
import * as user_pill from "./user_pill";
|
||||
import type {UserPillData} from "./user_pill";
|
||||
|
||||
function person_matcher(query: string, item: User): boolean {
|
||||
function person_matcher(query: string, item: UserPillData): boolean {
|
||||
return (
|
||||
people.is_known_user_id(item.user_id) && typeahead_helper.query_matches_person(query, item)
|
||||
);
|
||||
}
|
||||
|
||||
function group_matcher(query: string, item: UserGroup): boolean {
|
||||
function group_matcher(query: string, item: UserGroupPillData): boolean {
|
||||
return typeahead_helper.query_matches_name(query, item);
|
||||
}
|
||||
|
||||
|
@ -99,10 +98,7 @@ export function set_up(
|
|||
// handles `include_users` cases along with
|
||||
// default cases.
|
||||
assert(item.type === "user");
|
||||
return typeahead_helper.render_person({
|
||||
...item,
|
||||
is_broadcast: undefined,
|
||||
});
|
||||
return typeahead_helper.render_person(item);
|
||||
},
|
||||
matcher(item: TypeaheadItem, query: string): boolean {
|
||||
query = query.toLowerCase();
|
||||
|
|
|
@ -3,10 +3,12 @@ import _ from "lodash";
|
|||
import assert from "minimalistic-assert";
|
||||
|
||||
import * as typeahead from "../shared/src/typeahead";
|
||||
import type {EmojiSuggestion} from "../shared/src/typeahead";
|
||||
import render_typeahead_list_item from "../templates/typeahead_list_item.hbs";
|
||||
|
||||
import * as buddy_data from "./buddy_data";
|
||||
import * as compose_state from "./compose_state";
|
||||
import type {LanguageSuggestion, SlashCommandSuggestion} from "./composebox_typeahead";
|
||||
import type {InputPillContainer, InputPillItem} from "./input_pill";
|
||||
import * as people from "./people";
|
||||
import type {PseudoMentionUser, User} from "./people";
|
||||
|
@ -19,15 +21,16 @@ import * as stream_list_sort from "./stream_list_sort";
|
|||
import type {StreamPill, StreamPillData} from "./stream_pill";
|
||||
import type {StreamSubscription} from "./sub_store";
|
||||
import type {UserGroupPill, UserGroupPillData} from "./user_group_pill";
|
||||
import * as user_groups from "./user_groups";
|
||||
import type {UserGroup} from "./user_groups";
|
||||
import type {UserPill, UserPillData} from "./user_pill";
|
||||
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});
|
||||
export type UserOrMentionPillData = UserOrMention & {type: "user_or_mention"};
|
||||
export type UserOrMentionPillData = UserOrMention & {
|
||||
type: "user_or_mention";
|
||||
is_silent?: boolean;
|
||||
};
|
||||
|
||||
export type CombinedPillContainer = InputPillContainer<StreamPill | UserGroupPill | UserPill>;
|
||||
|
||||
|
@ -98,7 +101,7 @@ export function render_typeahead_item(args: {
|
|||
is_user_group?: boolean;
|
||||
stream?: StreamData;
|
||||
is_unsubscribed?: boolean;
|
||||
emoji_code?: string;
|
||||
emoji_code?: string | undefined;
|
||||
}): string {
|
||||
const has_image = args.img_src !== undefined;
|
||||
const has_status = args.status_emoji_info !== undefined;
|
||||
|
@ -113,8 +116,8 @@ export function render_typeahead_item(args: {
|
|||
});
|
||||
}
|
||||
|
||||
export function render_person(person: UserOrMention): string {
|
||||
if (person.is_broadcast) {
|
||||
export function render_person(person: UserPillData | UserOrMentionPillData): string {
|
||||
if (person.type === "user_or_mention" && person.is_broadcast) {
|
||||
return render_typeahead_item({
|
||||
primary: person.special_item_text,
|
||||
is_person: true,
|
||||
|
@ -154,9 +157,9 @@ export function render_user_group(user_group: {name: string; description: string
|
|||
}
|
||||
|
||||
export function render_person_or_user_group(
|
||||
item: UserGroup | (UserOrMention & {members: undefined}),
|
||||
item: UserGroupPillData | UserPillData | UserOrMentionPillData,
|
||||
): string {
|
||||
if (user_groups.is_user_group(item)) {
|
||||
if (item.type === "user_group") {
|
||||
return render_user_group(item);
|
||||
}
|
||||
|
||||
|
@ -178,11 +181,7 @@ export function render_stream(stream: StreamData): string {
|
|||
});
|
||||
}
|
||||
|
||||
export function render_emoji(item: {
|
||||
emoji_name: string;
|
||||
emoji_url: string;
|
||||
emoji_code: string;
|
||||
}): string {
|
||||
export function render_emoji(item: EmojiSuggestion): string {
|
||||
const args = {
|
||||
is_emoji: true,
|
||||
primary: item.emoji_name.replaceAll("_", " "),
|
||||
|
@ -419,10 +418,14 @@ function retain_unique_language_aliases(matches: string[]): string[] {
|
|||
return unique_aliases;
|
||||
}
|
||||
|
||||
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]);
|
||||
export function sort_languages(matches: LanguageSuggestion[], query: string): LanguageSuggestion[] {
|
||||
const languages = matches.map((object) => object.language);
|
||||
const results = typeahead.triage(query, languages, (x) => x, compare_language);
|
||||
const unique_languages = retain_unique_language_aliases([...results.matches, ...results.rest]);
|
||||
return unique_languages.map((language) => ({
|
||||
language,
|
||||
type: "syntax",
|
||||
}));
|
||||
}
|
||||
|
||||
export function sort_recipients<UserType extends UserOrMentionPillData | UserPillData>({
|
||||
|
@ -583,7 +586,10 @@ function slash_command_comparator(
|
|||
return 0;
|
||||
}
|
||||
|
||||
export function sort_slash_commands(matches: SlashCommand[], query: string): SlashCommand[] {
|
||||
export function sort_slash_commands(
|
||||
matches: SlashCommandSuggestion[],
|
||||
query: string,
|
||||
): SlashCommandSuggestion[] {
|
||||
// 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);
|
||||
|
@ -647,14 +653,29 @@ export function sort_streams(matches: StreamPillData[], query: string): StreamPi
|
|||
return [...name_results.matches, ...desc_results.matches, ...desc_results.rest];
|
||||
}
|
||||
|
||||
export function query_matches_person(query: string, person: User): boolean {
|
||||
return (
|
||||
typeahead.query_matches_string_in_order(query, person.full_name, " ") ||
|
||||
(Boolean(person.delivery_email) &&
|
||||
typeahead.query_matches_string_in_order(query, people.get_visible_email(person), " "))
|
||||
);
|
||||
export function query_matches_person(
|
||||
query: string,
|
||||
person: UserPillData | UserOrMentionPillData,
|
||||
): boolean {
|
||||
if (typeahead.query_matches_string_in_order(query, person.full_name, " ")) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(person.type === "user" || person.is_broadcast === undefined) &&
|
||||
Boolean(person.delivery_email)
|
||||
) {
|
||||
return typeahead.query_matches_string_in_order(
|
||||
query,
|
||||
people.get_visible_email(person),
|
||||
" ",
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function query_matches_name(query: string, user_group_or_stream: UserGroup): boolean {
|
||||
export function query_matches_name(
|
||||
query: string,
|
||||
user_group_or_stream: UserGroupPillData | StreamPillData,
|
||||
): boolean {
|
||||
return typeahead.query_matches_string_in_order(query, user_group_or_stream.name, " ");
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@ export type UserGroupPill = {
|
|||
|
||||
type UserGroupPillWidget = InputPillContainer<UserGroupPill>;
|
||||
|
||||
export type UserGroupPillData = UserGroup & {type: "user_group"};
|
||||
export type UserGroupPillData = UserGroup & {
|
||||
type: "user_group";
|
||||
is_silent?: boolean;
|
||||
};
|
||||
|
||||
function display_pill(group: UserGroup): string {
|
||||
return `${group.name}: ${group.members.size} users`;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -304,6 +304,13 @@ test("sort_streams", ({override, override_rewire}) => {
|
|||
assert.deepEqual(test_streams[5].name, "Mew"); // Unsubscribed and no match
|
||||
});
|
||||
|
||||
function language_items(languages) {
|
||||
return languages.map((language) => ({
|
||||
type: "syntax",
|
||||
language,
|
||||
}));
|
||||
}
|
||||
|
||||
test("sort_languages", ({override_rewire}) => {
|
||||
override_rewire(pygments_data, "langs", {
|
||||
python: {priority: 26},
|
||||
|
@ -314,18 +321,18 @@ test("sort_languages", ({override_rewire}) => {
|
|||
css: {priority: 21},
|
||||
});
|
||||
|
||||
let test_langs = ["pascal", "perl", "php", "python", "javascript"];
|
||||
let test_langs = language_items(["pascal", "perl", "php", "python", "javascript"]);
|
||||
test_langs = th.sort_languages(test_langs, "p");
|
||||
|
||||
// Sort languages by matching first letter, and then by popularity
|
||||
assert.deepEqual(test_langs, ["python", "php", "pascal", "perl", "javascript"]);
|
||||
assert.deepEqual(test_langs, language_items(["python", "php", "pascal", "perl", "javascript"]));
|
||||
|
||||
// Test if popularity between two languages are the same
|
||||
pygments_data.langs.php = {priority: 26};
|
||||
test_langs = ["pascal", "perl", "php", "python", "javascript"];
|
||||
test_langs = language_items(["pascal", "perl", "php", "python", "javascript"]);
|
||||
test_langs = th.sort_languages(test_langs, "p");
|
||||
|
||||
assert.deepEqual(test_langs, ["php", "python", "pascal", "perl", "javascript"]);
|
||||
assert.deepEqual(test_langs, language_items(["php", "python", "pascal", "perl", "javascript"]));
|
||||
});
|
||||
|
||||
test("sort_languages on actual data", () => {
|
||||
|
@ -334,23 +341,23 @@ test("sort_languages on actual data", () => {
|
|||
// We may eventually want to use human-readable names like
|
||||
// "JavaScript" with several machine-readable aliases for what the
|
||||
// user typed, which might help provide a better user experience.
|
||||
let test_langs = ["j", "java", "javascript", "js"];
|
||||
let test_langs = language_items(["j", "java", "javascript", "js"]);
|
||||
|
||||
// Sort according to priority only.
|
||||
test_langs = th.sort_languages(test_langs, "jav");
|
||||
assert.deepEqual(test_langs, ["javascript", "java", "j"]);
|
||||
assert.deepEqual(test_langs, language_items(["javascript", "java", "j"]));
|
||||
|
||||
// Push exact matches to top, regardless of priority
|
||||
test_langs = th.sort_languages(test_langs, "java");
|
||||
assert.deepEqual(test_langs, ["java", "javascript", "j"]);
|
||||
assert.deepEqual(test_langs, language_items(["java", "javascript", "j"]));
|
||||
test_langs = th.sort_languages(test_langs, "j");
|
||||
assert.deepEqual(test_langs, ["j", "javascript", "java"]);
|
||||
assert.deepEqual(test_langs, language_items(["j", "javascript", "java"]));
|
||||
|
||||
// (Only one alias should be shown per language
|
||||
// (e.g. searching for "js" shouldn't show "javascript")
|
||||
test_langs = ["js", "javascript", "java"];
|
||||
test_langs = language_items(["js", "javascript", "java"]);
|
||||
test_langs = th.sort_languages(test_langs, "js");
|
||||
assert.deepEqual(test_langs, ["js", "java"]);
|
||||
assert.deepEqual(test_langs, language_items(["js", "java"]));
|
||||
});
|
||||
|
||||
function get_typeahead_result(query, current_stream_id, current_topic) {
|
||||
|
@ -817,6 +824,7 @@ test("render_person special_item_text", ({mock_template}) => {
|
|||
user_id: 7,
|
||||
special_item_text: "special_text",
|
||||
is_broadcast: true,
|
||||
type: "user_or_mention",
|
||||
};
|
||||
|
||||
rendered = false;
|
||||
|
|
|
@ -33,7 +33,7 @@ EMOTICON_CONVERSIONS = emoji_codes["emoticon_conversions"]
|
|||
|
||||
possible_emoticons = EMOTICON_CONVERSIONS.keys()
|
||||
possible_emoticon_regexes = (re.escape(emoticon) for emoticon in possible_emoticons)
|
||||
terminal_symbols = r",.;?!()\[\] \"'\n\t" # from composebox_typeahead.js
|
||||
terminal_symbols = r",.;?!()\[\] \"'\n\t" # from composebox_typeahead.ts
|
||||
EMOTICON_RE = (
|
||||
rf"(?<![^{terminal_symbols}])(?P<emoticon>("
|
||||
+ r")|(".join(possible_emoticon_regexes)
|
||||
|
|
Loading…
Reference in New Issue