compose: Fix preview not being updated when compose textarea is updated.

In preview mode, background updates to the compose box don’t refresh
the preview. For example, canceling an ongoing file upload after
activating preview mode still displays "uploading."

This commit extracts the preview rendering logic from the
show_preview_area function into a new function, `render_preview_area`,
and calls it on the compose textarea’s change event to ensure the
preview updates whenever the textarea is modified.

However, this introduces a race condition where the latest request is
not always reflected in the preview. To resolve this, we introduce a
state variable, `preview_render_count`, which is used to ensure only
the latest preview update is applied.

Fixes #33589.
This commit is contained in:
ubaidrmn
2025-02-28 03:24:47 +05:00
committed by Tim Abbott
parent ddd14a3dc1
commit 1a97fecdb8
4 changed files with 28 additions and 3 deletions

View File

@@ -58,13 +58,16 @@ export function show_preview_area() {
$("#compose").addClass("preview_mode"); $("#compose").addClass("preview_mode");
$("#compose .preview_mode_disabled .compose_control_button").attr("tabindex", -1); $("#compose .preview_mode_disabled .compose_control_button").attr("tabindex", -1);
const $compose_textarea = $("textarea#compose-textarea");
const content = $compose_textarea.val();
$("#compose .markdown_preview").hide(); $("#compose .markdown_preview").hide();
$("#compose .undo_markdown_preview").show(); $("#compose .undo_markdown_preview").show();
$("#compose .undo_markdown_preview").trigger("focus"); $("#compose .undo_markdown_preview").trigger("focus");
render_preview_area();
}
export function render_preview_area() {
const $compose_textarea = $("textarea#compose-textarea");
const content = $compose_textarea.val();
const $preview_message_area = $("#compose .preview_message_area"); const $preview_message_area = $("#compose .preview_message_area");
compose_ui.render_and_show_preview( compose_ui.render_and_show_preview(
$("#compose .markdown_preview_spinner"), $("#compose .markdown_preview_spinner"),

View File

@@ -80,6 +80,9 @@ export function initialize() {
}); });
$("textarea#compose-textarea").on("input propertychange", () => { $("textarea#compose-textarea").on("input propertychange", () => {
if ($("#compose").hasClass("preview_mode")) {
compose.render_preview_area();
}
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"));
if (compose_text_length !== 0 && $("textarea#compose-textarea").hasClass("invalid")) { if (compose_text_length !== 0 && $("textarea#compose-textarea").hasClass("invalid")) {

View File

@@ -9,6 +9,7 @@ let message_type: "stream" | "private" | undefined;
let recipient_edited_manually = false; let recipient_edited_manually = false;
let is_content_unedited_restored_draft = false; let is_content_unedited_restored_draft = false;
let last_focused_compose_type_input: HTMLTextAreaElement | undefined; let last_focused_compose_type_input: HTMLTextAreaElement | undefined;
let preview_render_count = 0;
// We use this variable to keep track of whether user has viewed the topic resolved // We use this variable to keep track of whether user has viewed the topic resolved
// banner for the current compose session, for a narrow. This prevents the banner // banner for the current compose session, for a narrow. This prevents the banner
@@ -67,6 +68,14 @@ export function get_recipient_guest_ids_for_dm_warning(): number[] {
return recipient_guest_ids_for_dm_warning; return recipient_guest_ids_for_dm_warning;
} }
export function get_preview_render_count(): number {
return preview_render_count;
}
export function set_preview_render_count(count: number): void {
preview_render_count = count;
}
export function composing(): boolean { export function composing(): boolean {
// This is very similar to get_message_type(), but it returns // This is very similar to get_message_type(), but it returns
// a boolean. // a boolean.

View File

@@ -17,6 +17,7 @@ import type {Typeahead} from "./bootstrap_typeahead.ts";
import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util.ts"; import * as bulleted_numbered_list_util from "./bulleted_numbered_list_util.ts";
import * as channel from "./channel.ts"; import * as channel from "./channel.ts";
import * as common from "./common.ts"; import * as common from "./common.ts";
import * as compose_state from "./compose_state.ts";
import type {TypeaheadSuggestion} from "./composebox_typeahead.ts"; import type {TypeaheadSuggestion} from "./composebox_typeahead.ts";
import {$t, $t_html} from "./i18n.ts"; import {$t, $t_html} from "./i18n.ts";
import * as loading from "./loading.ts"; import * as loading from "./loading.ts";
@@ -1310,6 +1311,9 @@ export function render_and_show_preview(
$preview_content_box: JQuery, $preview_content_box: JQuery,
content: string, content: string,
): void { ): void {
const preview_render_count = compose_state.get_preview_render_count() + 1;
compose_state.set_preview_render_count(preview_render_count);
function show_preview(rendered_content: string, raw_content?: string): void { function show_preview(rendered_content: string, raw_content?: string): void {
// content is passed to check for status messages ("/me ...") // content is passed to check for status messages ("/me ...")
// and will be undefined in case of errors // and will be undefined in case of errors
@@ -1350,6 +1354,12 @@ export function render_and_show_preview(
url: "/json/messages/render", url: "/json/messages/render",
data: {content}, data: {content},
success(response_data) { success(response_data) {
if (preview_render_count !== compose_state.get_preview_render_count()) {
// The compose input has already been updated with new raw Markdown
// since this rendering request was sent off to the server, so
// there's nothing to do.
return;
}
const data = message_render_response_schema.parse(response_data); const data = message_render_response_schema.parse(response_data);
if (markdown.contains_backend_only_syntax(content)) { if (markdown.contains_backend_only_syntax(content)) {
loading.destroy_indicator($preview_spinner); loading.destroy_indicator($preview_spinner);