search_suggestion: Show profile pictures in autocomplete suggestions.

Because the typeahead.js list items are currently just text, a user's
full name and avatar should be displayed in `input_pill`. To use
`input_pill`, a separate Handlebars partial view was created to
provide a mandatory container (`<div class="pill-container">`) for
`input_pill` and a flex container (`<div class="search_list_item">`)
for vertically aligning the text.

The description of each suggestion (i.e `description_html`) is
rendered as raw HTML, so every special character (e.g. whitespace)
should be HTML-escaped. This enables highlighting the substring in
each search suggestion that matches the query.

Fixes: #20267
This commit is contained in:
Oliver Pham
2021-12-06 17:44:26 -05:00
committed by Tim Abbott
parent 9cc8a2bc80
commit 2ed650f596
9 changed files with 335 additions and 134 deletions

View File

@@ -30,9 +30,9 @@ const search_pill = zrequire("search_pill");
const {Filter} = zrequire("../js/filter"); const {Filter} = zrequire("../js/filter");
function test(label, f) { function test(label, f) {
run_test(label, ({override}) => { run_test(label, ({override, mock_template}) => {
page_params.search_pills_enabled = true; page_params.search_pills_enabled = true;
f({override}); f({override, mock_template});
}); });
} }
@@ -81,12 +81,23 @@ test("update_button_visibility", () => {
assert.ok(!$search_button.prop("disabled")); assert.ok(!$search_button.prop("disabled"));
}); });
test("initialize", () => { test("initialize", ({mock_template}) => {
const $search_query_box = $("#search_query"); const $search_query_box = $("#search_query");
const $searchbox_form = $("#searchbox_form"); const $searchbox_form = $("#searchbox_form");
const $search_button = $(".search_button"); const $search_button = $(".search_button");
const $searchbox = $("#searchbox"); const $searchbox = $("#searchbox");
mock_template("search_list_item.hbs", true, (data, html) => {
assert.equal(typeof data.description_html, "string");
if (data.is_person) {
assert.equal(typeof data.user_pill_context.id, "number");
assert.equal(typeof data.user_pill_context.display_value, "string");
assert.equal(typeof data.user_pill_context.has_image, "boolean");
assert.equal(typeof data.user_pill_context.img_src, "string");
}
return html;
});
$search_query_box[0] = "stub"; $search_query_box[0] = "stub";
search_pill.get_search_string_for_current_filter = () => "is:starred"; search_pill.get_search_string_for_current_filter = () => "is:starred";
@@ -106,7 +117,7 @@ test("initialize", () => {
[ [
"stream:Verona", "stream:Verona",
{ {
description_html: "Stream <strong>Ver</strong>ona", description_html: "Stream&nbsp;<strong>Ver</strong>ona",
search_string: "stream:Verona", search_string: "stream:Verona",
}, },
], ],
@@ -128,16 +139,98 @@ test("initialize", () => {
assert.equal(source, expected_source_value); assert.equal(source, expected_source_value);
/* Test highlighter */ /* Test highlighter */
let expected_value = "Search for ver"; let expected_value = `<div class="search_list_item">\n Search for ver\n</div>\n`;
assert.equal(opts.highlighter(source[0]), expected_value); assert.equal(opts.highlighter(source[0]), expected_value);
expected_value = "Stream <strong>Ver</strong>ona"; expected_value = `<div class="search_list_item">\n Stream&nbsp;<strong>Ver</strong>ona\n</div>\n`;
assert.equal(opts.highlighter(source[1]), expected_value); assert.equal(opts.highlighter(source[1]), expected_value);
/* Test sorter */ /* Test sorter */
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings); assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
} }
{
const search_suggestions = {
lookup_table: new Map([
[
"group-pm-with:zo",
{
description_html: "group private messages including",
is_person: true,
search_string: "group-pm-with:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"pm-with:zo",
{
description_html: "private messages with",
is_person: true,
search_string: "pm-with:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"sender:zo",
{
description_html: "sent by",
is_person: true,
search_string: "sender:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"zo",
{
description_html: "Search for zo",
search_string: "zo",
},
],
]),
strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
};
/* Test source */
search_suggestion.get_suggestions = () => search_suggestions;
const expected_source_value = search_suggestions.strings;
const source = opts.source("zo");
assert.equal(source, expected_source_value);
/* Test highlighter */
let expected_value = `<div class="search_list_item">\n Search for zo\n</div>\n`;
assert.equal(opts.highlighter(source[0]), expected_value);
expected_value = `<div class="search_list_item">\n sent by\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[1]), expected_value);
expected_value = `<div class="search_list_item">\n private messages with\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[2]), expected_value);
expected_value = `<div class="search_list_item">\n group private messages including\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[3]), expected_value);
/* Test sorter */
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
}
{ {
let operators; let operators;
let is_blurred; let is_blurred;

View File

@@ -73,11 +73,22 @@ run_test("update_button_visibility", () => {
assert.ok(!$search_button.prop("disabled")); assert.ok(!$search_button.prop("disabled"));
}); });
run_test("initialize", () => { run_test("initialize", ({mock_template}) => {
const $search_query_box = $("#search_query"); const $search_query_box = $("#search_query");
const $searchbox_form = $("#searchbox_form"); const $searchbox_form = $("#searchbox_form");
const $search_button = $(".search_button"); const $search_button = $(".search_button");
mock_template("search_list_item.hbs", true, (data, html) => {
assert.equal(typeof data.description_html, "string");
if (data.is_person) {
assert.equal(typeof data.user_pill_context.id, "number");
assert.equal(typeof data.user_pill_context.display_value, "string");
assert.equal(typeof data.user_pill_context.has_image, "boolean");
assert.equal(typeof data.user_pill_context.img_src, "string");
}
return html;
});
search_suggestion.max_num_of_search_results = 999; search_suggestion.max_num_of_search_results = 999;
$search_query_box.typeahead = (opts) => { $search_query_box.typeahead = (opts) => {
assert.equal(opts.fixed, true); assert.equal(opts.fixed, true);
@@ -92,7 +103,7 @@ run_test("initialize", () => {
[ [
"stream:Verona", "stream:Verona",
{ {
description_html: "Stream <strong>Ver</strong>ona", description_html: "Stream&nbsp;<strong>Ver</strong>ona",
search_string: "stream:Verona", search_string: "stream:Verona",
}, },
], ],
@@ -114,16 +125,98 @@ run_test("initialize", () => {
assert.equal(source, expected_source_value); assert.equal(source, expected_source_value);
/* Test highlighter */ /* Test highlighter */
let expected_value = "Search for ver"; let expected_value = `<div class="search_list_item">\n Search for ver\n</div>\n`;
assert.equal(opts.highlighter(source[0]), expected_value); assert.equal(opts.highlighter(source[0]), expected_value);
expected_value = "Stream <strong>Ver</strong>ona"; expected_value = `<div class="search_list_item">\n Stream&nbsp;<strong>Ver</strong>ona\n</div>\n`;
assert.equal(opts.highlighter(source[1]), expected_value); assert.equal(opts.highlighter(source[1]), expected_value);
/* Test sorter */ /* Test sorter */
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings); assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
} }
{
const search_suggestions = {
lookup_table: new Map([
[
"group-pm-with:zo",
{
description_html: "group private messages including",
is_person: true,
search_string: "group-pm-with:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"pm-with:zo",
{
description_html: "private messages with",
is_person: true,
search_string: "pm-with:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"sender:zo",
{
description_html: "sent by",
is_person: true,
search_string: "sender:user7@zulipdev.com",
user_pill_context: {
display_value: "<strong>Zo</strong>e",
has_image: true,
id: 7,
img_src:
"https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1&s=50",
},
},
],
[
"zo",
{
description_html: "Search for zo",
search_string: "zo",
},
],
]),
strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
};
/* Test source */
search_suggestion.get_suggestions = () => search_suggestions;
const expected_source_value = search_suggestions.strings;
const source = opts.source("zo");
assert.equal(source, expected_source_value);
/* Test highlighter */
let expected_value = `<div class="search_list_item">\n Search for zo\n</div>\n`;
assert.equal(opts.highlighter(source[0]), expected_value);
expected_value = `<div class="search_list_item">\n sent by\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[1]), expected_value);
expected_value = `<div class="search_list_item">\n private messages with\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[2]), expected_value);
expected_value = `<div class="search_list_item">\n group private messages including\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-value">&lt;strong&gt;Zo&lt;/strong&gt;e</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(source[3]), expected_value);
/* Test sorter */
assert.equal(opts.sorter(search_suggestions.strings), search_suggestions.strings);
}
{ {
let operators; let operators;
let is_blurred; let is_blurred;

View File

@@ -2,7 +2,7 @@
const {strict: assert} = require("assert"); const {strict: assert} = require("assert");
const {mock_esm, with_overrides, zrequire} = require("../zjsunit/namespace"); const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test"); const {run_test} = require("../zjsunit/test");
const {page_params} = require("../zjsunit/zpage_params"); const {page_params} = require("../zjsunit/zpage_params");
@@ -51,6 +51,7 @@ const jeff = {
}; };
const noop = () => {}; const noop = () => {};
const example_avatar_url = "http://example.com/example.png";
function init() { function init() {
page_params.is_admin = true; page_params.is_admin = true;
@@ -831,6 +832,7 @@ function people_suggestion_setup() {
email: "bob@zulip.com", email: "bob@zulip.com",
user_id: 202, user_id: 202,
full_name: "Bob Térry", full_name: "Bob Térry",
avatar_url: example_avatar_url,
}; };
people.add_active_user(bob); people.add_active_user(bob);
@@ -860,16 +862,35 @@ test("people_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
const describe = (q) => suggestions.lookup_table.get(q).description_html; const is_person = (q) => suggestions.lookup_table.get(q).is_person;
assert.equal(is_person("pm-with:ted@zulip.com"), true);
assert.equal(is_person("sender:ted@zulip.com"), true);
assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
assert.equal( const has_image = (q) => suggestions.lookup_table.get(q).user_pill_context.has_image;
describe("pm-with:ted@zulip.com"), assert.equal(has_image("pm-with:bob@zulip.com"), true);
"Private messages with <strong>Te</strong>d Smith &lt;<strong>te</strong>d@zulip.com&gt;", assert.equal(has_image("sender:bob@zulip.com"), true);
); assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
assert.equal(
describe("sender:ted@zulip.com"), const describe = (q) => suggestions.lookup_table.get(q).description_html;
"Sent by <strong>Te</strong>d Smith &lt;<strong>te</strong>d@zulip.com&gt;", assert.equal(describe("pm-with:ted@zulip.com"), "Private messages with");
); assert.equal(describe("sender:ted@zulip.com"), "Sent by");
assert.equal(describe("group-pm-with:ted@zulip.com"), "Group private messages including");
let expectedString = "<strong>Te</strong>d Smith";
const get_full_name = (q) =>
suggestions.lookup_table.get(q).user_pill_context.display_value.string;
assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
expectedString = `${example_avatar_url}?s=50`;
const get_avatar_url = (q) => suggestions.lookup_table.get(q).user_pill_context.img_src;
assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
suggestions = get_suggestions("", "Ted "); // note space suggestions = get_suggestions("", "Ted "); // note space
expected = [ expected = [
@@ -906,39 +927,6 @@ test("people_suggestions", ({override}) => {
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
}); });
test("people_suggestion (Admin only email visibility)", ({override}) => {
/* Suggestions when realm_email_address_visibility is set to admin
only */
override(narrow_state, "stream", noop);
people_suggestion_setup();
const query = "te";
const suggestions = with_overrides(({override}) => {
override(page_params, "is_admin", false);
return get_suggestions("", query);
});
const expected = [
"te",
"sender:bob@zulip.com",
"sender:ted@zulip.com",
"pm-with:bob@zulip.com", // bob térry
"pm-with:ted@zulip.com",
"group-pm-with:bob@zulip.com",
"group-pm-with:ted@zulip.com",
];
assert.deepEqual(suggestions.strings, expected);
const describe = (q) => suggestions.lookup_table.get(q).description_html;
assert.equal(
describe("pm-with:ted@zulip.com"),
"Private messages with <strong>Te</strong>d Smith",
);
assert.equal(describe("sender:ted@zulip.com"), "Sent by <strong>Te</strong>d Smith");
});
test("operator_suggestions", ({override}) => { test("operator_suggestions", ({override}) => {
override(narrow_state, "stream", () => undefined); override(narrow_state, "stream", () => undefined);

View File

@@ -2,7 +2,7 @@
const {strict: assert} = require("assert"); const {strict: assert} = require("assert");
const {mock_esm, with_overrides, zrequire} = require("../zjsunit/namespace"); const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test"); const {run_test} = require("../zjsunit/test");
const {page_params} = require("../zjsunit/zpage_params"); const {page_params} = require("../zjsunit/zpage_params");
@@ -50,6 +50,8 @@ const jeff = {
full_name: "Jeff Zoolipson", full_name: "Jeff Zoolipson",
}; };
const example_avatar_url = "http://example.com/example.png";
function init() { function init() {
page_params.is_admin = true; page_params.is_admin = true;
page_params.search_pills_enabled = false; page_params.search_pills_enabled = false;
@@ -805,6 +807,7 @@ test("people_suggestions", ({override}) => {
email: "bob@zulip.com", email: "bob@zulip.com",
user_id: 202, user_id: 202,
full_name: "Bob Térry", full_name: "Bob Térry",
avatar_url: example_avatar_url,
}; };
const alice = { const alice = {
@@ -829,17 +832,45 @@ test("people_suggestions", ({override}) => {
]; ];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
function is_person(q) {
return suggestions.lookup_table.get(q).is_person;
}
assert.equal(is_person("pm-with:ted@zulip.com"), true);
assert.equal(is_person("sender:ted@zulip.com"), true);
assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
function has_image(q) {
return suggestions.lookup_table.get(q).user_pill_context.has_image;
}
assert.equal(has_image("pm-with:bob@zulip.com"), true);
assert.equal(has_image("sender:bob@zulip.com"), true);
assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
function describe(q) { function describe(q) {
return suggestions.lookup_table.get(q).description_html; return suggestions.lookup_table.get(q).description_html;
} }
assert.equal( assert.equal(describe("pm-with:ted@zulip.com"), "Private messages with");
describe("pm-with:ted@zulip.com"), assert.equal(describe("sender:ted@zulip.com"), "Sent by");
"Private messages with <strong>Te</strong>d Smith &lt;<strong>te</strong>d@zulip.com&gt;", assert.equal(describe("group-pm-with:ted@zulip.com"), "Group private messages including");
);
assert.equal( let expectedString = "<strong>Te</strong>d Smith";
describe("sender:ted@zulip.com"),
"Sent by <strong>Te</strong>d Smith &lt;<strong>te</strong>d@zulip.com&gt;", function get_full_name(q) {
); return suggestions.lookup_table.get(q).user_pill_context.display_value.string;
}
assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
expectedString = example_avatar_url + "?s=50";
function get_avatar_url(q) {
return suggestions.lookup_table.get(q).user_pill_context.img_src;
}
assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
suggestions = get_suggestions("", "Ted "); // note space suggestions = get_suggestions("", "Ted "); // note space
@@ -926,59 +957,3 @@ test("queries_with_spaces", () => {
expected = ["stream:offi", "stream:office"]; expected = ["stream:offi", "stream:office"];
assert.deepEqual(suggestions.strings, expected); assert.deepEqual(suggestions.strings, expected);
}); });
function people_suggestion_setup() {
const ted = {
email: "ted@zulip.com",
user_id: 201,
full_name: "Ted Smith",
};
people.add_active_user(ted);
const bob = {
email: "bob@zulip.com",
user_id: 202,
full_name: "Bob Térry",
};
people.add_active_user(bob);
const alice = {
email: "alice@zulip.com",
user_id: 203,
full_name: "Alice Ignore",
};
people.add_active_user(alice);
}
test("people_suggestion (Admin only email visibility)", ({override}) => {
/* Suggestions when realm_email_address_visibility is set to admin
only */
override(narrow_state, "stream", () => {});
people_suggestion_setup();
const query = "te";
const suggestions = with_overrides(({override}) => {
override(page_params, "is_admin", false);
return get_suggestions("", query);
});
const expected = [
"te",
"sender:bob@zulip.com",
"sender:ted@zulip.com",
"pm-with:bob@zulip.com", // bob térry
"pm-with:ted@zulip.com",
"group-pm-with:bob@zulip.com",
"group-pm-with:ted@zulip.com",
];
assert.deepEqual(suggestions.strings, expected);
const describe = (q) => suggestions.lookup_table.get(q).description_html;
assert.equal(
describe("pm-with:ted@zulip.com"),
"Private messages with <strong>Te</strong>d Smith",
);
assert.equal(describe("sender:ted@zulip.com"), "Sent by <strong>Te</strong>d Smith");
});

View File

@@ -1,5 +1,7 @@
import $ from "jquery"; import $ from "jquery";
import render_search_list_item from "../templates/search_list_item.hbs";
import {Filter} from "./filter"; import {Filter} from "./filter";
import * as message_view_header from "./message_view_header"; import * as message_view_header from "./message_view_header";
import * as narrow from "./narrow"; import * as narrow from "./narrow";
@@ -94,7 +96,7 @@ export function initialize() {
naturalSearch: true, naturalSearch: true,
highlighter(item) { highlighter(item) {
const obj = search_map.get(item); const obj = search_map.get(item);
return obj.description_html; return render_search_list_item(obj);
}, },
matcher() { matcher() {
return true; return true;

View File

@@ -6,7 +6,6 @@ import * as huddle_data from "./huddle_data";
import * as narrow_state from "./narrow_state"; import * as narrow_state from "./narrow_state";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as people from "./people"; import * as people from "./people";
import * as settings_data from "./settings_data";
import * as stream_data from "./stream_data"; import * as stream_data from "./stream_data";
import * as stream_topic_history from "./stream_topic_history"; import * as stream_topic_history from "./stream_topic_history";
import * as stream_topic_history_util from "./stream_topic_history_util"; import * as stream_topic_history_util from "./stream_topic_history_util";
@@ -22,15 +21,22 @@ function make_person_highlighter(query) {
const highlight_query = typeahead_helper.make_query_highlighter(query); const highlight_query = typeahead_helper.make_query_highlighter(query);
return function (person) { return function (person) {
if (settings_data.show_email()) {
return (
highlight_query(person.full_name) + " &lt;" + highlight_query(person.email) + "&gt;"
);
}
return highlight_query(person.full_name); return highlight_query(person.full_name);
}; };
} }
function highlight_person(person, highlighter) {
const avatar_url = people.small_avatar_url_for_person(person);
const highlighted_name = highlighter(person);
return {
id: person.user_id,
display_value: new Handlebars.SafeString(highlighted_name),
has_image: true,
img_src: avatar_url,
};
}
function match_criteria(operators, criteria) { function match_criteria(operators, criteria) {
const filter = new Filter(operators); const filter = new Filter(operators);
return criteria.some((cr) => { return criteria.some((cr) => {
@@ -115,7 +121,7 @@ function get_stream_suggestions(last, operators) {
const prefix = "stream"; const prefix = "stream";
const highlighted_stream = highlight_query(regex, stream); const highlighted_stream = highlight_query(regex, stream);
const verb = last.negated ? "exclude " : ""; const verb = last.negated ? "exclude " : "";
const description_html = verb + prefix + " " + highlighted_stream; const description_html = verb + prefix + "&nbsp;" + highlighted_stream;
const term = { const term = {
operator: "stream", operator: "stream",
operand: stream, operand: stream,
@@ -178,15 +184,24 @@ function get_group_suggestions(last, operators) {
operand: all_but_last_part + "," + person.email, operand: all_but_last_part + "," + person.email,
negated, negated,
}; };
const name = person_highlighter(person);
// Note that description_html won't contain the user's
// identity; that instead will be rendered in the separate
// user pill.
const description_html = const description_html =
prefix + " " + Handlebars.Utils.escapeExpression(all_but_last_part) + "," + name; prefix + Handlebars.Utils.escapeExpression(" " + all_but_last_part + ",");
let terms = [term]; let terms = [term];
if (negated) { if (negated) {
terms = [{operator: "is", operand: "private"}, term]; terms = [{operator: "is", operand: "private"}, term];
} }
const search_string = Filter.unparse(terms);
return {description_html, search_string}; return {
description_html,
search_string: Filter.unparse(terms),
is_person: true,
user_pill_context: highlight_person(person, person_highlighter),
};
}); });
return suggestions; return suggestions;
@@ -254,8 +269,6 @@ function get_person_suggestions(people_getter, last, operators, autocomplete_ope
const person_highlighter = make_person_highlighter(query); const person_highlighter = make_person_highlighter(query);
const objs = persons.map((person) => { const objs = persons.map((person) => {
const name = person_highlighter(person);
const description_html = prefix + " " + name;
const terms = [ const terms = [
{ {
operator: autocomplete_operator, operator: autocomplete_operator,
@@ -268,8 +281,13 @@ function get_person_suggestions(people_getter, last, operators, autocomplete_ope
// because we assume the user still wants to narrow to PMs // because we assume the user still wants to narrow to PMs
terms.unshift({operator: "is", operand: "private"}); terms.unshift({operator: "is", operand: "private"});
} }
const search_string = Filter.unparse(terms);
return {description_html, search_string}; return {
description_html: prefix,
search_string: Filter.unparse(terms),
is_person: true,
user_pill_context: highlight_person(person, person_highlighter),
};
}); });
return objs; return objs;

View File

@@ -73,6 +73,20 @@
} }
} }
&.pill-container-btn {
cursor: pointer;
padding: 0;
.pill {
margin: 0;
border: none;
.exit {
display: none;
}
}
}
.input { .input {
display: inline-block; display: inline-block;
padding: 2px 4px; padding: 2px 4px;

View File

@@ -751,6 +751,16 @@ strong {
hsl(200, 100%, 35%) hsl(200, 100%, 35%)
); );
} }
.search_list_item {
display: flex;
align-items: center;
}
.search_list_item .pill-container {
margin-left: 5px;
}
/* styles defined for user_circle here only deal with positioning of user_presence_circle /* styles defined for user_circle here only deal with positioning of user_presence_circle
in typeahead list in order to ensure they are rendered correctly in in all screen sizes. in typeahead list in order to ensure they are rendered correctly in in all screen sizes.
Most of the style rules related to color, gradient etc. which are generally common throughout Most of the style rules related to color, gradient etc. which are generally common throughout

View File

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