mirror of https://github.com/zulip/zulip.git
search pills: Direct message group pill support.
This supports adding users to DM pills by simply typing a user's name in the text input directly after a complete dm pill. It only works for DM pills at the end of search input. Clicking the X button removes a user from its user pill container, and if that user was the last user left in the container, then the whole container is also removed.
This commit is contained in:
parent
278edb1b3f
commit
11bb44c7cc
|
@ -121,6 +121,7 @@ EXEMPT_FILES = make_set(
|
|||
"web/src/inbox_util.ts",
|
||||
"web/src/info_overlay.ts",
|
||||
"web/src/information_density.ts",
|
||||
"web/src/input_pill.ts",
|
||||
"web/src/integration_url_modal.ts",
|
||||
"web/src/invite.ts",
|
||||
"web/src/invite_stream_picker_pill.ts",
|
||||
|
@ -199,6 +200,7 @@ EXEMPT_FILES = make_set(
|
|||
"web/src/scroll_util.ts",
|
||||
"web/src/search.ts",
|
||||
"web/src/search_pill.ts",
|
||||
"web/src/search_suggestion.ts",
|
||||
"web/src/sent_messages.ts",
|
||||
"web/src/sentry.ts",
|
||||
"web/src/server_events.js",
|
||||
|
|
|
@ -4,10 +4,12 @@ import $ from "jquery";
|
|||
import assert from "minimalistic-assert";
|
||||
|
||||
import render_input_pill from "../templates/input_pill.hbs";
|
||||
import render_search_user_pill from "../templates/search_user_pill.hbs";
|
||||
|
||||
import * as blueslip from "./blueslip";
|
||||
import type {EmojiRenderingDetails} from "./emoji";
|
||||
import * as keydown_util from "./keydown_util";
|
||||
import type {SearchUserPill} from "./search_pill";
|
||||
import * as ui_util from "./ui_util";
|
||||
|
||||
// See https://zulip.readthedocs.io/en/latest/subsystems/input-pills.html
|
||||
|
@ -21,6 +23,8 @@ export type InputPillItem<T> = {
|
|||
should_add_guest_user_indicator?: boolean;
|
||||
user_id?: number;
|
||||
group_id?: number;
|
||||
// Used for search pills
|
||||
operator?: string;
|
||||
} & T;
|
||||
|
||||
export type InputPillConfig = {
|
||||
|
@ -158,36 +162,39 @@ export function create<T>(opts: InputPillCreateOptions<T>): InputPillContainer<T
|
|||
blueslip.error("no type defined for the item");
|
||||
return;
|
||||
}
|
||||
let pill_html;
|
||||
if (item.type === "search_user") {
|
||||
pill_html = render_search_user_pill(item);
|
||||
} else {
|
||||
const has_image = item.img_src !== undefined;
|
||||
|
||||
const has_image = item.img_src !== undefined;
|
||||
const opts: InputPillRenderingDetails = {
|
||||
display_value: item.display_value,
|
||||
has_image,
|
||||
deactivated: item.deactivated,
|
||||
should_add_guest_user_indicator: item.should_add_guest_user_indicator,
|
||||
};
|
||||
|
||||
const opts: InputPillRenderingDetails = {
|
||||
display_value: item.display_value,
|
||||
has_image,
|
||||
deactivated: item.deactivated,
|
||||
should_add_guest_user_indicator: item.should_add_guest_user_indicator,
|
||||
};
|
||||
|
||||
if (item.user_id) {
|
||||
opts.user_id = item.user_id;
|
||||
}
|
||||
if (item.group_id) {
|
||||
opts.group_id = item.group_id;
|
||||
}
|
||||
|
||||
if (has_image) {
|
||||
opts.img_src = item.img_src;
|
||||
}
|
||||
|
||||
if (store.pill_config?.show_user_status_emoji === true) {
|
||||
const has_status = item.status_emoji_info !== undefined;
|
||||
if (has_status) {
|
||||
opts.status_emoji_info = item.status_emoji_info;
|
||||
if (item.user_id) {
|
||||
opts.user_id = item.user_id;
|
||||
}
|
||||
if (item.group_id) {
|
||||
opts.group_id = item.group_id;
|
||||
}
|
||||
opts.has_status = has_status;
|
||||
}
|
||||
|
||||
const pill_html = render_input_pill(opts);
|
||||
if (has_image) {
|
||||
opts.img_src = item.img_src;
|
||||
}
|
||||
|
||||
if (store.pill_config?.show_user_status_emoji === true) {
|
||||
const has_status = item.status_emoji_info !== undefined;
|
||||
if (has_status) {
|
||||
opts.status_emoji_info = item.status_emoji_info;
|
||||
}
|
||||
opts.has_status = has_status;
|
||||
}
|
||||
pill_html = render_input_pill(opts);
|
||||
}
|
||||
const payload: InputPill<T> = {
|
||||
item,
|
||||
$element: $(pill_html),
|
||||
|
@ -249,6 +256,59 @@ export function create<T>(opts: InputPillCreateOptions<T>): InputPillContainer<T
|
|||
return undefined;
|
||||
},
|
||||
|
||||
// TODO: This function is only used for the search input supporting multiple user
|
||||
// pills within an individual top-level pill. Ideally, we'd encapsulate it in a
|
||||
// subclass used only for search so that this code can be part of search_pill.ts.
|
||||
removeUserPill(user_container: HTMLElement, user_id: number) {
|
||||
// First get the outer pill that contains the user pills.
|
||||
let container_idx: number | undefined;
|
||||
for (let x = 0; x < store.pills.length; x += 1) {
|
||||
if (store.pills[x]!.$element[0] === user_container) {
|
||||
container_idx = x;
|
||||
}
|
||||
}
|
||||
assert(container_idx !== undefined);
|
||||
assert(store.pills[container_idx]!.item.type === "search_user");
|
||||
// TODO: Figure out how to get this typed correctly.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const user_pill_container = store.pills[container_idx]!
|
||||
.item as unknown as InputPillItem<SearchUserPill>;
|
||||
|
||||
// If there's only one user in this pill, delete the whole pill.
|
||||
if (user_pill_container.users.length === 1) {
|
||||
assert(user_pill_container.users[0]!.user_id === user_id);
|
||||
this.removePill(user_container);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the user id from the pill data.
|
||||
let user_idx: number | undefined;
|
||||
for (let x = 0; x < user_pill_container.users.length; x += 1) {
|
||||
if (user_pill_container.users[x]!.user_id === user_id) {
|
||||
user_idx = x;
|
||||
}
|
||||
}
|
||||
assert(user_idx !== undefined);
|
||||
user_pill_container.users.splice(user_idx, 1);
|
||||
const sign = user_pill_container.negated ? "-" : "";
|
||||
const search_string =
|
||||
sign +
|
||||
user_pill_container.operator +
|
||||
":" +
|
||||
user_pill_container.users.map((user) => user.email).join(",");
|
||||
user_pill_container.display_value = search_string;
|
||||
|
||||
// Remove the user pill from the DOM.
|
||||
const $user_pill = $(store.pills[container_idx]!.$element.children(".pill")[user_idx]!);
|
||||
assert($user_pill.data("user-id") === user_id);
|
||||
$user_pill.remove();
|
||||
|
||||
// This is needed to run the "change" event handler registered in
|
||||
// compose_recipient.js, which calls the `update_on_recipient_change` to update
|
||||
// the compose_fade state.
|
||||
store.$input.trigger("change");
|
||||
},
|
||||
|
||||
// this will remove the last pill in the container -- by default tied
|
||||
// to the "Backspace" key when the value of the input is empty.
|
||||
// If quiet is a truthy value, the event handler associated with the
|
||||
|
@ -444,6 +504,18 @@ export function create<T>(opts: InputPillCreateOptions<T>): InputPillContainer<T
|
|||
// when the "×" is clicked on a pill, it should delete that pill and then
|
||||
// select the next pill (or input).
|
||||
store.$parent.on("click", ".exit", function (this: HTMLElement, e) {
|
||||
const $user_pill_container = $(this).parents(".user-pill-container");
|
||||
if ($user_pill_container.length) {
|
||||
// The user-pill-container container class is used exclusively for
|
||||
// group-DM search pills, where multiple user pills sit inside a larger
|
||||
// pill. The exit icons in those individual user pills should remove
|
||||
// just that pill, not the outer pill.
|
||||
// TODO: Figure out how to move this code into search_pill.ts.
|
||||
const user_id = $(this).closest(".pill").attr("data-user-id");
|
||||
assert(user_id !== undefined);
|
||||
funcs.removeUserPill($user_pill_container[0]!, Number.parseInt(user_id, 10));
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
const $pill = $(this).closest(".pill");
|
||||
const $next = $pill.next();
|
||||
|
|
|
@ -83,6 +83,10 @@ export function initialize({on_narrow_search}: {on_narrow_search: OnNarrowSearch
|
|||
search_input_has_changed = true;
|
||||
});
|
||||
|
||||
$search_query_box.on("change", () => {
|
||||
search_typeahead.lookup(false);
|
||||
});
|
||||
|
||||
// Data storage for the typeahead.
|
||||
// This maps a search string to an object with a "description_html" field.
|
||||
// (It's a bit of legacy that we have an object with only one important
|
||||
|
|
|
@ -1,13 +1,42 @@
|
|||
import assert from "minimalistic-assert";
|
||||
|
||||
import {Filter} from "./filter";
|
||||
import * as input_pill from "./input_pill";
|
||||
import type {InputPillContainer} from "./input_pill";
|
||||
import * as people from "./people";
|
||||
import type {User} from "./people";
|
||||
import type {NarrowTerm} from "./state_data";
|
||||
import * as user_status from "./user_status";
|
||||
import type {UserStatusEmojiInfo} from "./user_status";
|
||||
|
||||
type SearchPill = {
|
||||
export type SearchUserPill = {
|
||||
type: "search_user";
|
||||
operator: string;
|
||||
// TODO: It would be nice if we just call this `search_string` instead of
|
||||
// `display_value`, because we don't actually display this value for user
|
||||
// pills, but `display_value` is needed to hook into the generic input pill
|
||||
// logic and it would be a decent amount of work to change that.
|
||||
display_value: string;
|
||||
type: string;
|
||||
description_html: string;
|
||||
negated: boolean;
|
||||
users: {
|
||||
display_value: string;
|
||||
user_id: number;
|
||||
email: string;
|
||||
img_src: string;
|
||||
status_emoji_info: UserStatusEmojiInfo | undefined;
|
||||
should_add_guest_user_indicator: boolean;
|
||||
deactivated: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
type SearchPill =
|
||||
| {
|
||||
type: "search";
|
||||
display_value: string;
|
||||
description_html: string;
|
||||
}
|
||||
| SearchUserPill;
|
||||
|
||||
export type SearchPillWidget = InputPillContainer<SearchPill>;
|
||||
|
||||
export function create_item_from_search_string(search_string: string): SearchPill {
|
||||
|
@ -34,6 +63,36 @@ export function create_pills($pill_container: JQuery): SearchPillWidget {
|
|||
return pills;
|
||||
}
|
||||
|
||||
function append_user_pill(
|
||||
users: User[],
|
||||
pill_widget: SearchPillWidget,
|
||||
operator: string,
|
||||
negated: boolean,
|
||||
): void {
|
||||
const sign = negated ? "-" : "";
|
||||
const search_string = sign + operator + ":" + users.map((user) => user.email).join(",");
|
||||
const pill_data: SearchUserPill = {
|
||||
type: "search_user",
|
||||
operator,
|
||||
display_value: search_string,
|
||||
negated,
|
||||
users: users.map((user) => ({
|
||||
display_value: user.full_name,
|
||||
user_id: user.user_id,
|
||||
email: user.email,
|
||||
img_src: people.small_avatar_url_for_person(user),
|
||||
status_emoji_info: user_status.get_status_emoji(user.user_id),
|
||||
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user.user_id),
|
||||
deactivated: !people.is_person_active(user.user_id) && !user.is_inaccessible_user,
|
||||
})),
|
||||
};
|
||||
|
||||
pill_widget.appendValidatedData(pill_data);
|
||||
pill_widget.clear_text();
|
||||
}
|
||||
|
||||
const user_pill_operators = new Set(["dm", "dm-including", "sender"]);
|
||||
|
||||
export function set_search_bar_contents(
|
||||
search_terms: NarrowTerm[],
|
||||
pill_widget: SearchPillWidget,
|
||||
|
@ -42,6 +101,16 @@ export function set_search_bar_contents(
|
|||
pill_widget.clear();
|
||||
let partial_pill = "";
|
||||
for (const term of search_terms) {
|
||||
if (user_pill_operators.has(term.operator) && term.operand !== "") {
|
||||
const user_emails = term.operand.split(",");
|
||||
const users = user_emails.map((email) => {
|
||||
const user = people.get_by_email(email);
|
||||
assert(user !== undefined);
|
||||
return user;
|
||||
});
|
||||
append_user_pill(users, pill_widget, term.operator, term.negated ?? false);
|
||||
continue;
|
||||
}
|
||||
const input = Filter.unparse([term]);
|
||||
// If the last term looks something like `dm:`, we
|
||||
// don't want to make it a pill, since it isn't isn't
|
||||
|
|
|
@ -181,9 +181,40 @@ function get_channel_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggest
|
|||
}
|
||||
|
||||
function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
|
||||
// We only suggest groups once a term with a valid user already exists
|
||||
if (terms.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const last_complete_term = terms.at(-1)!;
|
||||
// For users with "pm-with" in their muscle memory, still
|
||||
// have group direct message suggestions with "dm:" operator.
|
||||
if (!check_validity(last, terms, ["dm", "pm-with"], [{operator: "channel"}])) {
|
||||
if (
|
||||
!check_validity(
|
||||
last_complete_term,
|
||||
terms.slice(-1),
|
||||
["dm", "pm-with"],
|
||||
[{operator: "channel"}],
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If they started typing since a user pill, we'll parse that as "search"
|
||||
// but they might actually want to parse that as a user instead to add to
|
||||
// the most recent pill. So we shuffle some things around to support that.
|
||||
if (last.operator === "search") {
|
||||
const text_input = last.operand;
|
||||
const operand = `${last_complete_term.operand},${text_input}`;
|
||||
last = {
|
||||
...last_complete_term,
|
||||
operand,
|
||||
};
|
||||
terms = terms.slice(-1);
|
||||
} else if (last.operator === "") {
|
||||
last = last_complete_term;
|
||||
} else {
|
||||
// If they already started another term with an other operator, we're
|
||||
// no longer dealing with a group DM situation.
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -197,14 +228,17 @@ function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestio
|
|||
// we only use the last part to generate suggestions.
|
||||
|
||||
const last_comma_index = operand.lastIndexOf(",");
|
||||
let all_but_last_part;
|
||||
let last_part;
|
||||
if (last_comma_index < 0) {
|
||||
return [];
|
||||
all_but_last_part = operand;
|
||||
last_part = "";
|
||||
} else {
|
||||
// Neither all_but_last_part nor last_part include the final comma.
|
||||
all_but_last_part = operand.slice(0, last_comma_index);
|
||||
last_part = operand.slice(last_comma_index + 1);
|
||||
}
|
||||
|
||||
// Neither all_but_last_part nor last_part include the final comma.
|
||||
const all_but_last_part = operand.slice(0, last_comma_index);
|
||||
const last_part = operand.slice(last_comma_index + 1);
|
||||
|
||||
// We don't suggest a person if their email is already present in the
|
||||
// operand (not including the last part).
|
||||
const parts = [...all_but_last_part.split(","), people.my_current_email()];
|
||||
|
@ -827,7 +861,24 @@ class Attacher {
|
|||
|
||||
attach_many(suggestions: Suggestion[]): void {
|
||||
for (const suggestion of suggestions) {
|
||||
const suggestion_line = [...this.base, suggestion];
|
||||
let suggestion_line;
|
||||
if (this.base.length === 0) {
|
||||
suggestion_line = [suggestion];
|
||||
} else {
|
||||
// When we add a user to a user group, we
|
||||
// replace the last pill.
|
||||
const last_base_term = this.base.at(-1)!;
|
||||
const last_base_string = last_base_term.search_string;
|
||||
const new_search_string = suggestion.search_string;
|
||||
if (
|
||||
new_search_string.startsWith("dm:") &&
|
||||
new_search_string.includes(last_base_string)
|
||||
) {
|
||||
suggestion_line = [...this.base.slice(0, -1), suggestion];
|
||||
} else {
|
||||
suggestion_line = [...this.base, suggestion];
|
||||
}
|
||||
}
|
||||
this.push(suggestion_line);
|
||||
}
|
||||
}
|
||||
|
@ -949,15 +1000,19 @@ export function get_search_result(query_from_pills: string, query_from_text: str
|
|||
|
||||
// Remember to update the spectator list when changing this.
|
||||
let filterers = [
|
||||
// This should show before other `get_people` suggestions
|
||||
// because both are valid suggestions for typing a user's
|
||||
// name, and if there's already has a DM pill then the
|
||||
// searching user probably is looking to make a group DM.
|
||||
get_group_suggestions,
|
||||
get_channels_filter_suggestions,
|
||||
get_is_filter_suggestions,
|
||||
get_sent_by_me_suggestions,
|
||||
get_channel_suggestions,
|
||||
get_people("sender"),
|
||||
get_people("dm"),
|
||||
get_people("sender"),
|
||||
get_people("dm-including"),
|
||||
get_people("from"),
|
||||
get_group_suggestions,
|
||||
get_topic_suggestions,
|
||||
get_operator_suggestions,
|
||||
get_has_filter_suggestions,
|
||||
|
|
|
@ -567,6 +567,7 @@
|
|||
--color-background-exit-hover-deactivated-user-pill: hsl(
|
||||
4deg 75% 53% / 15%
|
||||
);
|
||||
--color-background-user-pill: hsla(0deg 0% 100% / 85%);
|
||||
|
||||
/* Inbox view constants - Values from Figma design */
|
||||
--height-inbox-search: 26px;
|
||||
|
@ -949,6 +950,7 @@
|
|||
--color-focus-outline-deactivated-user-pill: hsl(0deg 0% 100% / 70%);
|
||||
--color-close-deactivated-user-pill: hsl(7deg 100% 74%);
|
||||
--color-background-exit-hover-deactivated-user-pill: hsl(0deg 0% 100% / 7%);
|
||||
--color-background-user-pill: hsl(0deg 0% 0% / 40%);
|
||||
|
||||
/* Inbox view */
|
||||
--color-background-inbox: var(--color-background);
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
.search-input-and-pills {
|
||||
grid-area: search-pills;
|
||||
display: flex;
|
||||
padding: 3px 0;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 2px;
|
||||
align-self: center;
|
||||
|
@ -222,6 +222,7 @@
|
|||
.pill {
|
||||
margin: 0;
|
||||
min-width: unset;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
&:not(.focused) {
|
||||
|
@ -233,6 +234,33 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.user-pill-container {
|
||||
padding: 2px;
|
||||
height: 22px;
|
||||
min-width: fit-content;
|
||||
|
||||
> .pill-label {
|
||||
min-width: fit-content;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.pill {
|
||||
height: 22px;
|
||||
margin: 2px;
|
||||
border: none;
|
||||
|
||||
&:not(.deactivated-pill) {
|
||||
background-color: var(--color-background-user-pill);
|
||||
}
|
||||
}
|
||||
|
||||
.pill-image {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= $md_min) {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="user-pill-container pill" tabindex=0>
|
||||
<span class="pill-label">
|
||||
{{~#if this.negated}}-{{~/if~}}
|
||||
{{ operator }}:
|
||||
</span>
|
||||
{{#each users}}
|
||||
<div class="pill{{#if deactivated}} deactivated-pill{{/if}}" data-user-id="{{this.user_id}}">
|
||||
<img class="pill-image" src="{{this.img_src}}" />
|
||||
<span class="pill-label">
|
||||
<span class="pill-value">{{ this.display_value }}</span>
|
||||
{{~#if this.should_add_guest_user_indicator}} <i>({{t 'guest'}})</i>{{~/if~}}
|
||||
{{~#if deactivated}} ({{t 'deactivated'}}){{~/if~}}
|
||||
{{~#if this.status_emoji_info~}}
|
||||
{{~> status_emoji this.status_emoji_info~}}
|
||||
{{~/if~}}
|
||||
</span>
|
||||
<div class="exit">
|
||||
<a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -529,6 +529,9 @@ run_test("exit button on pill", ({mock_template}) => {
|
|||
assert.equal(sel, ".pill");
|
||||
return $curr_pill_stub;
|
||||
},
|
||||
parents() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -68,8 +68,8 @@ function init() {
|
|||
stream_data.clear_subscriptions();
|
||||
}
|
||||
|
||||
function get_suggestions(query) {
|
||||
return search.get_suggestions("", query);
|
||||
function get_suggestions(query, pill_query = "") {
|
||||
return search.get_suggestions(pill_query, query);
|
||||
}
|
||||
|
||||
function test(label, f) {
|
||||
|
@ -151,8 +151,8 @@ test("dm_suggestions", ({override, mock_template}) => {
|
|||
expected = [
|
||||
"is:dm al",
|
||||
"is:dm is:alerted",
|
||||
"is:dm sender:alice@zulip.com",
|
||||
"is:dm dm:alice@zulip.com",
|
||||
"is:dm sender:alice@zulip.com",
|
||||
"is:dm dm-including:alice@zulip.com",
|
||||
"is:dm",
|
||||
];
|
||||
|
@ -234,8 +234,8 @@ test("dm_suggestions", ({override, mock_template}) => {
|
|||
expected = [
|
||||
"is:starred has:link is:dm al",
|
||||
"is:starred has:link is:dm is:alerted",
|
||||
"is:starred has:link is:dm sender:alice@zulip.com",
|
||||
"is:starred has:link is:dm dm:alice@zulip.com",
|
||||
"is:starred has:link is:dm sender:alice@zulip.com",
|
||||
"is:starred has:link is:dm dm-including:alice@zulip.com",
|
||||
"is:starred has:link is:dm",
|
||||
"is:starred has:link",
|
||||
|
@ -272,90 +272,73 @@ test("group_suggestions", ({mock_template}) => {
|
|||
mock_template("search_description.hbs", true, (_data, html) => html);
|
||||
mock_template("user_pill.hbs", true, (_data, html) => html);
|
||||
|
||||
// Entering a comma in a "dm:" query should immediately
|
||||
// generate suggestions for the next person.
|
||||
let query = "dm:bob@zulip.com,";
|
||||
let suggestions = get_suggestions(query);
|
||||
// If there's an existing completed user pill right before
|
||||
// the input string, we suggest a user group as one of the
|
||||
// suggestions.
|
||||
let pill_query = "dm:bob@zulip.com";
|
||||
let query = "alice";
|
||||
let suggestions = get_suggestions(query, pill_query);
|
||||
let expected = [
|
||||
"dm:bob@zulip.com,",
|
||||
"dm:bob@zulip.com alice",
|
||||
"dm:bob@zulip.com,alice@zulip.com",
|
||||
"dm:bob@zulip.com,jeff@zulip.com",
|
||||
"dm:bob@zulip.com,ted@zulip.com",
|
||||
"dm:bob@zulip.com sender:alice@zulip.com",
|
||||
"dm:bob@zulip.com dm-including:alice@zulip.com",
|
||||
"dm:bob@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// Only the last part of a comma-separated "dm" query
|
||||
// should be used to generate suggestions.
|
||||
query = "dm:bob@zulip.com,t";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["dm:bob@zulip.com,t", "dm:bob@zulip.com,ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// Smit should also generate ted@zulip.com (Ted Smith) as a suggestion.
|
||||
query = "dm:bob@zulip.com,Smit";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["dm:bob@zulip.com,Smit", "dm:bob@zulip.com,ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// Do not suggest "myself@zulip.com" (the name of the current user)
|
||||
query = "dm:ted@zulip.com,my";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["dm:ted@zulip.com,my"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// No superfluous suggestions should be generated.
|
||||
query = "dm:bob@zulip.com,red";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["dm:bob@zulip.com,red"];
|
||||
// Do not suggest "myself@zulip.com" (the name of the current user) for dms
|
||||
pill_query = "dm:ted@zulip.com";
|
||||
query = "my";
|
||||
suggestions = get_suggestions(query, pill_query);
|
||||
expected = [
|
||||
"dm:ted@zulip.com my",
|
||||
"dm:ted@zulip.com sender:myself@zulip.com",
|
||||
"dm:ted@zulip.com dm-including:myself@zulip.com",
|
||||
"dm:ted@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// "is:dm" should be properly prepended to each suggestion
|
||||
// if the "dm" operator is negated.
|
||||
|
||||
query = "-dm:bob@zulip.com,";
|
||||
query = "-dm:bob@zulip.co";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = [
|
||||
"-dm:bob@zulip.com,",
|
||||
"is:dm -dm:bob@zulip.com,alice@zulip.com",
|
||||
"is:dm -dm:bob@zulip.com,jeff@zulip.com",
|
||||
"is:dm -dm:bob@zulip.com,ted@zulip.com",
|
||||
"-dm:bob@zulip.co",
|
||||
"is:dm -dm:bob@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
query = "-dm:bob@zulip.com,t";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["-dm:bob@zulip.com,t", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
query = "-dm:bob@zulip.com,Smit";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["-dm:bob@zulip.com,Smit", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
query = "-dm:bob@zulip.com,red";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["-dm:bob@zulip.com,red"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// If user types "pm-with" operator, an email and a comma,
|
||||
// show suggestions for group direct messages with the "dm"
|
||||
// operator.
|
||||
query = "pm-with:bob@zulip.com,";
|
||||
suggestions = get_suggestions(query);
|
||||
// If user types "pm-with" operator, show suggestions for
|
||||
// group direct messages with the "dm" operator.
|
||||
pill_query = "pm-with:bob@zulip.com";
|
||||
query = "alice";
|
||||
suggestions = get_suggestions(query, pill_query);
|
||||
expected = [
|
||||
"dm:bob@zulip.com,",
|
||||
"dm:bob@zulip.com alice",
|
||||
"dm:bob@zulip.com,alice@zulip.com",
|
||||
"dm:bob@zulip.com,jeff@zulip.com",
|
||||
"dm:bob@zulip.com,ted@zulip.com",
|
||||
"dm:bob@zulip.com sender:alice@zulip.com",
|
||||
"dm:bob@zulip.com dm-including:alice@zulip.com",
|
||||
"dm:bob@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// Test multiple terms
|
||||
query = "is:starred has:link dm:bob@zulip.com,Smit";
|
||||
suggestions = get_suggestions(query);
|
||||
pill_query = "is:starred has:link dm:bob@zulip.com";
|
||||
query = "Smit";
|
||||
suggestions = get_suggestions(query, pill_query);
|
||||
expected = [
|
||||
"is:starred has:link dm:bob@zulip.com,Smit",
|
||||
"is:starred has:link dm:bob@zulip.com Smit",
|
||||
"is:starred has:link dm:bob@zulip.com,ted@zulip.com",
|
||||
"is:starred has:link dm:bob@zulip.com sender:ted@zulip.com",
|
||||
"is:starred has:link dm:bob@zulip.com dm-including:ted@zulip.com",
|
||||
"is:starred has:link dm:bob@zulip.com",
|
||||
"is:starred has:link",
|
||||
"is:starred",
|
||||
];
|
||||
|
@ -377,62 +360,6 @@ test("group_suggestions", ({mock_template}) => {
|
|||
suggestions = get_suggestions(query);
|
||||
expected = ["has:link dm:invalid@zulip.com,Smit", "has:link"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
function message(user_ids, timestamp) {
|
||||
return {
|
||||
type: "private",
|
||||
display_recipient: user_ids.map((id) => ({
|
||||
id,
|
||||
})),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
direct_message_group_data.process_loaded_messages([
|
||||
message([bob.user_id, ted.user_id], 99),
|
||||
message([bob.user_id, ted.user_id, jeff.user_id], 98),
|
||||
]);
|
||||
|
||||
// Simulate a past group direct message which should now
|
||||
// prioritize ted over alice
|
||||
query = "dm:bob@zulip.com,";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = [
|
||||
"dm:bob@zulip.com,",
|
||||
"dm:bob@zulip.com,ted@zulip.com",
|
||||
"dm:bob@zulip.com,alice@zulip.com",
|
||||
"dm:bob@zulip.com,jeff@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// bob, ted, and jeff are already an existing group direct message,
|
||||
// so prioritize this one
|
||||
query = "dm:bob@zulip.com,ted@zulip.com,";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = [
|
||||
"dm:bob@zulip.com,ted@zulip.com,",
|
||||
"dm:bob@zulip.com,ted@zulip.com,jeff@zulip.com",
|
||||
"dm:bob@zulip.com,ted@zulip.com,alice@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
// bob, ted, and jeff are already an existing group direct message,
|
||||
// but if we start with just jeff, then don't prioritize ted over
|
||||
// alice because it doesn't complete the full group direct message.
|
||||
query = "dm:jeff@zulip.com,";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = [
|
||||
"dm:jeff@zulip.com,",
|
||||
"dm:jeff@zulip.com,alice@zulip.com",
|
||||
"dm:jeff@zulip.com,bob@zulip.com",
|
||||
"dm:jeff@zulip.com,ted@zulip.com",
|
||||
];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
query = "dm:jeff@zulip.com,ted@zulip.com hi";
|
||||
suggestions = get_suggestions(query);
|
||||
expected = ["dm:jeff@zulip.com,ted@zulip.com hi", "dm:jeff@zulip.com,ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
});
|
||||
|
||||
test("empty_query_suggestions", () => {
|
||||
|
@ -560,8 +487,8 @@ test("check_is_suggestions", ({override, mock_template}) => {
|
|||
"is:alerted",
|
||||
"is:unread",
|
||||
"is:resolved",
|
||||
"sender:alice@zulip.com",
|
||||
"dm:alice@zulip.com",
|
||||
"sender:alice@zulip.com",
|
||||
"dm-including:alice@zulip.com",
|
||||
"has:image",
|
||||
];
|
||||
|
@ -733,7 +660,7 @@ test("topic_suggestions", ({override, mock_template}) => {
|
|||
stream_data.add_sub({stream_id: office_id, name: "office", subscribed: true});
|
||||
|
||||
suggestions = get_suggestions("te");
|
||||
expected = ["te", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
|
||||
expected = ["te", "dm:ted@zulip.com", "sender:ted@zulip.com", "dm-including:ted@zulip.com"];
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
stream_topic_history.add_message({
|
||||
|
@ -751,8 +678,8 @@ test("topic_suggestions", ({override, mock_template}) => {
|
|||
suggestions = get_suggestions("te");
|
||||
expected = [
|
||||
"te",
|
||||
"sender:ted@zulip.com",
|
||||
"dm:ted@zulip.com",
|
||||
"sender:ted@zulip.com",
|
||||
"dm-including:ted@zulip.com",
|
||||
"channel:office topic:team",
|
||||
"channel:office topic:test",
|
||||
|
@ -929,10 +856,10 @@ test("people_suggestions", ({override, mock_template}) => {
|
|||
|
||||
let expected = [
|
||||
"te",
|
||||
"sender:bob@zulip.com",
|
||||
"sender:ted@zulip.com",
|
||||
"dm:bob@zulip.com", // bob térry
|
||||
"dm:ted@zulip.com",
|
||||
"sender:bob@zulip.com",
|
||||
"sender:ted@zulip.com",
|
||||
"dm-including:bob@zulip.com",
|
||||
"dm-including:ted@zulip.com",
|
||||
];
|
||||
|
@ -949,12 +876,12 @@ test("people_suggestions", ({override, mock_template}) => {
|
|||
|
||||
expected = [
|
||||
"te",
|
||||
"sender:bob@zulip.com",
|
||||
"sender:ted@zulip.com",
|
||||
"sender:user299@zulipdev.com",
|
||||
"dm:bob@zulip.com",
|
||||
"dm:ted@zulip.com",
|
||||
"dm:user299@zulipdev.com",
|
||||
"sender:bob@zulip.com",
|
||||
"sender:ted@zulip.com",
|
||||
"sender:user299@zulipdev.com",
|
||||
"dm-including:bob@zulip.com",
|
||||
"dm-including:ted@zulip.com",
|
||||
"dm-including:user299@zulipdev.com",
|
||||
|
@ -1035,7 +962,7 @@ test("people_suggestions", ({override, mock_template}) => {
|
|||
|
||||
suggestions = get_suggestions("Ted "); // note space
|
||||
|
||||
expected = ["Ted", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
|
||||
expected = ["Ted", "dm:ted@zulip.com", "sender:ted@zulip.com", "dm-including:ted@zulip.com"];
|
||||
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
|
||||
|
|
Loading…
Reference in New Issue