mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	compose: Change stream field in composebox to be a dropdown.
Fixes #11832 This lets the user see more options than the three that appear in the typeahead menu, and prevents them from inputting invalid stream names. This change replaces the input field with the dropdown, and updates everything that referred to the classnames of the old input field, so that they now get the data they need from the new dropdown.
This commit is contained in:
		@@ -13,6 +13,12 @@ async function check_compose_form_empty(page: Page): Promise<void> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function close_compose_box(page: Page): Promise<void> {
 | 
			
		||||
    const stream_dropdown_visible = (await page.$("#compose_select_stream_widget .open")) !== null;
 | 
			
		||||
 | 
			
		||||
    if (stream_dropdown_visible) {
 | 
			
		||||
        await page.keyboard.press("Escape");
 | 
			
		||||
        await page.waitForSelector("#id_compose_select_stream.open", {hidden: true});
 | 
			
		||||
    }
 | 
			
		||||
    await page.keyboard.press("Escape");
 | 
			
		||||
    await page.waitForSelector("#compose-textarea", {hidden: true});
 | 
			
		||||
}
 | 
			
		||||
@@ -119,7 +125,7 @@ async function test_narrow_to_private_messages_with_cordelia(page: Page): Promis
 | 
			
		||||
 | 
			
		||||
    await page.keyboard.press("KeyC");
 | 
			
		||||
    await page.waitForSelector("#compose", {visible: true});
 | 
			
		||||
    await page.waitForSelector(".compose_table #stream_message_recipient_stream:focus", {
 | 
			
		||||
    await page.waitForSelector(".compose_table #id_compose_select_stream.open", {
 | 
			
		||||
        visible: true,
 | 
			
		||||
    });
 | 
			
		||||
    await close_compose_box(page);
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,8 @@ async function create_stream_message_draft(page: Page): Promise<void> {
 | 
			
		||||
    console.log("Creating stream message draft");
 | 
			
		||||
    await page.keyboard.press("KeyC");
 | 
			
		||||
    await page.waitForSelector("#compose-stream-recipient", {visible: true});
 | 
			
		||||
    await common.select_item_via_dropdown(page, "#compose_select_stream_widget", "Denmark");
 | 
			
		||||
    await common.fill_form(page, "form#send_message_form", {
 | 
			
		||||
        stream_message_recipient_stream: "Denmark",
 | 
			
		||||
        stream_message_recipient_topic: "tests",
 | 
			
		||||
        content: "Test stream message.",
 | 
			
		||||
    });
 | 
			
		||||
@@ -129,8 +129,8 @@ async function test_restore_message_draft_via_draft_overlay(page: Page): Promise
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function edit_stream_message_draft(page: Page): Promise<void> {
 | 
			
		||||
    await common.select_item_via_dropdown(page, "#compose_select_stream_widget", "Denmark");
 | 
			
		||||
    await common.fill_form(page, "form#send_message_form", {
 | 
			
		||||
        stream_message_recipient_stream: "Denmark",
 | 
			
		||||
        stream_message_recipient_topic: "tests",
 | 
			
		||||
        content: "Updated stream message",
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,11 @@ import {test_credentials} from "../../../var/puppeteer/test_credentials";
 | 
			
		||||
const root_dir = path.resolve(__dirname, "../../..");
 | 
			
		||||
const puppeteer_dir = path.join(root_dir, "var/puppeteer");
 | 
			
		||||
 | 
			
		||||
type Message = Record<string, string | boolean> & {recipient?: string; content: string};
 | 
			
		||||
type Message = Record<string, string | boolean> & {
 | 
			
		||||
    recipient?: string;
 | 
			
		||||
    content: string;
 | 
			
		||||
    stream?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let browser: Browser | null = null;
 | 
			
		||||
let screenshot_id = 0;
 | 
			
		||||
@@ -205,7 +209,10 @@ export async function check_compose_state(
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    const form_params: Record<string, string> = {content: params.content};
 | 
			
		||||
    if (params.stream) {
 | 
			
		||||
        form_params.stream_message_recipient_stream = params.stream;
 | 
			
		||||
        assert.equal(
 | 
			
		||||
            await get_text_from_selector(page, "#compose_select_stream_name"),
 | 
			
		||||
            params.stream,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if (params.topic) {
 | 
			
		||||
        form_params.stream_message_recipient_topic = params.topic;
 | 
			
		||||
@@ -377,6 +384,26 @@ export async function wait_for_fully_processed_message(page: Page, content: stri
 | 
			
		||||
    await scroll_delay;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function select_item_via_dropdown(
 | 
			
		||||
    page: Page,
 | 
			
		||||
    dropdown_selector: string,
 | 
			
		||||
    item: string,
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    console.log(`Clicking on ${dropdown_selector} to select ${item}`);
 | 
			
		||||
    const menu_visible = (await page.$(`${dropdown_selector} .open`)) !== null;
 | 
			
		||||
    if (!menu_visible) {
 | 
			
		||||
        await page.waitForSelector(dropdown_selector, {visible: true});
 | 
			
		||||
        await page.click(`${dropdown_selector} .dropdown-toggle`);
 | 
			
		||||
        await page.waitForSelector(`${dropdown_selector} .dropdown-menu`, {visible: true});
 | 
			
		||||
    }
 | 
			
		||||
    const entry_selector = `xpath///*[${has_class_x(
 | 
			
		||||
        "list_item",
 | 
			
		||||
    )} and contains(normalize-space(), "${item}")]`;
 | 
			
		||||
    await page.waitForSelector(entry_selector, {visible: true});
 | 
			
		||||
    await page.click(entry_selector);
 | 
			
		||||
    await page.waitForSelector(`.dropdown-menu`, {visible: false});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Wait for any previous send to finish, then send a message.
 | 
			
		||||
export async function send_message(
 | 
			
		||||
    page: Page,
 | 
			
		||||
@@ -405,7 +432,7 @@ export async function send_message(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (params.stream) {
 | 
			
		||||
        params.stream_message_recipient_stream = params.stream;
 | 
			
		||||
        await select_item_via_dropdown(page, "#compose_select_stream_widget", params.stream);
 | 
			
		||||
        delete params.stream;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ async function test_mention(page: Page): Promise<void> {
 | 
			
		||||
    await page.keyboard.press("KeyC");
 | 
			
		||||
    await page.waitForSelector("#compose", {visible: true});
 | 
			
		||||
 | 
			
		||||
    await common.select_item_via_dropdown(page, "#compose_select_stream_widget", "Verona");
 | 
			
		||||
    await common.fill_form(page, 'form[action^="/json/messages"]', {
 | 
			
		||||
        stream_message_recipient_stream: "Verona",
 | 
			
		||||
        stream_message_recipient_topic: "Test mention all",
 | 
			
		||||
    });
 | 
			
		||||
    await common.select_item_via_typeahead(page, "#compose-textarea", "@**all", "all");
 | 
			
		||||
 
 | 
			
		||||
@@ -668,6 +668,11 @@ export function initialize() {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The dropdown menu needs to process clicks to open and close.
 | 
			
		||||
        if ($target.parents("#compose_stream_selection_dropdown").length > 0) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The mobile compose button has its own popover when clicked, so it already.
 | 
			
		||||
        // hides other popovers.
 | 
			
		||||
        if ($target.is(".compose_mobile_button, .compose_mobile_button *")) {
 | 
			
		||||
 
 | 
			
		||||
@@ -445,12 +445,14 @@ export function render_and_show_preview($preview_spinner, $preview_content_box,
 | 
			
		||||
 | 
			
		||||
export function initialize() {
 | 
			
		||||
    $("#below-compose-content .video_link").toggle(compute_show_video_chat_button());
 | 
			
		||||
    $(
 | 
			
		||||
        "#stream_message_recipient_stream,#stream_message_recipient_topic,#private_message_recipient",
 | 
			
		||||
    ).on("keyup", update_on_recipient_change);
 | 
			
		||||
    $(
 | 
			
		||||
        "#stream_message_recipient_stream,#stream_message_recipient_topic,#private_message_recipient",
 | 
			
		||||
    ).on("change", () => {
 | 
			
		||||
    // `keyup` isn't relevant for streams since it registers as a change only
 | 
			
		||||
    // when an item in the dropdown is selected.
 | 
			
		||||
    $("#stream_message_recipient_topic,#private_message_recipient").on(
 | 
			
		||||
        "keyup",
 | 
			
		||||
        update_on_recipient_change,
 | 
			
		||||
    );
 | 
			
		||||
    // changes for the stream dropdown are handled in on_compose_select_stream_update
 | 
			
		||||
    $("#stream_message_recipient_topic,#private_message_recipient").on("change", () => {
 | 
			
		||||
        update_on_recipient_change();
 | 
			
		||||
        compose_state.set_recipient_edited_manually(true);
 | 
			
		||||
    });
 | 
			
		||||
@@ -516,7 +518,7 @@ export function initialize() {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
 | 
			
		||||
            const stream_name = compose_state.stream_name();
 | 
			
		||||
            if (stream_name === undefined) {
 | 
			
		||||
            if (stream_name === "") {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            const sub = stream_data.get_sub(stream_name);
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ function get_focus_area(msg_type, opts) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (msg_type === "stream") {
 | 
			
		||||
        return "#stream_message_recipient_stream";
 | 
			
		||||
        return "#compose_select_stream_widget";
 | 
			
		||||
    }
 | 
			
		||||
    return "#private_message_recipient";
 | 
			
		||||
}
 | 
			
		||||
@@ -70,9 +70,16 @@ function get_focus_area(msg_type, opts) {
 | 
			
		||||
// Export for testing
 | 
			
		||||
export const _get_focus_area = get_focus_area;
 | 
			
		||||
 | 
			
		||||
export function open_compose_stream_dropup() {
 | 
			
		||||
    if ($("#id_compose_select_stream").hasClass("open")) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    $("#id_compose_select_stream > .dropdown-toggle").dropdown("toggle");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function set_focus(msg_type, opts) {
 | 
			
		||||
    const focus_area = get_focus_area(msg_type, opts);
 | 
			
		||||
    if (window.getSelection().toString() === "" || opts.trigger !== "message click") {
 | 
			
		||||
        const focus_area = get_focus_area(msg_type, opts);
 | 
			
		||||
        const $elt = $(focus_area);
 | 
			
		||||
        $elt.trigger("focus").trigger("select");
 | 
			
		||||
    }
 | 
			
		||||
@@ -321,6 +328,12 @@ export function start(msg_type, opts) {
 | 
			
		||||
        clear_box();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    compose_ui.compose_stream_widget.render(opts.stream);
 | 
			
		||||
    const $stream_header_colorblock = $("#compose_stream_selection_dropdown").find(
 | 
			
		||||
        ".stream_header_colorblock",
 | 
			
		||||
    );
 | 
			
		||||
    stream_bar.decorate(opts.stream, $stream_header_colorblock, false);
 | 
			
		||||
 | 
			
		||||
    // We set the stream/topic/private_message_recipient
 | 
			
		||||
    // unconditionally here, which assumes the caller will have passed
 | 
			
		||||
    // '' or undefined for these values if they are not appropriate
 | 
			
		||||
 
 | 
			
		||||
@@ -98,5 +98,10 @@ export function show_stream_does_not_exist_error(stream_name: string): void {
 | 
			
		||||
    const $compose_banner_area = $("#compose_banners");
 | 
			
		||||
    $compose_banner_area.append(new_row);
 | 
			
		||||
    hide_compose_spinner();
 | 
			
		||||
    $("#stream_message_recipient_stream").trigger("focus").trigger("select");
 | 
			
		||||
 | 
			
		||||
    // TODO: Replace with compose_actions.open_compose_stream_dropup() when it is converted to ts.
 | 
			
		||||
    if ($("#id_compose_select_stream").hasClass("open")) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    $("#id_compose_select_stream button").trigger("click");
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import $ from "jquery";
 | 
			
		||||
 | 
			
		||||
import * as compose_pm_pill from "./compose_pm_pill";
 | 
			
		||||
import * as compose_ui from "./compose_ui";
 | 
			
		||||
 | 
			
		||||
let message_type = false; // 'stream', 'private', or false-y
 | 
			
		||||
let recipient_edited_manually = false;
 | 
			
		||||
@@ -67,13 +68,13 @@ function get_or_set(fieldname, keep_leading_whitespace, no_trim) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stream_name() {
 | 
			
		||||
    return $("#stream_message_recipient_stream").val().trim();
 | 
			
		||||
    return compose_ui.compose_stream_widget.value();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function set_stream_name(newval) {
 | 
			
		||||
    if (newval !== undefined) {
 | 
			
		||||
        const $elem = $("#stream_message_recipient_stream");
 | 
			
		||||
        $elem.val(newval);
 | 
			
		||||
    if (newval !== undefined && newval !== "" && compose_ui.compose_stream_widget) {
 | 
			
		||||
        compose_ui.compose_stream_widget.render(newval);
 | 
			
		||||
        compose_ui.on_compose_select_stream_update(newval);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -127,7 +128,7 @@ export function focus_in_empty_compose(consider_start_of_whitespace_message_empt
 | 
			
		||||
            return private_message_recipient().length === 0;
 | 
			
		||||
        case "stream_message_recipient_topic":
 | 
			
		||||
            return topic() === "";
 | 
			
		||||
        case "stream_message_recipient_stream":
 | 
			
		||||
        case "compose_select_stream_name":
 | 
			
		||||
            return stream_name() === "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,16 @@ import $ from "jquery";
 | 
			
		||||
import {insert, replace, set, wrapSelection} from "text-field-edit";
 | 
			
		||||
 | 
			
		||||
import * as common from "./common";
 | 
			
		||||
import * as compose from "./compose";
 | 
			
		||||
import * as compose_actions from "./compose_actions";
 | 
			
		||||
import {DropdownListWidget} from "./dropdown_list_widget";
 | 
			
		||||
import {$t} from "./i18n";
 | 
			
		||||
import * as loading from "./loading";
 | 
			
		||||
import * as people from "./people";
 | 
			
		||||
import * as popover_menus from "./popover_menus";
 | 
			
		||||
import * as rtl from "./rtl";
 | 
			
		||||
import * as stream_bar from "./stream_bar";
 | 
			
		||||
import * as stream_data from "./stream_data";
 | 
			
		||||
import * as user_status from "./user_status";
 | 
			
		||||
 | 
			
		||||
export let compose_spinner_visible = false;
 | 
			
		||||
@@ -247,6 +252,9 @@ export function make_compose_box_full_size() {
 | 
			
		||||
 | 
			
		||||
    // Set the `top` property of compose-box.
 | 
			
		||||
    set_compose_box_top(true);
 | 
			
		||||
    // The compose select dropup should now open down because it's
 | 
			
		||||
    // at the top of the screen.
 | 
			
		||||
    $("#id_compose_select_stream").removeClass("dropup").addClass("dropdown");
 | 
			
		||||
 | 
			
		||||
    $(".collapse_composebox_button").show();
 | 
			
		||||
    $(".expand_composebox_button").hide();
 | 
			
		||||
@@ -261,6 +269,9 @@ export function make_compose_box_original_size() {
 | 
			
		||||
 | 
			
		||||
    // Unset the `top` property of compose-box.
 | 
			
		||||
    set_compose_box_top(false);
 | 
			
		||||
    // The compose select dropup should now open up because it's
 | 
			
		||||
    // near the bottom of the screen.
 | 
			
		||||
    $("#id_compose_select_stream").removeClass("dropdown").addClass("dropup");
 | 
			
		||||
 | 
			
		||||
    // Again initialise the compose textarea as it was destroyed
 | 
			
		||||
    // when compose box was made full screen
 | 
			
		||||
@@ -500,3 +511,49 @@ export function get_submit_button() {
 | 
			
		||||
    }
 | 
			
		||||
    return $("#compose-send-button");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function on_compose_select_stream_update(new_value) {
 | 
			
		||||
    const $stream_header_colorblock = $("#compose_stream_selection_dropdown").find(
 | 
			
		||||
        ".stream_header_colorblock",
 | 
			
		||||
    );
 | 
			
		||||
    stream_bar.decorate(new_value, $stream_header_colorblock, true);
 | 
			
		||||
    compose.update_on_recipient_change();
 | 
			
		||||
    $("#stream_message_recipient_topic").trigger("focus").trigger("select");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let compose_stream_widget;
 | 
			
		||||
export function initialize_compose_stream_dropdown() {
 | 
			
		||||
    const streams_list = stream_data
 | 
			
		||||
        .subscribed_subs()
 | 
			
		||||
        .filter((stream) => stream_data.can_post_messages_in_stream(stream))
 | 
			
		||||
        .map((stream) => ({
 | 
			
		||||
            name: stream.name,
 | 
			
		||||
            value: stream.name,
 | 
			
		||||
        }))
 | 
			
		||||
        .sort((a, b) => {
 | 
			
		||||
            if (a.name.toLowerCase() < b.name.toLowerCase()) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            if (a.name.toLowerCase() > b.name.toLowerCase()) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
            return 0;
 | 
			
		||||
        });
 | 
			
		||||
    const opts = {
 | 
			
		||||
        widget_name: "compose_select_stream",
 | 
			
		||||
        data: streams_list,
 | 
			
		||||
        default_text: $t({defaultMessage: "Select a stream"}),
 | 
			
		||||
        value: null,
 | 
			
		||||
        on_update: on_compose_select_stream_update,
 | 
			
		||||
    };
 | 
			
		||||
    compose_stream_widget = new DropdownListWidget(opts);
 | 
			
		||||
    compose_stream_widget.setup();
 | 
			
		||||
 | 
			
		||||
    $("#compose_select_stream_widget").on("select", (e) => {
 | 
			
		||||
        // We often focus on input fields to bring the user to fill it out.
 | 
			
		||||
        // In this situation, a focus on the dropdown div opens the dropdown
 | 
			
		||||
        // menu so that the user can select an option.
 | 
			
		||||
        compose_actions.open_compose_stream_dropup();
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -396,7 +396,7 @@ export function validation_error(error_type, stream_name) {
 | 
			
		||||
            compose_banner.show_error_message(
 | 
			
		||||
                $t({defaultMessage: "Error checking subscription."}),
 | 
			
		||||
                compose_banner.CLASSNAMES.subscription_error,
 | 
			
		||||
                $("#stream_message_recipient_stream"),
 | 
			
		||||
                $("#compose_select_stream_widget"),
 | 
			
		||||
            );
 | 
			
		||||
            return false;
 | 
			
		||||
        case "not-subscribed": {
 | 
			
		||||
@@ -443,7 +443,7 @@ function validate_stream_message() {
 | 
			
		||||
        compose_banner.show_error_message(
 | 
			
		||||
            $t({defaultMessage: "Please specify a stream."}),
 | 
			
		||||
            compose_banner.CLASSNAMES.missing_stream,
 | 
			
		||||
            $("#stream_message_recipient_stream"),
 | 
			
		||||
            $("#compose_select_stream_widget"),
 | 
			
		||||
        );
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -204,7 +204,6 @@ function handle_keydown(e) {
 | 
			
		||||
            target_sel = `#${CSS.escape(e.target.id)}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const on_stream = target_sel === "#stream_message_recipient_stream";
 | 
			
		||||
        const on_topic = target_sel === "#stream_message_recipient_topic";
 | 
			
		||||
        const on_pm = target_sel === "#private_message_recipient";
 | 
			
		||||
        const on_compose = target_sel === "#compose-textarea";
 | 
			
		||||
@@ -242,15 +241,9 @@ function handle_keydown(e) {
 | 
			
		||||
 | 
			
		||||
                handle_enter($("#compose-textarea"), e);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (on_stream || on_topic || on_pm) {
 | 
			
		||||
        } else if (on_topic || on_pm) {
 | 
			
		||||
            // We are doing the focusing on keyup to not abort the typeahead.
 | 
			
		||||
            if (on_stream) {
 | 
			
		||||
                $nextFocus = $("#stream_message_recipient_topic");
 | 
			
		||||
            } else if (on_topic) {
 | 
			
		||||
            $nextFocus = $("#compose-textarea");
 | 
			
		||||
            } else if (on_pm) {
 | 
			
		||||
                $nextFocus = $("#compose-textarea");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1096,24 +1089,6 @@ export function initialize() {
 | 
			
		||||
    $("form#send_message_form").on("keydown", handle_keydown);
 | 
			
		||||
    $("form#send_message_form").on("keyup", handle_keyup);
 | 
			
		||||
 | 
			
		||||
    // limit number of items so the list doesn't fall off the screen
 | 
			
		||||
    $("#stream_message_recipient_stream").typeahead({
 | 
			
		||||
        source() {
 | 
			
		||||
            return stream_data.subscribed_streams();
 | 
			
		||||
        },
 | 
			
		||||
        items: 3,
 | 
			
		||||
        fixed: true,
 | 
			
		||||
        highlighter(item) {
 | 
			
		||||
            return typeahead_helper.render_typeahead_item({primary: item});
 | 
			
		||||
        },
 | 
			
		||||
        matcher(item) {
 | 
			
		||||
            // The matcher for "stream" is strictly prefix-based,
 | 
			
		||||
            // because we want to avoid mixing up streams.
 | 
			
		||||
            const q = this.query.trim().toLowerCase();
 | 
			
		||||
            return item.toLowerCase().startsWith(q);
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $("#stream_message_recipient_topic").typeahead({
 | 
			
		||||
        source() {
 | 
			
		||||
            const stream_name = compose_state.stream_name();
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import $ from "jquery";
 | 
			
		||||
import * as stream_data from "./stream_data";
 | 
			
		||||
 | 
			
		||||
function update_compose_stream_icon(stream_name) {
 | 
			
		||||
    const $streamfield = $("#stream_message_recipient_stream");
 | 
			
		||||
    const $streamfield = $("#compose_select_stream_name");
 | 
			
		||||
    const $globe_icon = $("#compose-globe-icon");
 | 
			
		||||
    const $lock_icon = $("#compose-lock-icon");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import * as common from "./common";
 | 
			
		||||
import * as compose from "./compose";
 | 
			
		||||
import * as compose_closed_ui from "./compose_closed_ui";
 | 
			
		||||
import * as compose_pm_pill from "./compose_pm_pill";
 | 
			
		||||
import * as compose_ui from "./compose_ui";
 | 
			
		||||
import * as composebox_typeahead from "./composebox_typeahead";
 | 
			
		||||
import * as condense from "./condense";
 | 
			
		||||
import * as copy_and_paste from "./copy_and_paste";
 | 
			
		||||
@@ -88,7 +89,6 @@ import * as settings_sections from "./settings_sections";
 | 
			
		||||
import * as settings_toggle from "./settings_toggle";
 | 
			
		||||
import * as spoilers from "./spoilers";
 | 
			
		||||
import * as starred_messages from "./starred_messages";
 | 
			
		||||
import * as stream_bar from "./stream_bar";
 | 
			
		||||
import * as stream_data from "./stream_data";
 | 
			
		||||
import * as stream_edit from "./stream_edit";
 | 
			
		||||
import * as stream_edit_subscribers from "./stream_edit_subscribers";
 | 
			
		||||
@@ -293,6 +293,10 @@ export function initialize_kitchen_sink_stuff() {
 | 
			
		||||
 | 
			
		||||
    // Ignore wheel events in the compose area which weren't already handled above.
 | 
			
		||||
    $("#compose").on("wheel", (e) => {
 | 
			
		||||
        // Except for the stream select dropdown, which still needs scroll events.
 | 
			
		||||
        if ($(e.target).parents(".dropdown-list-body").length > 0) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
@@ -367,14 +371,6 @@ export function initialize_kitchen_sink_stuff() {
 | 
			
		||||
        $(this).removeClass("fa fa-play");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $("#stream_message_recipient_stream").on("change", function () {
 | 
			
		||||
        stream_bar.decorate(
 | 
			
		||||
            this.value,
 | 
			
		||||
            $("#compose-stream-recipient .message_header_stream"),
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(window).on("blur", () => {
 | 
			
		||||
        $(document.body).addClass("window_blurred");
 | 
			
		||||
    });
 | 
			
		||||
@@ -637,6 +633,7 @@ export function initialize_everything() {
 | 
			
		||||
    user_group_edit.initialize();
 | 
			
		||||
    stream_edit_subscribers.initialize();
 | 
			
		||||
    stream_data.initialize(stream_data_params);
 | 
			
		||||
    compose_ui.initialize_compose_stream_dropdown();
 | 
			
		||||
    user_group_edit_members.initialize();
 | 
			
		||||
    pm_conversations.recent.initialize(pm_conversations_params);
 | 
			
		||||
    user_topics.initialize();
 | 
			
		||||
 
 | 
			
		||||
@@ -514,22 +514,9 @@ input.recipient_box {
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#stream_message_recipient_stream.recipient_box {
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    border-radius: 0 3px 3px 0;
 | 
			
		||||
    border-left: 0;
 | 
			
		||||
 | 
			
		||||
    @media (width > $sm_min) {
 | 
			
		||||
        min-width: 120px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:focus {
 | 
			
		||||
        border-left: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.lock-padding {
 | 
			
		||||
        padding-left: 20px;
 | 
			
		||||
    }
 | 
			
		||||
#compose_select_stream_widget,
 | 
			
		||||
#compose_select_stream_widget .button {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#stream_message_recipient_topic.recipient_box {
 | 
			
		||||
@@ -774,6 +761,59 @@ a.compose_control_button.hide {
 | 
			
		||||
    margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#compose_stream_selection_dropdown {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
 | 
			
		||||
    .stream_header_colorblock {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropdown-toggle {
 | 
			
		||||
        border-radius: 0 4px 4px 0 !important;
 | 
			
		||||
        display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropup .dropdown-menu {
 | 
			
		||||
        top: auto;
 | 
			
		||||
        bottom: 100%;
 | 
			
		||||
        left: -10px;
 | 
			
		||||
        margin-bottom: 17px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropdown .dropdown-menu {
 | 
			
		||||
        top: auto;
 | 
			
		||||
        left: -10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #compose_select_stream_name {
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
 | 
			
		||||
        &.lock-padding {
 | 
			
		||||
            padding-left: 3px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropdown-list-body .list_item a {
 | 
			
		||||
        white-space: normal;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .fa-chevron-down {
 | 
			
		||||
        float: right;
 | 
			
		||||
        padding-bottom: 2px;
 | 
			
		||||
        padding-left: 5px;
 | 
			
		||||
        padding-bottom: 2px;
 | 
			
		||||
        color: hsl(0deg 0% 58%);
 | 
			
		||||
        font-weight: lighter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* This is the "Select a stream" default message */
 | 
			
		||||
    .text-warning {
 | 
			
		||||
        color: inherit;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu {
 | 
			
		||||
    & ul {
 | 
			
		||||
        list-style: none;
 | 
			
		||||
 
 | 
			
		||||
@@ -68,17 +68,15 @@
 | 
			
		||||
                            </template>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div id="compose-stream-recipient" class="order-1">
 | 
			
		||||
                            <div class="stream-selection-header-colorblock message_header_stream left_part" tab-index="-1"></div>
 | 
			
		||||
                            <div class="right_part">
 | 
			
		||||
                                <span id="compose-lock-icon">
 | 
			
		||||
                                    <i class="fa fa-lock" title="{{t 'This is a private stream' }}" aria-hidden="true"></i>
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span id="compose-globe-icon">
 | 
			
		||||
                                    <i class="zulip-icon zulip-icon-globe" title="{{t 'This is a web-public stream' }}" aria-hidden="true"></i>
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <a role="button" class="narrow_to_compose_recipients zulip-icon zulip-icon-arrow-left-circle order-1" data-tooltip-template-id="narrow_to_compose_recipients_tooltip" tabindex="0">
 | 
			
		||||
                                </a>
 | 
			
		||||
                                <input type="text" class="recipient_box" name="stream_message_recipient_stream" id="stream_message_recipient_stream" maxlength="{{ max_stream_name_length }}" value="" placeholder="{{t 'Stream' }}" autocomplete="off" tabindex="0" aria-label="{{t 'Stream' }}" />
 | 
			
		||||
                                <div id="compose_stream_selection_dropdown" class="new-style">
 | 
			
		||||
                                    <div class="stream_header_colorblock"></div>
 | 
			
		||||
                                    {{> settings/dropdown_list_widget
 | 
			
		||||
                                      widget_name="compose_select_stream"
 | 
			
		||||
                                      list_placeholder=(t 'Filter streams')}}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <i class="fa fa-angle-right" aria-hidden="true"></i>
 | 
			
		||||
                                <input type="text" class="recipient_box" name="stream_message_recipient_topic" id="stream_message_recipient_topic" maxlength="{{ max_topic_length }}" value="" placeholder="{{t 'Topic' }}" autocomplete="off" tabindex="0" aria-label="{{t 'Topic' }}" />
 | 
			
		||||
                            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const MockDate = require("mockdate");
 | 
			
		||||
 | 
			
		||||
const {mock_stream_header_colorblock} = require("./lib/compose");
 | 
			
		||||
const {mock_banners} = require("./lib/compose_banner");
 | 
			
		||||
const {$t} = require("./lib/i18n");
 | 
			
		||||
const {mock_esm, set_global, zrequire} = require("./lib/namespace");
 | 
			
		||||
@@ -303,6 +304,20 @@ test_ui("send_message", ({override, override_rewire, mock_template}) => {
 | 
			
		||||
test_ui("enter_with_preview_open", ({override, override_rewire}) => {
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    $("#compose-textarea").toggleClass = noop;
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    compose_actions.open_compose_stream_dropup = noop;
 | 
			
		||||
    compose.update_on_recipient_change = noop;
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = noop;
 | 
			
		||||
    let stream_value = "";
 | 
			
		||||
    compose_ui.compose_stream_widget = {
 | 
			
		||||
        value() {
 | 
			
		||||
            return stream_value;
 | 
			
		||||
        },
 | 
			
		||||
        render(val) {
 | 
			
		||||
            stream_value = val;
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    override_rewire(compose_banner, "clear_message_sent_banners", () => {});
 | 
			
		||||
    override(reminder, "is_deferred_delivery", () => false);
 | 
			
		||||
    override(document, "to_$", () => $("document-stub"));
 | 
			
		||||
@@ -351,6 +366,8 @@ test_ui("enter_with_preview_open", ({override, override_rewire}) => {
 | 
			
		||||
 | 
			
		||||
test_ui("finish", ({override, override_rewire}) => {
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    override_rewire(compose_banner, "clear_message_sent_banners", () => {});
 | 
			
		||||
    override(reminder, "is_deferred_delivery", () => false);
 | 
			
		||||
    override(document, "to_$", () => $("document-stub"));
 | 
			
		||||
@@ -516,9 +533,8 @@ test_ui("update_fade", ({override}) => {
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    initialize_handlers({override});
 | 
			
		||||
 | 
			
		||||
    const selector =
 | 
			
		||||
        "#stream_message_recipient_stream,#stream_message_recipient_topic,#private_message_recipient";
 | 
			
		||||
    const keyup_handler_func = $(selector).get_on_handler("keyup");
 | 
			
		||||
    const selector = "#stream_message_recipient_topic,#private_message_recipient";
 | 
			
		||||
    const keyup_handler_func = $(selector).get_on_handler("change");
 | 
			
		||||
 | 
			
		||||
    let set_focused_recipient_checked = false;
 | 
			
		||||
    let update_all_called = false;
 | 
			
		||||
@@ -575,6 +591,8 @@ test_ui("trigger_submit_compose_form", ({override, override_rewire}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test_ui("on_events", ({override}) => {
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    initialize_handlers({override});
 | 
			
		||||
 | 
			
		||||
    override(rendered_markdown, "update_elements", () => {});
 | 
			
		||||
@@ -745,6 +763,8 @@ test_ui("on_events", ({override}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test_ui("create_message_object", ({override, override_rewire}) => {
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    compose_state.set_stream_name("social");
 | 
			
		||||
    $("#stream_message_recipient_topic").val("lunch");
 | 
			
		||||
    $("#compose-textarea").val("burrito");
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const {mock_stream_header_colorblock} = require("./lib/compose");
 | 
			
		||||
const {mock_banners} = require("./lib/compose_banner");
 | 
			
		||||
const {mock_esm, set_global, zrequire} = require("./lib/namespace");
 | 
			
		||||
const {run_test} = require("./lib/test");
 | 
			
		||||
@@ -25,8 +26,18 @@ const compose_fade = mock_esm("../src/compose_fade", {
 | 
			
		||||
    clear_compose: noop,
 | 
			
		||||
});
 | 
			
		||||
const compose_pm_pill = mock_esm("../src/compose_pm_pill");
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
const compose_ui = mock_esm("../src/compose_ui", {
 | 
			
		||||
    autosize_textarea: noop,
 | 
			
		||||
    on_compose_select_stream_update: noop,
 | 
			
		||||
    compose_stream_widget: {
 | 
			
		||||
        value() {
 | 
			
		||||
            return stream_value;
 | 
			
		||||
        },
 | 
			
		||||
        render(val) {
 | 
			
		||||
            stream_value = val;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    is_full_size: () => false,
 | 
			
		||||
});
 | 
			
		||||
const hash_util = mock_esm("../src/hash_util");
 | 
			
		||||
@@ -62,6 +73,7 @@ const compose_state = zrequire("compose_state");
 | 
			
		||||
const compose_actions = zrequire("compose_actions");
 | 
			
		||||
const message_lists = zrequire("message_lists");
 | 
			
		||||
const stream_data = zrequire("stream_data");
 | 
			
		||||
const stream_bar = zrequire("stream_bar");
 | 
			
		||||
 | 
			
		||||
const start = compose_actions.start;
 | 
			
		||||
const cancel = compose_actions.cancel;
 | 
			
		||||
@@ -113,6 +125,8 @@ test("start", ({override, override_rewire}) => {
 | 
			
		||||
    override_rewire(compose_actions, "complete_starting_tasks", () => {});
 | 
			
		||||
    override_rewire(compose_actions, "blur_compose_inputs", () => {});
 | 
			
		||||
    override_rewire(compose_actions, "clear_textarea", () => {});
 | 
			
		||||
    stream_bar.decorate = () => {};
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    let compose_defaults;
 | 
			
		||||
    override(narrow_state, "set_compose_defaults", () => compose_defaults);
 | 
			
		||||
@@ -234,6 +248,7 @@ test("respond_to_message", ({override, override_rewire}) => {
 | 
			
		||||
    override_rewire(compose_actions, "complete_starting_tasks", () => {});
 | 
			
		||||
    override_rewire(compose_actions, "clear_textarea", () => {});
 | 
			
		||||
    override_private_message_recipient({override});
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    // Test PM
 | 
			
		||||
    const person = {
 | 
			
		||||
@@ -271,6 +286,7 @@ test("respond_to_message", ({override, override_rewire}) => {
 | 
			
		||||
 | 
			
		||||
test("reply_with_mention", ({override, override_rewire}) => {
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    compose_state.set_message_type("stream");
 | 
			
		||||
    override_rewire(compose_actions, "set_focus", () => {});
 | 
			
		||||
    override_rewire(compose_actions, "complete_starting_tasks", () => {});
 | 
			
		||||
@@ -318,6 +334,7 @@ test("reply_with_mention", ({override, override_rewire}) => {
 | 
			
		||||
 | 
			
		||||
test("quote_and_reply", ({disallow, override, override_rewire}) => {
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    compose_state.set_message_type("stream");
 | 
			
		||||
    const steve = {
 | 
			
		||||
        user_id: 90,
 | 
			
		||||
@@ -419,7 +436,7 @@ test("get_focus_area", () => {
 | 
			
		||||
        }),
 | 
			
		||||
        "#compose-textarea",
 | 
			
		||||
    );
 | 
			
		||||
    assert.equal(get_focus_area("stream", {}), "#stream_message_recipient_stream");
 | 
			
		||||
    assert.equal(get_focus_area("stream", {}), "#compose_select_stream_widget");
 | 
			
		||||
    assert.equal(get_focus_area("stream", {stream: "fun"}), "#stream_message_recipient_topic");
 | 
			
		||||
    assert.equal(get_focus_area("stream", {stream: "fun", topic: "more"}), "#compose-textarea");
 | 
			
		||||
    assert.equal(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,6 @@ const {run_test} = require("./lib/test");
 | 
			
		||||
 | 
			
		||||
mock_jquery((selector) => {
 | 
			
		||||
    switch (selector) {
 | 
			
		||||
        case "#stream_message_recipient_stream":
 | 
			
		||||
            return {
 | 
			
		||||
                val() {
 | 
			
		||||
                    return "social";
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
        case "#stream_message_recipient_topic":
 | 
			
		||||
            return {
 | 
			
		||||
                val() {
 | 
			
		||||
@@ -29,8 +23,15 @@ const stream_data = zrequire("stream_data");
 | 
			
		||||
const peer_data = zrequire("peer_data");
 | 
			
		||||
const people = zrequire("people");
 | 
			
		||||
const compose_fade = zrequire("compose_fade");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
const compose_fade_helper = zrequire("compose_fade_helper");
 | 
			
		||||
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return "social";
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const me = {
 | 
			
		||||
    email: "me@example.com",
 | 
			
		||||
    user_id: 30,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,28 @@
 | 
			
		||||
 | 
			
		||||
const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const {mock_stream_header_colorblock} = require("./lib/compose");
 | 
			
		||||
const {mock_esm, zrequire} = require("./lib/namespace");
 | 
			
		||||
const {run_test} = require("./lib/test");
 | 
			
		||||
const $ = require("./lib/zjquery");
 | 
			
		||||
 | 
			
		||||
const compose_pm_pill = mock_esm("../src/compose_pm_pill");
 | 
			
		||||
 | 
			
		||||
const compose_state = zrequire("compose_state");
 | 
			
		||||
const compose_fade = zrequire("compose_fade");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
 | 
			
		||||
const noop = () => {};
 | 
			
		||||
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return stream_value;
 | 
			
		||||
    },
 | 
			
		||||
    render(val) {
 | 
			
		||||
        stream_value = val;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
run_test("private_message_recipient", ({override}) => {
 | 
			
		||||
    let emails;
 | 
			
		||||
@@ -22,6 +38,12 @@ run_test("private_message_recipient", ({override}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("has_full_recipient", ({override}) => {
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    $(`#compose_banners .topic_resolved`).remove = noop;
 | 
			
		||||
    compose_fade.update_all = noop;
 | 
			
		||||
    $(".narrow_to_compose_recipients").toggleClass = noop;
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = noop;
 | 
			
		||||
 | 
			
		||||
    let emails;
 | 
			
		||||
    override(compose_pm_pill, "set_from_emails", (value) => {
 | 
			
		||||
        emails = value;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ const $ = require("./lib/zjquery");
 | 
			
		||||
const {page_params} = require("./lib/zpage_params");
 | 
			
		||||
 | 
			
		||||
const channel = mock_esm("../src/channel");
 | 
			
		||||
const compose_actions = mock_esm("../src/compose_actions");
 | 
			
		||||
 | 
			
		||||
const compose_actions = zrequire("compose_actions");
 | 
			
		||||
const compose_banner = zrequire("compose_banner");
 | 
			
		||||
const compose_pm_pill = zrequire("compose_pm_pill");
 | 
			
		||||
const compose_state = zrequire("compose_state");
 | 
			
		||||
@@ -23,6 +23,17 @@ const resolved_topic = zrequire("../shared/src/resolved_topic");
 | 
			
		||||
const settings_config = zrequire("settings_config");
 | 
			
		||||
const settings_data = mock_esm("../src/settings_data");
 | 
			
		||||
const stream_data = zrequire("stream_data");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return stream_value;
 | 
			
		||||
    },
 | 
			
		||||
    render(val) {
 | 
			
		||||
        stream_value = val;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const me = {
 | 
			
		||||
    email: "me@example.com",
 | 
			
		||||
@@ -136,8 +147,9 @@ test_ui("validate_stream_message_address_info", ({mock_template}) => {
 | 
			
		||||
    assert.ok(subscription_error_rendered);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test_ui("validate", ({override, mock_template}) => {
 | 
			
		||||
    override(compose_actions, "update_placeholder_text", () => {});
 | 
			
		||||
test_ui("validate", ({mock_template}) => {
 | 
			
		||||
    compose_actions.update_placeholder_text = () => {};
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = () => {};
 | 
			
		||||
 | 
			
		||||
    function initialize_pm_pill() {
 | 
			
		||||
        $.clear_all_elements();
 | 
			
		||||
@@ -654,6 +666,7 @@ test_ui("warn_if_private_stream_is_linked", ({mock_template}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test_ui("warn_if_mentioning_unsubscribed_user", ({override, mock_template}) => {
 | 
			
		||||
    stream_value = "";
 | 
			
		||||
    override(settings_data, "user_can_subscribe_other_users", () => true);
 | 
			
		||||
 | 
			
		||||
    let mentioned_details = {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const {mock_stream_header_colorblock} = require("./lib/compose");
 | 
			
		||||
const {mock_esm, set_global, with_overrides, zrequire} = require("./lib/namespace");
 | 
			
		||||
const {run_test} = require("./lib/test");
 | 
			
		||||
const $ = require("./lib/zjquery");
 | 
			
		||||
@@ -9,19 +10,14 @@ const {user_settings} = require("./lib/zpage_params");
 | 
			
		||||
 | 
			
		||||
const noop = () => {};
 | 
			
		||||
 | 
			
		||||
const compose = mock_esm("../src/compose", {
 | 
			
		||||
    finish: noop,
 | 
			
		||||
});
 | 
			
		||||
const compose_validate = mock_esm("../src/compose_validate", {
 | 
			
		||||
    validate_message_length: () => true,
 | 
			
		||||
    warn_if_topic_resolved: noop,
 | 
			
		||||
});
 | 
			
		||||
const input_pill = mock_esm("../src/input_pill");
 | 
			
		||||
const message_user_ids = mock_esm("../src/message_user_ids", {
 | 
			
		||||
    user_ids: () => [],
 | 
			
		||||
});
 | 
			
		||||
const stream_topic_history = mock_esm("../src/stream_topic_history", {
 | 
			
		||||
    stream_has_topics: () => false,
 | 
			
		||||
});
 | 
			
		||||
const stream_topic_history_util = mock_esm("../src/stream_topic_history_util");
 | 
			
		||||
 | 
			
		||||
let autosize_called;
 | 
			
		||||
@@ -35,6 +31,7 @@ set_global("setTimeout", (f, time) => {
 | 
			
		||||
set_global("document", "document-stub");
 | 
			
		||||
 | 
			
		||||
const typeahead = zrequire("../shared/src/typeahead");
 | 
			
		||||
const stream_topic_history = zrequire("stream_topic_history");
 | 
			
		||||
const compose_state = zrequire("compose_state");
 | 
			
		||||
const emoji = zrequire("emoji");
 | 
			
		||||
const typeahead_helper = zrequire("typeahead_helper");
 | 
			
		||||
@@ -42,6 +39,7 @@ const muted_users = zrequire("muted_users");
 | 
			
		||||
const people = zrequire("people");
 | 
			
		||||
const user_groups = zrequire("user_groups");
 | 
			
		||||
const stream_data = zrequire("stream_data");
 | 
			
		||||
const compose = zrequire("compose");
 | 
			
		||||
const compose_pm_pill = zrequire("compose_pm_pill");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
const composebox_typeahead = zrequire("composebox_typeahead");
 | 
			
		||||
@@ -55,6 +53,16 @@ const ct = composebox_typeahead;
 | 
			
		||||
// broadcast-mentions/persons/groups.
 | 
			
		||||
ct.__Rewire__("max_num_items", 15);
 | 
			
		||||
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return stream_value;
 | 
			
		||||
    },
 | 
			
		||||
    render(val) {
 | 
			
		||||
        stream_value = val;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
run_test("verify wildcard mentions typeahead for stream message", () => {
 | 
			
		||||
    const mention_all = ct.broadcast_mentions()[0];
 | 
			
		||||
    const mention_everyone = ct.broadcast_mentions()[1];
 | 
			
		||||
@@ -359,8 +367,8 @@ function test(label, f) {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test("topics_seen_for", ({override}) => {
 | 
			
		||||
    override(stream_topic_history, "get_recent_topic_names", (stream_id) => {
 | 
			
		||||
test("topics_seen_for", ({override, override_rewire}) => {
 | 
			
		||||
    override_rewire(stream_topic_history, "get_recent_topic_names", (stream_id) => {
 | 
			
		||||
        assert.equal(stream_id, denmark_stream.stream_id);
 | 
			
		||||
        return ["With Twisted Metal", "acceptance", "civil fears"];
 | 
			
		||||
    });
 | 
			
		||||
@@ -664,7 +672,11 @@ function sorted_names_from(subs) {
 | 
			
		||||
    return subs.map((sub) => sub.name).sort();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test("initialize", ({override, mock_template}) => {
 | 
			
		||||
test("initialize", ({override, override_rewire, mock_template}) => {
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    compose.update_on_recipient_change = noop;
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = noop;
 | 
			
		||||
 | 
			
		||||
    let pill_items = [];
 | 
			
		||||
    let cleared = false;
 | 
			
		||||
    let appended_names = [];
 | 
			
		||||
@@ -695,40 +707,6 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
    });
 | 
			
		||||
    override(stream_topic_history_util, "get_server_history", () => {});
 | 
			
		||||
 | 
			
		||||
    let stream_typeahead_called = false;
 | 
			
		||||
    $("#stream_message_recipient_stream").typeahead = (options) => {
 | 
			
		||||
        // options.source()
 | 
			
		||||
        //
 | 
			
		||||
        let actual_value = options.source();
 | 
			
		||||
        assert.deepEqual(actual_value.sort(), ["Denmark", "Sweden"]);
 | 
			
		||||
 | 
			
		||||
        // options.highlighter()
 | 
			
		||||
        options.query = "De";
 | 
			
		||||
        actual_value = options.highlighter("Denmark");
 | 
			
		||||
        expected_value = "<strong>Denmark</strong>";
 | 
			
		||||
        assert.equal(actual_value, expected_value);
 | 
			
		||||
 | 
			
		||||
        options.query = "the n";
 | 
			
		||||
        actual_value = options.highlighter("The Netherlands");
 | 
			
		||||
        expected_value = "<strong>The Netherlands</strong>";
 | 
			
		||||
        assert.equal(actual_value, expected_value);
 | 
			
		||||
 | 
			
		||||
        // options.matcher()
 | 
			
		||||
        options.query = "de";
 | 
			
		||||
        assert.equal(options.matcher("Denmark"), true);
 | 
			
		||||
        assert.equal(options.matcher("Sweden"), false);
 | 
			
		||||
 | 
			
		||||
        options.query = "De";
 | 
			
		||||
        assert.equal(options.matcher("Denmark"), true);
 | 
			
		||||
        assert.equal(options.matcher("Sweden"), false);
 | 
			
		||||
 | 
			
		||||
        options.query = "the ";
 | 
			
		||||
        assert.equal(options.matcher("The Netherlands"), true);
 | 
			
		||||
        assert.equal(options.matcher("Sweden"), false);
 | 
			
		||||
 | 
			
		||||
        stream_typeahead_called = true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let topic_typeahead_called = false;
 | 
			
		||||
    $("#stream_message_recipient_topic").typeahead = (options) => {
 | 
			
		||||
        const topics = ["<&>", "even more ice", "furniture", "ice", "kronor", "more ice"];
 | 
			
		||||
@@ -818,6 +796,8 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
            backend,
 | 
			
		||||
            call_center,
 | 
			
		||||
        ];
 | 
			
		||||
        actual_value.sort((a, b) => a.user_id - b.user_id);
 | 
			
		||||
        expected_value.sort((a, b) => a.user_id - b.user_id);
 | 
			
		||||
        assert.deepEqual(actual_value, expected_value);
 | 
			
		||||
 | 
			
		||||
        function matcher(query, person) {
 | 
			
		||||
@@ -888,6 +868,8 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
        query = "co"; // Matches everything ("x@zulip.COm")
 | 
			
		||||
        actual_value = sorter(query, [othello, deactivated_user, cordelia]);
 | 
			
		||||
        expected_value = [cordelia, deactivated_user, othello];
 | 
			
		||||
        actual_value.sort((a, b) => a.user_id - b.user_id);
 | 
			
		||||
        expected_value.sort((a, b) => a.user_id - b.user_id);
 | 
			
		||||
        assert.deepEqual(actual_value, expected_value);
 | 
			
		||||
 | 
			
		||||
        query = "non-existing-user";
 | 
			
		||||
@@ -1113,24 +1095,19 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
    $("#private_message_recipient").trigger("blur");
 | 
			
		||||
    assert.equal($("#private_message_recipient").val(), "othello@zulip.com");
 | 
			
		||||
 | 
			
		||||
    // handle_keydown()
 | 
			
		||||
    // the UI of selecting a stream is tested in puppeteer tests.
 | 
			
		||||
    compose_state.set_stream_name("Sweden");
 | 
			
		||||
 | 
			
		||||
    let event = {
 | 
			
		||||
        type: "keydown",
 | 
			
		||||
        key: "Enter",
 | 
			
		||||
        key: "Tab",
 | 
			
		||||
        shiftKey: false,
 | 
			
		||||
        target: {
 | 
			
		||||
            id: "stream_message_recipient_stream",
 | 
			
		||||
            id: "stream_message_recipient_topic",
 | 
			
		||||
        },
 | 
			
		||||
        preventDefault: noop,
 | 
			
		||||
        stopPropagation: noop,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
 | 
			
		||||
    event.key = "Tab";
 | 
			
		||||
    event.shiftKey = false;
 | 
			
		||||
    event.target.id = "stream_message_recipient_topic";
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
    event.target.id = "compose-textarea";
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
@@ -1155,7 +1132,7 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
    user_settings.enter_sends = false;
 | 
			
		||||
    event.metaKey = true;
 | 
			
		||||
    let compose_finish_called = false;
 | 
			
		||||
    override(compose, "finish", () => {
 | 
			
		||||
    override_rewire(compose, "finish", () => {
 | 
			
		||||
        compose_finish_called = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -1182,12 +1159,14 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
    event.key = "a";
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
 | 
			
		||||
    // the UI of selecting a stream is tested in puppeteer tests.
 | 
			
		||||
    compose_state.set_stream_name("Sweden");
 | 
			
		||||
    // handle_keyup()
 | 
			
		||||
    event = {
 | 
			
		||||
        type: "keydown",
 | 
			
		||||
        key: "Enter",
 | 
			
		||||
        target: {
 | 
			
		||||
            id: "stream_message_recipient_stream",
 | 
			
		||||
            id: "stream_message_recipient_topic",
 | 
			
		||||
        },
 | 
			
		||||
        preventDefault: noop,
 | 
			
		||||
    };
 | 
			
		||||
@@ -1202,7 +1181,6 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
    event.key = "a";
 | 
			
		||||
    $("form#send_message_form").trigger(event);
 | 
			
		||||
 | 
			
		||||
    $("#stream_message_recipient_stream").off("focus");
 | 
			
		||||
    $("#stream_message_recipient_topic").off("focus");
 | 
			
		||||
    $("#private_message_recipient").off("focus");
 | 
			
		||||
    $("form#send_message_form").off("keydown");
 | 
			
		||||
@@ -1213,7 +1191,6 @@ test("initialize", ({override, mock_template}) => {
 | 
			
		||||
 | 
			
		||||
    // Now let's make sure that all the stub functions have been called
 | 
			
		||||
    // during the initialization.
 | 
			
		||||
    assert.ok(stream_typeahead_called);
 | 
			
		||||
    assert.ok(topic_typeahead_called);
 | 
			
		||||
    assert.ok(pm_recipient_typeahead_called);
 | 
			
		||||
    assert.ok(compose_textarea_typeahead_called);
 | 
			
		||||
@@ -1257,11 +1234,6 @@ test("begins_typeahead", ({override, override_rewire}) => {
 | 
			
		||||
        assert.deepEqual(values, reference);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function assert_stream_list(input, rest = "") {
 | 
			
		||||
        const values = get_values(input, rest);
 | 
			
		||||
        assert.deepEqual(sorted_names_from(values), ["Denmark", "Sweden", "The Netherlands"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const people_only = {is_silent: true};
 | 
			
		||||
    const all_mentions = {is_silent: false};
 | 
			
		||||
    const lang_list = Object.keys(pygments_data.langs);
 | 
			
		||||
@@ -1276,7 +1248,6 @@ test("begins_typeahead", ({override, override_rewire}) => {
 | 
			
		||||
    // Make sure that the last token is the one we read.
 | 
			
		||||
    assert_typeahead_equals("~~~ @zulip", all_mentions);
 | 
			
		||||
    assert_typeahead_equals("@zulip :ta", emoji_list);
 | 
			
		||||
    assert_stream_list(":tada: #foo");
 | 
			
		||||
    assert_typeahead_equals("#foo\n~~~py", lang_list);
 | 
			
		||||
    assert_typeahead_equals(":tada: <time:", ["translated: Mention a time-zone-aware time"]);
 | 
			
		||||
 | 
			
		||||
@@ -1350,10 +1321,6 @@ test("begins_typeahead", ({override, override_rewire}) => {
 | 
			
		||||
    assert_typeahead_equals("test #", false);
 | 
			
		||||
    assert_typeahead_equals("test # a", false);
 | 
			
		||||
    assert_typeahead_equals("test no#o", false);
 | 
			
		||||
    assert_stream_list("#s");
 | 
			
		||||
    assert_stream_list(" #s");
 | 
			
		||||
    assert_stream_list("test #D");
 | 
			
		||||
    assert_stream_list("test #**v");
 | 
			
		||||
 | 
			
		||||
    assert_typeahead_equals("/", composebox_typeahead.slash_commands);
 | 
			
		||||
    assert_typeahead_equals("/m", composebox_typeahead.slash_commands);
 | 
			
		||||
@@ -1430,7 +1397,6 @@ test("begins_typeahead", ({override, override_rewire}) => {
 | 
			
		||||
    assert_typeahead_equals("~~~test", "ing", false);
 | 
			
		||||
    const terminal_symbols = ",.;?!()[]> \"'\n\t";
 | 
			
		||||
    for (const symbol of terminal_symbols.split()) {
 | 
			
		||||
        assert_stream_list("#test", symbol);
 | 
			
		||||
        assert_typeahead_equals("@test", symbol, all_mentions);
 | 
			
		||||
        assert_typeahead_equals(":test", symbol, emoji_list);
 | 
			
		||||
        assert_typeahead_equals("```test", symbol, lang_list);
 | 
			
		||||
@@ -1755,6 +1721,7 @@ test("PM recipients sorted according to stream / topic being viewed", ({override
 | 
			
		||||
        (stream_id, user_id) =>
 | 
			
		||||
            stream_id === denmark_stream.stream_id && user_id === cordelia.user_id,
 | 
			
		||||
    );
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
 | 
			
		||||
    // When viewing no stream, sorting is alphabetical
 | 
			
		||||
    compose_state.set_stream_name("");
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@
 | 
			
		||||
 | 
			
		||||
const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const {mock_stream_header_colorblock} = require("./lib/compose");
 | 
			
		||||
const {mock_banners} = require("./lib/compose_banner");
 | 
			
		||||
const {mock_esm, set_global, zrequire, with_overrides} = require("./lib/namespace");
 | 
			
		||||
const {run_test} = require("./lib/test");
 | 
			
		||||
const $ = require("./lib/zjquery");
 | 
			
		||||
@@ -11,10 +13,22 @@ const blueslip = zrequire("blueslip");
 | 
			
		||||
const compose_pm_pill = zrequire("compose_pm_pill");
 | 
			
		||||
const user_pill = zrequire("user_pill");
 | 
			
		||||
const people = zrequire("people");
 | 
			
		||||
const compose_fade = zrequire("compose_fade");
 | 
			
		||||
const compose_state = zrequire("compose_state");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
const sub_store = zrequire("sub_store");
 | 
			
		||||
const stream_data = zrequire("stream_data");
 | 
			
		||||
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return stream_value;
 | 
			
		||||
    },
 | 
			
		||||
    render(val) {
 | 
			
		||||
        stream_value = val;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const aaron = {
 | 
			
		||||
    email: "aaron@zulip.com",
 | 
			
		||||
    user_id: 6,
 | 
			
		||||
@@ -150,13 +164,20 @@ test("draft_model delete", ({override}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("snapshot_message", ({override_rewire}) => {
 | 
			
		||||
    override_rewire(user_pill, "get_user_ids", () => [aaron.user_id]);
 | 
			
		||||
    override_rewire(compose_pm_pill, "set_from_emails", noop);
 | 
			
		||||
    mock_banners();
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = noop;
 | 
			
		||||
 | 
			
		||||
    stream_data.get_sub = (stream_name) => {
 | 
			
		||||
        assert.equal(stream_name, "stream");
 | 
			
		||||
        return {stream_id: 30};
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    override_rewire(user_pill, "get_user_ids", () => [aaron.user_id]);
 | 
			
		||||
    override_rewire(compose_pm_pill, "set_from_emails", noop);
 | 
			
		||||
    $(".narrow_to_compose_recipients").toggleClass = noop;
 | 
			
		||||
 | 
			
		||||
    mock_stream_header_colorblock();
 | 
			
		||||
    compose_fade.update_all = noop;
 | 
			
		||||
 | 
			
		||||
    let curr_draft;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								web/tests/lib/compose.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/tests/lib/compose.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const $ = require("./zjquery");
 | 
			
		||||
 | 
			
		||||
exports.mock_stream_header_colorblock = () => {
 | 
			
		||||
    const $stream_selection_dropdown = $("#compose_stream_selection_dropdown");
 | 
			
		||||
    const $stream_header_colorblock = $(".stream_header_colorblock");
 | 
			
		||||
    $(".stream_header_colorblock").css = () => {};
 | 
			
		||||
    $stream_selection_dropdown.set_find_results(
 | 
			
		||||
        ".stream_header_colorblock",
 | 
			
		||||
        $stream_header_colorblock,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -17,6 +17,7 @@ const stream_data = zrequire("stream_data");
 | 
			
		||||
const {Filter} = zrequire("../src/filter");
 | 
			
		||||
const narrow = zrequire("narrow");
 | 
			
		||||
const settings_config = zrequire("settings_config");
 | 
			
		||||
const compose_ui = zrequire("compose_ui");
 | 
			
		||||
 | 
			
		||||
const compose_pm_pill = mock_esm("../src/compose_pm_pill");
 | 
			
		||||
mock_esm("../src/spectators", {
 | 
			
		||||
@@ -26,6 +27,16 @@ const recent_topics_util = mock_esm("../src/recent_topics_util", {
 | 
			
		||||
    is_visible() {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let stream_value = "";
 | 
			
		||||
compose_ui.compose_stream_widget = {
 | 
			
		||||
    value() {
 | 
			
		||||
        return stream_value;
 | 
			
		||||
    },
 | 
			
		||||
    render(val) {
 | 
			
		||||
        stream_value = val;
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function empty_narrow_html(title, html, search_data) {
 | 
			
		||||
    const opts = {
 | 
			
		||||
        title,
 | 
			
		||||
@@ -639,6 +650,7 @@ run_test("show_invalid_narrow_message", ({mock_template}) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("narrow_to_compose_target errors", ({disallow_rewire}) => {
 | 
			
		||||
    compose_ui.on_compose_select_stream_update = () => {};
 | 
			
		||||
    disallow_rewire(narrow, "activate");
 | 
			
		||||
 | 
			
		||||
    // No-op when not composing.
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user