search_suggestion: Show pills for all users in a group suggestion.

This fixes half of #23665. It shows all user pills when making
a group suggestions as the active new suggestion, but still
doesn't show user pills for the first parts of suggestion lines.
This commit is contained in:
evykassirer 2024-04-29 12:55:42 -07:00 committed by Tim Abbott
parent 387fba7156
commit 0899b621d3
5 changed files with 206 additions and 105 deletions

View File

@ -1,6 +1,8 @@
import Handlebars from "handlebars/runtime"; import Handlebars from "handlebars/runtime";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import render_user_pill from "../templates/user_pill.hbs";
import * as common from "./common"; import * as common from "./common";
import * as direct_message_group_data from "./direct_message_group_data"; import * as direct_message_group_data from "./direct_message_group_data";
import {Filter} from "./filter"; import {Filter} from "./filter";
@ -27,9 +29,29 @@ type TermPattern = Omit<NarrowTerm, "operand"> & Partial<Pick<NarrowTerm, "opera
export type Suggestion = { export type Suggestion = {
description_html: string; description_html: string;
search_string: string; search_string: string;
is_person?: boolean; } & (
user_pill_context?: UserPillItem; | {
}; is_people: false;
}
| {
is_people: true;
users: {
user_pill_context: UserPillItem;
}[];
}
);
function create_user_pill_context(user: User): UserPillItem {
const avatar_url = people.small_avatar_url_for_person(user);
return {
id: user.user_id,
display_value: new Handlebars.SafeString(user.full_name),
has_image: true,
img_src: avatar_url,
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user.user_id),
};
}
export const max_num_of_search_results = 12; export const max_num_of_search_results = 12;
@ -89,6 +111,10 @@ function format_as_suggestion(terms: NarrowTerm[]): Suggestion {
return { return {
description_html: Filter.search_description_as_html(terms), description_html: Filter.search_description_as_html(terms),
search_string: Filter.unparse(terms), search_string: Filter.unparse(terms),
// TODO: This isn't actually always false. We should
// treat terms with emails (dm, sender, etc) as `is_people`
// and show user pills for those.
is_people: false,
}; };
} }
@ -151,7 +177,7 @@ function get_channel_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggest
const regex = typeahead_helper.build_highlight_regex(query); const regex = typeahead_helper.build_highlight_regex(query);
const highlight_query = typeahead_helper.highlight_with_escaping_and_regex; const highlight_query = typeahead_helper.highlight_with_escaping_and_regex;
const objs = channels.map((channel) => { return channels.map((channel) => {
const prefix = "channel"; const prefix = "channel";
const highlighted_channel = highlight_query(regex, channel); const highlighted_channel = highlight_query(regex, channel);
const verb = last.negated ? "exclude " : ""; const verb = last.negated ? "exclude " : "";
@ -162,10 +188,8 @@ function get_channel_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggest
negated: last.negated, negated: last.negated,
}; };
const search_string = Filter.unparse([term]); const search_string = Filter.unparse([term]);
return {description_html, search_string}; return {description_html, search_string, is_people: false};
}); });
return objs;
} }
function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] { function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
@ -197,6 +221,22 @@ function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestio
// operand (not including the last part). // operand (not including the last part).
const parts = [...all_but_last_part.split(","), people.my_current_email()]; const parts = [...all_but_last_part.split(","), people.my_current_email()];
const all_users_but_last_part = [];
for (const email of all_but_last_part.split(",")) {
const user = people.get_by_email(email);
// Somehow an invalid email is showing up earlier in the group.
// This can happen if e.g. the user manually enters multiple emails.
// We won't have group suggestions built from an invalid user, so
// return an empty list.
if (user === undefined) {
return [];
}
all_users_but_last_part.push(user);
}
const user_pill_contexts = all_users_but_last_part.map((person) =>
create_user_pill_context(person),
);
const person_matcher = people.build_person_matcher(last_part); const person_matcher = people.build_person_matcher(last_part);
let persons = people.filter_all_persons((person) => { let persons = people.filter_all_persons((person) => {
if (parts.includes(person.email)) { if (parts.includes(person.email)) {
@ -214,33 +254,30 @@ function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestio
const person_highlighter = make_person_highlighter(last_part); const person_highlighter = make_person_highlighter(last_part);
const suggestions = persons.map((person) => { return persons.map((person) => {
const term = { const term = {
operator: "dm", operator: "dm",
operand: all_but_last_part + "," + person.email, operand: all_but_last_part + "," + person.email,
negated, negated,
}; };
// Note that description_html won't contain the user's
// identity; that instead will be rendered in the separate
// user pill.
const description_html =
prefix + Handlebars.Utils.escapeExpression(" " + all_but_last_part + ",");
let terms: NarrowTerm[] = [term]; let terms: NarrowTerm[] = [term];
if (negated) { if (negated) {
terms = [{operator: "is", operand: "dm"}, term]; terms = [{operator: "is", operand: "dm"}, term];
} }
const all_user_pill_contexts = [
...user_pill_contexts,
highlight_person(person, person_highlighter),
];
return { return {
description_html, description_html: prefix,
search_string: Filter.unparse(terms), search_string: Filter.unparse(terms),
is_person: true, is_people: true,
user_pill_context: highlight_person(person, person_highlighter), users: all_user_pill_contexts.map((user_pill_context) => ({user_pill_context})),
}; };
}); });
return suggestions;
} }
function make_people_getter(last: NarrowTerm): () => User[] { function make_people_getter(last: NarrowTerm): () => User[] {
@ -322,7 +359,7 @@ function get_person_suggestions(
const person_highlighter = make_person_highlighter(query); const person_highlighter = make_person_highlighter(query);
const objs = persons.map((person) => { return persons.map((person) => {
const terms: NarrowTerm[] = [ const terms: NarrowTerm[] = [
{ {
operator: autocomplete_operator, operator: autocomplete_operator,
@ -344,17 +381,19 @@ function get_person_suggestions(
return { return {
description_html: prefix, description_html: prefix,
search_string: Filter.unparse(terms), search_string: Filter.unparse(terms),
is_person: true, is_people: true,
user_pill_context: highlight_person(person, person_highlighter), users: [
{
user_pill_context: highlight_person(person, person_highlighter),
},
],
}; };
}); });
return objs;
} }
function get_default_suggestion_line(terms: NarrowTerm[]): SuggestionLine { function get_default_suggestion_line(terms: NarrowTerm[]): SuggestionLine {
if (terms.length === 0) { if (terms.length === 0) {
return [{description_html: "", search_string: ""}]; return [{description_html: "", search_string: "", is_people: false}];
} }
return terms.map((term) => format_as_suggestion([term])); return terms.map((term) => format_as_suggestion([term]));
} }
@ -512,10 +551,12 @@ function get_term_subset_suggestions(terms: NarrowTerm[]): Suggestion[] {
return suggestions; return suggestions;
} }
type SuggestionAndIncompatiblePatterns = Suggestion & {incompatible_patterns: TermPattern[]};
function get_special_filter_suggestions( function get_special_filter_suggestions(
last: NarrowTerm, last: NarrowTerm,
terms: NarrowTerm[], terms: NarrowTerm[],
suggestions: (Suggestion & {incompatible_patterns: TermPattern[]})[], suggestions: SuggestionAndIncompatiblePatterns[],
): Suggestion[] { ): Suggestion[] {
const is_search_operand_negated = last.operator === "search" && last.operand.startsWith("-"); const is_search_operand_negated = last.operator === "search" && last.operand.startsWith("-");
// Negating suggestions on is_search_operand_negated is required for // Negating suggestions on is_search_operand_negated is required for
@ -525,6 +566,7 @@ function get_special_filter_suggestions(
search_string: "-" + suggestion.search_string, search_string: "-" + suggestion.search_string,
description_html: "exclude " + suggestion.description_html, description_html: "exclude " + suggestion.description_html,
incompatible_patterns: suggestion.incompatible_patterns, incompatible_patterns: suggestion.incompatible_patterns,
is_people: false,
})); }));
} }
@ -560,10 +602,11 @@ function get_channels_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]):
if (last.operator === "search" && common.phrase_match(last.operand, "streams")) { if (last.operator === "search" && common.phrase_match(last.operand, "streams")) {
search_string = "streams:public"; search_string = "streams:public";
} }
const suggestions = [ const suggestions: SuggestionAndIncompatiblePatterns[] = [
{ {
search_string, search_string,
description_html: "All public channels in organization", description_html: "All public channels in organization",
is_people: false,
incompatible_patterns: [ incompatible_patterns: [
{operator: "is", operand: "dm"}, {operator: "is", operand: "dm"},
{operator: "channel"}, {operator: "channel"},
@ -577,10 +620,11 @@ function get_channels_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]):
return get_special_filter_suggestions(last, terms, suggestions); return get_special_filter_suggestions(last, terms, suggestions);
} }
function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] { function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
const suggestions = [ const suggestions: SuggestionAndIncompatiblePatterns[] = [
{ {
search_string: "is:dm", search_string: "is:dm",
description_html: "direct messages", description_html: "direct messages",
is_people: false,
incompatible_patterns: [ incompatible_patterns: [
{operator: "is", operand: "dm"}, {operator: "is", operand: "dm"},
{operator: "is", operand: "resolved"}, {operator: "is", operand: "resolved"},
@ -592,16 +636,19 @@ function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugge
{ {
search_string: "is:starred", search_string: "is:starred",
description_html: "starred messages", description_html: "starred messages",
is_people: false,
incompatible_patterns: [{operator: "is", operand: "starred"}], incompatible_patterns: [{operator: "is", operand: "starred"}],
}, },
{ {
search_string: "is:mentioned", search_string: "is:mentioned",
description_html: "@-mentions", description_html: "@-mentions",
is_people: false,
incompatible_patterns: [{operator: "is", operand: "mentioned"}], incompatible_patterns: [{operator: "is", operand: "mentioned"}],
}, },
{ {
search_string: "is:followed", search_string: "is:followed",
description_html: "followed topics", description_html: "followed topics",
is_people: false,
incompatible_patterns: [ incompatible_patterns: [
{operator: "is", operand: "followed"}, {operator: "is", operand: "followed"},
{operator: "is", operand: "dm"}, {operator: "is", operand: "dm"},
@ -612,16 +659,19 @@ function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugge
{ {
search_string: "is:alerted", search_string: "is:alerted",
description_html: "alerted messages", description_html: "alerted messages",
is_people: false,
incompatible_patterns: [{operator: "is", operand: "alerted"}], incompatible_patterns: [{operator: "is", operand: "alerted"}],
}, },
{ {
search_string: "is:unread", search_string: "is:unread",
description_html: "unread messages", description_html: "unread messages",
is_people: false,
incompatible_patterns: [{operator: "is", operand: "unread"}], incompatible_patterns: [{operator: "is", operand: "unread"}],
}, },
{ {
search_string: "is:resolved", search_string: "is:resolved",
description_html: "topics marked as resolved", description_html: "topics marked as resolved",
is_people: false,
incompatible_patterns: [ incompatible_patterns: [
{operator: "is", operand: "resolved"}, {operator: "is", operand: "resolved"},
{operator: "is", operand: "dm"}, {operator: "is", operand: "dm"},
@ -644,25 +694,29 @@ function get_is_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugge
} }
function get_has_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] { function get_has_filter_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestion[] {
const suggestions = [ const suggestions: SuggestionAndIncompatiblePatterns[] = [
{ {
search_string: "has:link", search_string: "has:link",
description_html: "messages that contain links", description_html: "messages that contain links",
is_people: false,
incompatible_patterns: [{operator: "has", operand: "link"}], incompatible_patterns: [{operator: "has", operand: "link"}],
}, },
{ {
search_string: "has:image", search_string: "has:image",
description_html: "messages that contain images", description_html: "messages that contain images",
is_people: false,
incompatible_patterns: [{operator: "has", operand: "image"}], incompatible_patterns: [{operator: "has", operand: "image"}],
}, },
{ {
search_string: "has:attachment", search_string: "has:attachment",
description_html: "messages that contain attachments", description_html: "messages that contain attachments",
is_people: false,
incompatible_patterns: [{operator: "has", operand: "attachment"}], incompatible_patterns: [{operator: "has", operand: "attachment"}],
}, },
{ {
search_string: "has:reaction", search_string: "has:reaction",
description_html: "messages that contain reactions", description_html: "messages that contain reactions",
is_people: false,
incompatible_patterns: [{operator: "has", operand: "reaction"}], incompatible_patterns: [{operator: "has", operand: "reaction"}],
}, },
]; ];
@ -699,6 +753,7 @@ function get_sent_by_me_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Sugg
{ {
search_string: sender_query, search_string: sender_query,
description_html, description_html,
is_people: false,
}, },
]; ];
} }
@ -795,27 +850,32 @@ class Attacher {
const search_strings = []; const search_strings = [];
for (const suggestion of suggestion_line) { for (const suggestion of suggestion_line) {
if (suggestion.description_html !== "") { if (suggestion.description_html !== "") {
description_htmls.push(suggestion.description_html); // To be able to render multiple user pills per suggestion,
// we generate the user pill html here and concatenate it
// together with other parts of the suggestion for the
// Suggestion html.
if (suggestion.is_people) {
const user_pills_html = suggestion.users
.map((user) => render_user_pill(user))
.join(" ");
description_htmls.push(suggestion.description_html + user_pills_html);
} else {
description_htmls.push(suggestion.description_html);
}
} }
if (suggestion.search_string !== "") { if (suggestion.search_string !== "") {
search_strings.push(suggestion.search_string); search_strings.push(suggestion.search_string);
} }
} }
const last_suggestion = suggestion_line.at(-1);
if (last_suggestion?.is_person) {
const user_pill_context = last_suggestion.user_pill_context;
assert(user_pill_context !== undefined);
return {
description_html: description_htmls.join(", "),
search_string: search_strings.join(" "),
is_person: true,
user_pill_context,
};
}
return { return {
description_html: description_htmls.join(", "), description_html: description_htmls.join(", "),
search_string: search_strings.join(" "), search_string: search_strings.join(" "),
// This is a full suggestion line of multiple suggestions,
// some of which might be people, but people can be suggested
// alongside non-people, so we do the html conversion for user
// suggestions already by this point.
is_people: false,
}; };
}); });
} }
@ -873,6 +933,7 @@ export function get_search_result(query: string): Suggestion[] {
description_html: `search for <strong>${Handlebars.Utils.escapeExpression( description_html: `search for <strong>${Handlebars.Utils.escapeExpression(
last.operand, last.operand,
)}</strong>`, )}</strong>`,
is_people: false,
}, },
]; ];
attacher.push([...attacher.base, ...suggestion_line]); attacher.push([...attacher.base, ...suggestion_line]);

View File

@ -1,8 +1,10 @@
<div class="search_list_item"> <div class="search_list_item">
<span>{{{ description_html }}}</span> <span>{{{ description_html }}}</span>
{{#if is_person}} {{#if is_people}}
<span class="pill-container pill-container-btn"> {{#each users}}
{{> input_pill user_pill_context}} <span class="pill-container pill-container-btn">
</span> {{> input_pill user_pill_context}}
</span>
{{/each}}
{{/if}} {{/if}}
</div> </div>

View File

@ -0,0 +1,3 @@
<span class="user-pill pill-container pill-container-btn">
{{> input_pill user_pill_context }}
</span>

View File

@ -26,11 +26,13 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
mock_template("search_list_item.hbs", true, (data, html) => { mock_template("search_list_item.hbs", true, (data, html) => {
assert.equal(typeof data.description_html, "string"); assert.equal(typeof data.description_html, "string");
if (data.is_person) { if (data.is_people) {
assert.equal(typeof data.user_pill_context.id, "number"); for (const user of data.users) {
assert.equal(typeof data.user_pill_context.display_value, "string"); assert.equal(typeof user.user_pill_context.id, "number");
assert.equal(typeof data.user_pill_context.has_image, "boolean"); assert.equal(typeof user.user_pill_context.display_value, "string");
assert.equal(typeof data.user_pill_context.img_src, "string"); assert.equal(typeof user.user_pill_context.has_image, "boolean");
assert.equal(typeof user.user_pill_context.img_src, "string");
}
} }
return html; return html;
}); });
@ -90,45 +92,57 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
"dm-including:zo", "dm-including:zo",
{ {
description_html: "group direct messages including", description_html: "group direct messages including",
is_person: true, is_people: true,
search_string: "dm-including:user7@zulipdev.com", search_string: "dm-including:user7@zulipdev.com",
user_pill_context: { users: [
display_value: "<strong>Zo</strong>e", {
has_image: true, user_pill_context: {
id: 7, display_value: "<strong>Zo</strong>e",
img_src: has_image: true,
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50", id: 7,
}, img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
}, },
], ],
[ [
"dm:zo", "dm:zo",
{ {
description_html: "direct messages with", description_html: "direct messages with",
is_person: true, is_people: true,
search_string: "dm:user7@zulipdev.com", search_string: "dm:user7@zulipdev.com",
user_pill_context: { users: [
display_value: "<strong>Zo</strong>e", {
has_image: true, user_pill_context: {
id: 7, display_value: "<strong>Zo</strong>e",
img_src: has_image: true,
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50", id: 7,
}, img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
}, },
], ],
[ [
"sender:zo", "sender:zo",
{ {
description_html: "sent by", description_html: "sent by",
is_person: true, is_people: true,
search_string: "sender:user7@zulipdev.com", search_string: "sender:user7@zulipdev.com",
user_pill_context: { users: [
display_value: "<strong>Zo</strong>e", {
has_image: true, user_pill_context: {
id: 7, display_value: "<strong>Zo</strong>e",
img_src: has_image: true,
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50", id: 7,
}, img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
}, },
], ],
[ [
@ -152,13 +166,13 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
let expected_value = `<div class="search_list_item">\n <span>Search for zo</span>\n</div>\n`; let expected_value = `<div class="search_list_item">\n <span>Search for zo</span>\n</div>\n`;
assert.equal(opts.highlighter_html(source[0]), expected_value); assert.equal(opts.highlighter_html(source[0]), expected_value);
expected_value = `<div class="search_list_item">\n <span>sent by</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`; expected_value = `<div class="search_list_item">\n <span>sent by</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`;
assert.equal(opts.highlighter_html(source[1]), expected_value); assert.equal(opts.highlighter_html(source[1]), expected_value);
expected_value = `<div class="search_list_item">\n <span>direct messages with</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`; expected_value = `<div class="search_list_item">\n <span>direct messages with</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`;
assert.equal(opts.highlighter_html(source[2]), expected_value); assert.equal(opts.highlighter_html(source[2]), expected_value);
expected_value = `<div class="search_list_item">\n <span>group direct messages including</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`; expected_value = `<div class="search_list_item">\n <span>group direct messages including</span>\n <span class="pill-container pill-container-btn">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d&#x3D;identicon&amp;version&#x3D;1&amp;s&#x3D;50" />\n <span class="pill-label">\n <span class="pill-value">&lt;strong&gt;Zo&lt;/strong&gt;e</span></span>\n <div class="exit">\n <span aria-hidden="true">&times;</span>\n </div>\n</div>\n </span>\n</div>\n`;
assert.equal(opts.highlighter_html(source[3]), expected_value); assert.equal(opts.highlighter_html(source[3]), expected_value);
/* Test sorter */ /* Test sorter */

View File

@ -132,6 +132,7 @@ test("subset_suggestions", ({mock_template}) => {
test("dm_suggestions", ({override, mock_template}) => { test("dm_suggestions", ({override, mock_template}) => {
mock_template("search_description.hbs", true, (_data, html) => html); mock_template("search_description.hbs", true, (_data, html) => html);
mock_template("user_pill.hbs", true, (_data, html) => html);
let query = "is:dm"; let query = "is:dm";
let suggestions = get_suggestions(query); let suggestions = get_suggestions(query);
@ -269,6 +270,7 @@ test("dm_suggestions", ({override, mock_template}) => {
test("group_suggestions", ({mock_template}) => { test("group_suggestions", ({mock_template}) => {
mock_template("search_description.hbs", true, (_data, html) => html); 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 // Entering a comma in a "dm:" query should immediately
// generate suggestions for the next person. // generate suggestions for the next person.
@ -359,6 +361,8 @@ test("group_suggestions", ({mock_template}) => {
]; ];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
// Doesn't show ted@ because it's invalid in combination
// with a channel.
query = "channel:Denmark has:link dm:bob@zulip.com,Smit"; query = "channel:Denmark has:link dm:bob@zulip.com,Smit";
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
expected = [ expected = [
@ -368,6 +372,12 @@ test("group_suggestions", ({mock_template}) => {
]; ];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
// Invalid emails don't give suggestions
query = "has:link dm:invalid@zulip.com,Smit";
suggestions = get_suggestions(query);
expected = ["has:link dm:invalid@zulip.com,Smit", "has:link"];
assert.deepEqual(suggestions.strings, expected);
function message(user_ids, timestamp) { function message(user_ids, timestamp) {
return { return {
type: "private", type: "private",
@ -533,6 +543,7 @@ test("has_suggestions", ({override, mock_template}) => {
test("check_is_suggestions", ({override, mock_template}) => { test("check_is_suggestions", ({override, mock_template}) => {
mock_template("search_description.hbs", true, (_data, html) => html); mock_template("search_description.hbs", true, (_data, html) => html);
mock_template("user_pill.hbs", true, (_data, html) => html);
stream_data.add_sub({stream_id: 44, name: "devel", subscribed: true}); stream_data.add_sub({stream_id: 44, name: "devel", subscribed: true});
stream_data.add_sub({stream_id: 77, name: "office", subscribed: true}); stream_data.add_sub({stream_id: 77, name: "office", subscribed: true});
@ -708,6 +719,7 @@ test("sent_by_me_suggestions", ({override, mock_template}) => {
test("topic_suggestions", ({override, mock_template}) => { test("topic_suggestions", ({override, mock_template}) => {
mock_template("search_description.hbs", true, (_data, html) => html); mock_template("search_description.hbs", true, (_data, html) => html);
mock_template("user_pill.hbs", true, (_data, html) => html);
let suggestions; let suggestions;
let expected; let expected;
@ -872,6 +884,7 @@ test("channel_completion", ({override, mock_template}) => {
test("people_suggestions", ({override, mock_template}) => { test("people_suggestions", ({override, mock_template}) => {
mock_template("search_description.hbs", true, (_data, html) => html); mock_template("search_description.hbs", true, (_data, html) => html);
mock_template("user_pill.hbs", true, (_data, html) => html);
let query = "te"; let query = "te";
@ -949,68 +962,76 @@ test("people_suggestions", ({override, mock_template}) => {
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
function is_person(q) { function is_person(q) {
return suggestions.lookup_table.get(q).is_person; return suggestions.lookup_table.get(q).description_html.includes(`class="user-pill`);
} }
assert.equal(is_person("dm:ted@zulip.com"), true); assert.equal(is_person("dm:ted@zulip.com"), true);
assert.equal(is_person("sender:ted@zulip.com"), true); assert.equal(is_person("sender:ted@zulip.com"), true);
assert.equal(is_person("dm-including:ted@zulip.com"), true); assert.equal(is_person("dm-including:ted@zulip.com"), true);
function has_image(q) { function has_image(q) {
return suggestions.lookup_table.get(q).user_pill_context.has_image; return suggestions.lookup_table.get(q).description_html.includes(`class="pill-image"`);
} }
assert.equal(has_image("dm:bob@zulip.com"), true); assert.equal(has_image("dm:bob@zulip.com"), true);
assert.equal(has_image("sender:bob@zulip.com"), true); assert.equal(has_image("sender:bob@zulip.com"), true);
assert.equal(has_image("dm-including:bob@zulip.com"), true); assert.equal(has_image("dm-including:bob@zulip.com"), true);
function describe(q) { function test_describe(q, description_html_start) {
return suggestions.lookup_table.get(q).description_html; assert.ok(
suggestions.lookup_table.get(q).description_html.startsWith(description_html_start),
);
} }
assert.equal(describe("dm:ted@zulip.com"), "Direct messages with"); test_describe("dm:ted@zulip.com", "Direct messages with");
assert.equal(describe("sender:ted@zulip.com"), "Sent by"); test_describe("sender:ted@zulip.com", "Sent by");
assert.equal(describe("dm-including:ted@zulip.com"), "Direct messages including"); test_describe("dm-including:ted@zulip.com", "Direct messages including");
let expectedString = "<strong>Te</strong>d Smith"; let expectedString = "<strong>Te</strong>d Smith";
function get_full_name(q) { function test_full_name(q, full_name_html) {
return suggestions.lookup_table.get(q).user_pill_context.display_value.string; return suggestions.lookup_table.get(q).description_html.includes(full_name_html);
} }
assert.equal(get_full_name("sender:ted@zulip.com"), expectedString); test_full_name("sender:ted@zulip.com", expectedString);
assert.equal(get_full_name("dm:ted@zulip.com"), expectedString); test_full_name("dm:ted@zulip.com", expectedString);
assert.equal(get_full_name("dm-including:ted@zulip.com"), expectedString); test_full_name("dm-including:ted@zulip.com", expectedString);
expectedString = example_avatar_url + "?s=50"; expectedString = example_avatar_url + "?s=50";
function get_avatar_url(q) { function test_avatar_url(q, avatar_url) {
return suggestions.lookup_table.get(q).user_pill_context.img_src; return suggestions.lookup_table.get(q).description_html.includes(avatar_url);
} }
assert.equal(get_avatar_url("dm:bob@zulip.com"), expectedString); test_avatar_url("dm:bob@zulip.com", expectedString);
assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString); test_avatar_url("sender:bob@zulip.com", expectedString);
assert.equal(get_avatar_url("dm-including:bob@zulip.com"), expectedString); test_avatar_url("dm-including:bob@zulip.com", expectedString);
function get_should_add_guest_user_indicator(q) { const guest_html = "<i>(guest)</i>";
return suggestions.lookup_table.get(q).user_pill_context.should_add_guest_user_indicator; function test_guest_user_indicator(q, should_have_guest_indicator) {
const description_html = suggestions.lookup_table.get(q).description_html;
if (should_have_guest_indicator) {
assert.ok(description_html.includes(guest_html));
} else {
assert.ok(!description_html.includes(guest_html));
}
} }
realm.realm_enable_guest_user_indicator = true; realm.realm_enable_guest_user_indicator = true;
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
assert.equal(get_should_add_guest_user_indicator("dm:bob@zulip.com"), false); test_guest_user_indicator("dm:bob@zulip.com", false);
assert.equal(get_should_add_guest_user_indicator("sender:bob@zulip.com"), false); test_guest_user_indicator("sender:bob@zulip.com", false);
assert.equal(get_should_add_guest_user_indicator("dm-including:bob@zulip.com"), false); test_guest_user_indicator("dm-including:bob@zulip.com", false);
bob.is_guest = true; bob.is_guest = true;
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
assert.equal(get_should_add_guest_user_indicator("dm:bob@zulip.com"), true); test_guest_user_indicator("dm:bob@zulip.com", true);
assert.equal(get_should_add_guest_user_indicator("sender:bob@zulip.com"), true); test_guest_user_indicator("sender:bob@zulip.com", true);
assert.equal(get_should_add_guest_user_indicator("dm-including:bob@zulip.com"), true); test_guest_user_indicator("dm-including:bob@zulip.com", true);
realm.realm_enable_guest_user_indicator = false; realm.realm_enable_guest_user_indicator = false;
suggestions = get_suggestions(query); suggestions = get_suggestions(query);
assert.equal(get_should_add_guest_user_indicator("dm:bob@zulip.com"), false); test_guest_user_indicator("dm:bob@zulip.com", false);
assert.equal(get_should_add_guest_user_indicator("sender:bob@zulip.com"), false); test_guest_user_indicator("sender:bob@zulip.com", false);
assert.equal(get_should_add_guest_user_indicator("dm-including:bob@zulip.com"), false); test_guest_user_indicator("dm-including:bob@zulip.com", false);
suggestions = get_suggestions("Ted "); // note space suggestions = get_suggestions("Ted "); // note space