compose: Add tooltip support for invalid messages.

This commit adds tooltip support for various
invalid conditions mentioned in issue 32115.

A `show_banner` positional argument is added
in the `validate` method which has a default
value of true.

The reason behind introducing this is to
not trigger banners on hovering the disabled
send button, since the tooltip message is also
determined using the same validate method.

We want to only disable the button on hover,
which is why the update_send_button_status() method
is called only on "mouseenter" event, which is
added to the send button in compose_setup.js

To incorporate this change a new param is
introduced which determines whether to enable/disable
send_button by running update_send_button_status

Earlier, typing something in the textarea or
recipient box would also trigger
`update_send_button_status` which doesn't
work well since we've introduced a lot of
new booleans which determine whether send
button gets disabled causing send button to
get disabled while typing instead while hovering
Hence this change.
This commit is contained in:
apoorvapendse
2025-01-10 12:06:44 +05:30
committed by Tim Abbott
parent 8496a40564
commit 0b12c51771
5 changed files with 100 additions and 22 deletions

View File

@@ -70,6 +70,14 @@ export function initialize() {
compose_ui.handle_keyup(event, $("textarea#compose-textarea").expectOne()); compose_ui.handle_keyup(event, $("textarea#compose-textarea").expectOne());
}); });
$("#compose-send-button").on("mouseenter", () => {
compose_validate.validate(false, false);
compose_validate.update_send_button_status();
});
$("#compose-send-button").on("mouseleave", () => {
$(".message-send-controls").removeClass("disabled-message-send-controls");
});
$("textarea#compose-textarea").on("input propertychange", () => { $("textarea#compose-textarea").on("input propertychange", () => {
compose_validate.warn_if_topic_resolved(false); compose_validate.warn_if_topic_resolved(false);
const compose_text_length = compose_validate.check_overflow_text($("#send_message_form")); const compose_text_length = compose_validate.check_overflow_text($("#send_message_form"));

View File

@@ -275,6 +275,12 @@ export function initialize(): void {
target: ".disabled-message-send-controls", target: ".disabled-message-send-controls",
// 350px at 14px/1em // 350px at 14px/1em
maxWidth: "25em", maxWidth: "25em",
onTrigger(instance) {
instance.setContent(
compose_recipient.get_posting_policy_error_message() ||
compose_validate.get_disabled_send_tooltip(),
);
},
content: () => content: () =>
compose_recipient.get_posting_policy_error_message() || compose_recipient.get_posting_policy_error_message() ||
compose_validate.get_disabled_send_tooltip(), compose_validate.get_disabled_send_tooltip(),

View File

@@ -33,6 +33,10 @@ import * as util from "./util.ts";
let user_acknowledged_stream_wildcard = false; let user_acknowledged_stream_wildcard = false;
let upload_in_progress = false; let upload_in_progress = false;
let no_channel_selected = false;
let missing_topic = false;
let no_private_recipient = true;
let no_message_content = false;
let message_too_long = false; let message_too_long = false;
let recipient_disallowed = false; let recipient_disallowed = false;
@@ -63,9 +67,24 @@ export function set_upload_in_progress(status: boolean): void {
update_send_button_status(); update_send_button_status();
} }
function set_no_channel_selected(status: boolean): void {
no_channel_selected = status;
}
function set_missing_topic(status: boolean): void {
missing_topic = status;
}
function set_missing_direct_message_recipient(status: boolean): void {
no_private_recipient = status;
}
function set_no_message_content(status: boolean): void {
no_message_content = status;
}
function set_message_too_long_for_compose(status: boolean): void { function set_message_too_long_for_compose(status: boolean): void {
message_too_long = status; message_too_long = status;
update_send_button_status();
} }
function set_message_too_long_for_edit(status: boolean, $container: JQuery): void { function set_message_too_long_for_edit(status: boolean, $container: JQuery): void {
@@ -81,18 +100,33 @@ function set_message_too_long_for_edit(status: boolean, $container: JQuery): voi
export function set_recipient_disallowed(status: boolean): void { export function set_recipient_disallowed(status: boolean): void {
recipient_disallowed = status; recipient_disallowed = status;
update_send_button_status();
} }
function update_send_button_status(): void { export function update_send_button_status(): void {
const recipient_type = compose_state.get_message_type();
$(".message-send-controls").toggleClass( $(".message-send-controls").toggleClass(
"disabled-message-send-controls", "disabled-message-send-controls",
message_too_long || upload_in_progress || recipient_disallowed, upload_in_progress ||
no_channel_selected ||
(missing_topic && recipient_type === "stream") ||
(no_private_recipient && recipient_type === "private") ||
message_too_long ||
recipient_disallowed ||
no_message_content,
); );
} }
export function get_disabled_send_tooltip(): string { export function get_disabled_send_tooltip(): string {
if (message_too_long) { const recipient_type = compose_state.get_message_type();
if (no_channel_selected && recipient_type === "stream") {
return NO_CHANNEL_SELECTED_ERROR_MESSAGE;
} else if (missing_topic && recipient_type === "stream") {
return TOPICS_REQUIRED_ERROR_MESSAGE;
} else if (no_private_recipient && recipient_type === "private") {
return NO_PRIVATE_RECIPIENT_ERROR_MESSAGE;
} else if (no_message_content) {
return NO_MESSAGE_CONTENT_ERROR_MESSAGE;
} else if (message_too_long) {
return get_message_too_long_for_compose_error(); return get_message_too_long_for_compose_error();
} else if (upload_in_progress) { } else if (upload_in_progress) {
return $t({defaultMessage: "Cannot send message while files are being uploaded."}); return $t({defaultMessage: "Cannot send message while files are being uploaded."});
@@ -649,16 +683,18 @@ export function validate_stream_message_address_info(sub: StreamSubscription): b
return false; return false;
} }
function validate_stream_message(scheduling_message: boolean): boolean { function validate_stream_message(scheduling_message: boolean, show_banner = true): boolean {
const stream_id = compose_state.stream_id();
const $banner_container = $("#compose_banners"); const $banner_container = $("#compose_banners");
const stream_id = compose_state.stream_id();
const no_channel_selected = stream_id === undefined; const no_channel_selected = stream_id === undefined;
set_no_channel_selected(no_channel_selected);
if (no_channel_selected) { if (no_channel_selected) {
compose_banner.show_error_message( report_validation_error(
NO_CHANNEL_SELECTED_ERROR_MESSAGE, NO_CHANNEL_SELECTED_ERROR_MESSAGE,
compose_banner.CLASSNAMES.missing_stream, compose_banner.CLASSNAMES.missing_stream,
$banner_container, $banner_container,
$("#compose_select_recipient_widget_wrapper"), $("#compose_select_recipient_widget_wrapper"),
show_banner,
); );
return false; return false;
} }
@@ -666,12 +702,14 @@ function validate_stream_message(scheduling_message: boolean): boolean {
if (realm.realm_mandatory_topics) { if (realm.realm_mandatory_topics) {
const topic = compose_state.topic(); const topic = compose_state.topic();
const missing_topic = topic === ""; const missing_topic = topic === "";
set_missing_topic(missing_topic);
if (missing_topic) { if (missing_topic) {
compose_banner.show_error_message( report_validation_error(
TOPICS_REQUIRED_ERROR_MESSAGE, TOPICS_REQUIRED_ERROR_MESSAGE,
compose_banner.CLASSNAMES.topic_missing, compose_banner.CLASSNAMES.topic_missing,
$banner_container, $banner_container,
$("input#stream_message_recipient_topic"), $("input#stream_message_recipient_topic"),
show_banner,
); );
return false; return false;
} }
@@ -715,18 +753,20 @@ function validate_stream_message(scheduling_message: boolean): boolean {
// The function checks whether the recipients are users of the realm or cross realm users (bots // The function checks whether the recipients are users of the realm or cross realm users (bots
// for now) // for now)
function validate_private_message(): boolean { function validate_private_message(show_banner = true): boolean {
const user_ids = compose_pm_pill.get_user_ids(); const user_ids = compose_pm_pill.get_user_ids();
const user_ids_string = util.sorted_ids(user_ids).join(","); const user_ids_string = util.sorted_ids(user_ids).join(",");
const $banner_container = $("#compose_banners"); const $banner_container = $("#compose_banners");
const missing_direct_message_recipient = compose_state.private_message_recipient().length === 0; const missing_direct_message_recipient = compose_state.private_message_recipient().length === 0;
set_missing_direct_message_recipient(missing_direct_message_recipient);
if (missing_direct_message_recipient) { if (missing_direct_message_recipient) {
compose_banner.show_error_message( report_validation_error(
NO_PRIVATE_RECIPIENT_ERROR_MESSAGE, NO_PRIVATE_RECIPIENT_ERROR_MESSAGE,
compose_banner.CLASSNAMES.missing_private_message_recipient, compose_banner.CLASSNAMES.missing_private_message_recipient,
$banner_container, $banner_container,
$("#private_message_recipient"), $("#private_message_recipient"),
show_banner,
); );
return false; return false;
} else if (realm.realm_is_zephyr_mirror_realm) { } else if (realm.realm_is_zephyr_mirror_realm) {
@@ -837,7 +877,7 @@ export function check_overflow_text($container: JQuery): number {
return text.length; return text.length;
} }
export function validate_message_length($container: JQuery): boolean { export function validate_message_length($container: JQuery, trigger_flash = true): boolean {
const $textarea = $container.find<HTMLTextAreaElement>(".message-textarea"); const $textarea = $container.find<HTMLTextAreaElement>(".message-textarea");
// Match the behavior of compose_state.message_content of trimming trailing whitespace // Match the behavior of compose_state.message_content of trimming trailing whitespace
const text = $textarea.val()!.trimEnd(); const text = $textarea.val()!.trimEnd();
@@ -848,33 +888,53 @@ export function validate_message_length($container: JQuery): boolean {
set_message_too_long_for_compose(message_too_long_for_compose); set_message_too_long_for_compose(message_too_long_for_compose);
if (message_too_long_for_compose) { if (message_too_long_for_compose) {
if (trigger_flash) {
$textarea.addClass("flash"); $textarea.addClass("flash");
setTimeout(() => $textarea.removeClass("flash"), 1500); setTimeout(() => $textarea.removeClass("flash"), 1500);
}
return false; return false;
} }
return true; return true;
} }
export function validate(scheduling_message: boolean): boolean { function report_validation_error(
message: string,
classname: string,
$container: JQuery,
$bad_input: JQuery,
show_banner: boolean,
precursor?: () => void,
): void {
if (show_banner) {
if (precursor) {
precursor();
}
compose_banner.show_error_message(message, classname, $container, $bad_input);
}
}
export function validate(scheduling_message: boolean, show_banner = true): boolean {
const message_content = compose_state.message_content(); const message_content = compose_state.message_content();
// The validation checks in this function are in a specific priority order. Don't // The validation checks in this function are in a specific priority order. Don't
// change their order unless you want to change which priority they're shown in. // change their order unless you want to change which priority they're shown in.
if ( if (
compose_state.get_message_type() !== "private" && compose_state.get_message_type() !== "private" &&
!validate_stream_message(scheduling_message) !validate_stream_message(scheduling_message, show_banner)
) { ) {
return false; return false;
} }
if (compose_state.get_message_type() === "private" && !validate_private_message()) { if (compose_state.get_message_type() === "private" && !validate_private_message(show_banner)) {
return false; return false;
} }
const no_message_content = /^\s*$/.test(message_content); const no_message_content = /^\s*$/.test(message_content);
set_no_message_content(no_message_content);
if (no_message_content) { if (no_message_content) {
if (show_banner) {
$("textarea#compose-textarea").toggleClass("invalid", true); $("textarea#compose-textarea").toggleClass("invalid", true);
$("textarea#compose-textarea").trigger("focus"); $("textarea#compose-textarea").trigger("focus");
}
return false; return false;
} }
@@ -889,7 +949,9 @@ export function validate(scheduling_message: boolean): boolean {
); );
return false; return false;
} }
if (!validate_message_length($("#send_message_form"))) { // TODO: This doesn't actually show a banner, it triggers a flash
const trigger_flash = show_banner;
if (!validate_message_length($("#send_message_form"), trigger_flash)) {
return false; return false;
} }

View File

@@ -146,7 +146,7 @@ class FakeComposeBox {
assert.ok(this.$content_textarea.hasClass("textarea-over-limit")); assert.ok(this.$content_textarea.hasClass("textarea-over-limit"));
assert.ok($(".message-limit-indicator").hasClass("textarea-over-limit")); assert.ok($(".message-limit-indicator").hasClass("textarea-over-limit"));
assert.ok($(".message-send-controls").hasClass("disabled-message-send-controls")); assert.ok(!$(".message-send-controls").hasClass("disabled-message-send-controls"));
} }
assert_message_size_is_under_the_limit(desired_html) { assert_message_size_is_under_the_limit(desired_html) {

View File

@@ -248,6 +248,7 @@ test("upload_files", async ({mock_template, override, override_rewire}) => {
banner_shown = true; banner_shown = true;
return "<banner-stub>"; return "<banner-stub>";
}); });
override(compose_state, "get_message_type", () => "stream");
await upload.upload_files(uppy, config, files); await upload.upload_files(uppy, config, files);
assert.ok($(".message-send-controls").hasClass("disabled-message-send-controls")); assert.ok($(".message-send-controls").hasClass("disabled-message-send-controls"));
assert.ok(banner_shown); assert.ok(banner_shown);
@@ -455,7 +456,7 @@ test("copy_paste", ({override, override_rewire}) => {
assert.equal(upload_files_called, false); assert.equal(upload_files_called, false);
}); });
test("uppy_events", ({override_rewire, mock_template}) => { test("uppy_events", ({override, override_rewire, mock_template}) => {
$("#compose_banners .upload_banner .moving_bar").css = noop; $("#compose_banners .upload_banner .moving_bar").css = noop;
$("#compose_banners .upload_banner").length = 0; $("#compose_banners .upload_banner").length = 0;
override_rewire(compose_ui, "smart_insert_inline", noop); override_rewire(compose_ui, "smart_insert_inline", noop);
@@ -517,6 +518,7 @@ test("uppy_events", ({override_rewire, mock_template}) => {
override_rewire(compose_ui, "autosize_textarea", () => { override_rewire(compose_ui, "autosize_textarea", () => {
compose_ui_autosize_textarea_called = true; compose_ui_autosize_textarea_called = true;
}); });
override(compose_state, "get_message_type", () => "stream");
on_upload_success_callback(file, response); on_upload_success_callback(file, response);
assert.ok(compose_ui_replace_syntax_called); assert.ok(compose_ui_replace_syntax_called);