mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	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:
		@@ -30,9 +30,9 @@ const search_pill = zrequire("search_pill");
 | 
			
		||||
const {Filter} = zrequire("../js/filter");
 | 
			
		||||
 | 
			
		||||
function test(label, f) {
 | 
			
		||||
    run_test(label, ({override}) => {
 | 
			
		||||
    run_test(label, ({override, mock_template}) => {
 | 
			
		||||
        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"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("initialize", () => {
 | 
			
		||||
test("initialize", ({mock_template}) => {
 | 
			
		||||
    const $search_query_box = $("#search_query");
 | 
			
		||||
    const $searchbox_form = $("#searchbox_form");
 | 
			
		||||
    const $search_button = $(".search_button");
 | 
			
		||||
    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_pill.get_search_string_for_current_filter = () => "is:starred";
 | 
			
		||||
@@ -106,7 +117,7 @@ test("initialize", () => {
 | 
			
		||||
                    [
 | 
			
		||||
                        "stream:Verona",
 | 
			
		||||
                        {
 | 
			
		||||
                            description_html: "Stream <strong>Ver</strong>ona",
 | 
			
		||||
                            description_html: "Stream <strong>Ver</strong>ona",
 | 
			
		||||
                            search_string: "stream:Verona",
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
@@ -128,16 +139,98 @@ test("initialize", () => {
 | 
			
		||||
            assert.equal(source, expected_source_value);
 | 
			
		||||
 | 
			
		||||
            /* 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);
 | 
			
		||||
 | 
			
		||||
            expected_value = "Stream <strong>Ver</strong>ona";
 | 
			
		||||
            expected_value = `<div class="search_list_item">\n    Stream <strong>Ver</strong>ona\n</div>\n`;
 | 
			
		||||
            assert.equal(opts.highlighter(source[1]), expected_value);
 | 
			
		||||
 | 
			
		||||
            /* Test sorter */
 | 
			
		||||
            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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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 is_blurred;
 | 
			
		||||
 
 | 
			
		||||
@@ -73,11 +73,22 @@ run_test("update_button_visibility", () => {
 | 
			
		||||
    assert.ok(!$search_button.prop("disabled"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("initialize", () => {
 | 
			
		||||
run_test("initialize", ({mock_template}) => {
 | 
			
		||||
    const $search_query_box = $("#search_query");
 | 
			
		||||
    const $searchbox_form = $("#searchbox_form");
 | 
			
		||||
    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_query_box.typeahead = (opts) => {
 | 
			
		||||
        assert.equal(opts.fixed, true);
 | 
			
		||||
@@ -92,7 +103,7 @@ run_test("initialize", () => {
 | 
			
		||||
                    [
 | 
			
		||||
                        "stream:Verona",
 | 
			
		||||
                        {
 | 
			
		||||
                            description_html: "Stream <strong>Ver</strong>ona",
 | 
			
		||||
                            description_html: "Stream <strong>Ver</strong>ona",
 | 
			
		||||
                            search_string: "stream:Verona",
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
@@ -114,16 +125,98 @@ run_test("initialize", () => {
 | 
			
		||||
            assert.equal(source, expected_source_value);
 | 
			
		||||
 | 
			
		||||
            /* 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);
 | 
			
		||||
 | 
			
		||||
            expected_value = "Stream <strong>Ver</strong>ona";
 | 
			
		||||
            expected_value = `<div class="search_list_item">\n    Stream <strong>Ver</strong>ona\n</div>\n`;
 | 
			
		||||
            assert.equal(opts.highlighter(source[1]), expected_value);
 | 
			
		||||
 | 
			
		||||
            /* Test sorter */
 | 
			
		||||
            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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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=identicon&version=1&s=50" />\n    <span class="pill-value"><strong>Zo</strong>e</span>\n    <div class="exit">\n        <span aria-hidden="true">×</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 is_blurred;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
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 {page_params} = require("../zjsunit/zpage_params");
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +51,7 @@ const jeff = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const noop = () => {};
 | 
			
		||||
const example_avatar_url = "http://example.com/example.png";
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
    page_params.is_admin = true;
 | 
			
		||||
@@ -831,6 +832,7 @@ function people_suggestion_setup() {
 | 
			
		||||
        email: "bob@zulip.com",
 | 
			
		||||
        user_id: 202,
 | 
			
		||||
        full_name: "Bob Térry",
 | 
			
		||||
        avatar_url: example_avatar_url,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    people.add_active_user(bob);
 | 
			
		||||
@@ -860,16 +862,35 @@ test("people_suggestions", ({override}) => {
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        describe("pm-with:ted@zulip.com"),
 | 
			
		||||
        "Private messages with <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
 | 
			
		||||
    );
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        describe("sender:ted@zulip.com"),
 | 
			
		||||
        "Sent by <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
 | 
			
		||||
    );
 | 
			
		||||
    const has_image = (q) => 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);
 | 
			
		||||
 | 
			
		||||
    const describe = (q) => suggestions.lookup_table.get(q).description_html;
 | 
			
		||||
    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
 | 
			
		||||
    expected = [
 | 
			
		||||
@@ -906,39 +927,6 @@ test("people_suggestions", ({override}) => {
 | 
			
		||||
    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}) => {
 | 
			
		||||
    override(narrow_state, "stream", () => undefined);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
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 {page_params} = require("../zjsunit/zpage_params");
 | 
			
		||||
 | 
			
		||||
@@ -50,6 +50,8 @@ const jeff = {
 | 
			
		||||
    full_name: "Jeff Zoolipson",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const example_avatar_url = "http://example.com/example.png";
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
    page_params.is_admin = true;
 | 
			
		||||
    page_params.search_pills_enabled = false;
 | 
			
		||||
@@ -805,6 +807,7 @@ test("people_suggestions", ({override}) => {
 | 
			
		||||
        email: "bob@zulip.com",
 | 
			
		||||
        user_id: 202,
 | 
			
		||||
        full_name: "Bob Térry",
 | 
			
		||||
        avatar_url: example_avatar_url,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const alice = {
 | 
			
		||||
@@ -829,17 +832,45 @@ test("people_suggestions", ({override}) => {
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
        return suggestions.lookup_table.get(q).description_html;
 | 
			
		||||
    }
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        describe("pm-with:ted@zulip.com"),
 | 
			
		||||
        "Private messages with <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
 | 
			
		||||
    );
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        describe("sender:ted@zulip.com"),
 | 
			
		||||
        "Sent by <strong>Te</strong>d Smith <<strong>te</strong>d@zulip.com>",
 | 
			
		||||
    );
 | 
			
		||||
    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";
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@@ -926,59 +957,3 @@ test("queries_with_spaces", () => {
 | 
			
		||||
    expected = ["stream:offi", "stream:office"];
 | 
			
		||||
    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");
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import $ from "jquery";
 | 
			
		||||
 | 
			
		||||
import render_search_list_item from "../templates/search_list_item.hbs";
 | 
			
		||||
 | 
			
		||||
import {Filter} from "./filter";
 | 
			
		||||
import * as message_view_header from "./message_view_header";
 | 
			
		||||
import * as narrow from "./narrow";
 | 
			
		||||
@@ -94,7 +96,7 @@ export function initialize() {
 | 
			
		||||
        naturalSearch: true,
 | 
			
		||||
        highlighter(item) {
 | 
			
		||||
            const obj = search_map.get(item);
 | 
			
		||||
            return obj.description_html;
 | 
			
		||||
            return render_search_list_item(obj);
 | 
			
		||||
        },
 | 
			
		||||
        matcher() {
 | 
			
		||||
            return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import * as huddle_data from "./huddle_data";
 | 
			
		||||
import * as narrow_state from "./narrow_state";
 | 
			
		||||
import {page_params} from "./page_params";
 | 
			
		||||
import * as people from "./people";
 | 
			
		||||
import * as settings_data from "./settings_data";
 | 
			
		||||
import * as stream_data from "./stream_data";
 | 
			
		||||
import * as stream_topic_history from "./stream_topic_history";
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
    return function (person) {
 | 
			
		||||
        if (settings_data.show_email()) {
 | 
			
		||||
            return (
 | 
			
		||||
                highlight_query(person.full_name) + " <" + highlight_query(person.email) + ">"
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        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) {
 | 
			
		||||
    const filter = new Filter(operators);
 | 
			
		||||
    return criteria.some((cr) => {
 | 
			
		||||
@@ -115,7 +121,7 @@ function get_stream_suggestions(last, operators) {
 | 
			
		||||
        const prefix = "stream";
 | 
			
		||||
        const highlighted_stream = highlight_query(regex, stream);
 | 
			
		||||
        const verb = last.negated ? "exclude " : "";
 | 
			
		||||
        const description_html = verb + prefix + " " + highlighted_stream;
 | 
			
		||||
        const description_html = verb + prefix + " " + highlighted_stream;
 | 
			
		||||
        const term = {
 | 
			
		||||
            operator: "stream",
 | 
			
		||||
            operand: stream,
 | 
			
		||||
@@ -178,15 +184,24 @@ function get_group_suggestions(last, operators) {
 | 
			
		||||
            operand: all_but_last_part + "," + person.email,
 | 
			
		||||
            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 =
 | 
			
		||||
            prefix + " " + Handlebars.Utils.escapeExpression(all_but_last_part) + "," + name;
 | 
			
		||||
            prefix + Handlebars.Utils.escapeExpression(" " + all_but_last_part + ",");
 | 
			
		||||
 | 
			
		||||
        let terms = [term];
 | 
			
		||||
        if (negated) {
 | 
			
		||||
            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;
 | 
			
		||||
@@ -254,8 +269,6 @@ function get_person_suggestions(people_getter, last, operators, autocomplete_ope
 | 
			
		||||
    const person_highlighter = make_person_highlighter(query);
 | 
			
		||||
 | 
			
		||||
    const objs = persons.map((person) => {
 | 
			
		||||
        const name = person_highlighter(person);
 | 
			
		||||
        const description_html = prefix + " " + name;
 | 
			
		||||
        const terms = [
 | 
			
		||||
            {
 | 
			
		||||
                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
 | 
			
		||||
            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;
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,20 @@
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.pill-container-btn {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
 | 
			
		||||
        .pill {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            border: none;
 | 
			
		||||
 | 
			
		||||
            .exit {
 | 
			
		||||
                display: none;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .input {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        padding: 2px 4px;
 | 
			
		||||
 
 | 
			
		||||
@@ -751,6 +751,16 @@ strong {
 | 
			
		||||
                    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
 | 
			
		||||
            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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								static/templates/search_list_item.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								static/templates/search_list_item.hbs
									
									
									
									
									
										Normal 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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user