From a64276c58fddb2b2cc326afb48823bb424f67787 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Wed, 17 Sep 2025 12:32:41 -0500 Subject: [PATCH] media: Replace legacy .message_inline_image class. This introduces two new replacement classes, depending on whether the inner content is an image or a video. --- web/src/click_handlers.ts | 2 +- web/src/lightbox.ts | 7 +++--- web/src/message_list_hover.ts | 10 ++++---- web/src/message_list_tooltips.ts | 6 ++--- web/src/message_list_view.ts | 4 ++-- web/src/message_parser.ts | 2 +- web/src/postprocess_content.ts | 32 +++++++++++++++++++------- web/styles/rendered_markdown.css | 5 ++-- web/tests/filter.test.cjs | 14 +++++++---- web/tests/postprocess_content.test.cjs | 24 +++++++++---------- 10 files changed, 65 insertions(+), 41 deletions(-) diff --git a/web/src/click_handlers.ts b/web/src/click_handlers.ts index 5aadd3ece2..8c5b99e169 100644 --- a/web/src/click_handlers.ts +++ b/web/src/click_handlers.ts @@ -117,7 +117,7 @@ export function initialize(): void { // Inline image, video and twitter previews. if ( - $target.is("img.message_inline_image") || + $target.is(".media-image-element") || $target.is(".message_inline_animated_image_still") || $target.is("video") || $target.is(".message_inline_video") || diff --git a/web/src/lightbox.ts b/web/src/lightbox.ts index 9c2750801d..c7fd78ab62 100644 --- a/web/src/lightbox.ts +++ b/web/src/lightbox.ts @@ -241,7 +241,7 @@ export function canonical_url_of_media(media: HTMLMediaElement | HTMLImageElemen export function render_lightbox_media_list(): void { if (!is_open) { const message_media_list = $( - ".focused-message-list .message_inline_image img, .focused-message-list .message_inline_video video", + ".focused-message-list .message-media-preview-image img, .focused-message-list .message_inline_video video", ).toArray(); const $lightbox_media_list = $("#lightbox_overlay .image-list").empty(); for (const media of message_media_list) { @@ -413,7 +413,8 @@ export function show_from_selected_message(): void { const $message_selected = $(".selected_message"); let $message = $message_selected; // This is a function to satisfy eslint unicorn/no-array-callback-reference - const media_classes = (): string => ".message_inline_image img, .message_inline_image video"; + const media_classes = (): string => + ".message-media-preview-image img, .message-media-preview-video video"; let $media = $message.find(media_classes()); let $prev_traverse = false; @@ -644,7 +645,7 @@ export function initialize(): void { $("#main_div, #compose .preview_content").on( "click", - ".message_inline_image:not(.message_inline_video) a, .message_inline_animated_image_still", + ".message-media-preview-image:not(.message_inline_video) a, .message_inline_animated_image_still", function (e) { // prevent the link from opening in a new page. e.preventDefault(); diff --git a/web/src/message_list_hover.ts b/web/src/message_list_hover.ts index 8faf59ed36..ff0797e57a 100644 --- a/web/src/message_list_hover.ts +++ b/web/src/message_list_hover.ts @@ -120,13 +120,13 @@ export function initialize(): void { $("#main_div").on( "mouseover", - '.message-list div.message_inline_image img[data-animated="true"]', + '.message-list .message-media-preview-image img[data-animated="true"]', function (this: HTMLElement) { if (user_settings.web_animate_image_previews !== "on_hover") { return; } const $img = $(this); - $img.closest(".message_inline_image").removeClass( + $img.closest(".message-media-preview-image").removeClass( "message_inline_animated_image_still", ); $img.attr( @@ -138,13 +138,15 @@ export function initialize(): void { $("#main_div").on( "mouseout", - '.message-list div.message_inline_image img[data-animated="true"]', + '.message-list .message-media-preview-image img[data-animated="true"]', function (this: HTMLElement) { if (user_settings.web_animate_image_previews !== "on_hover") { return; } const $img = $(this); - $img.closest(".message_inline_image").addClass("message_inline_animated_image_still"); + $img.closest(".message-media-preview-image").addClass( + "message_inline_animated_image_still", + ); $img.attr( "src", $img.attr("src")!.replace(/\/[^/]+$/, "/" + thumbnail.preferred_format.name), diff --git a/web/src/message_list_tooltips.ts b/web/src/message_list_tooltips.ts index 6a73dfb97e..ec39a34918 100644 --- a/web/src/message_list_tooltips.ts +++ b/web/src/message_list_tooltips.ts @@ -341,13 +341,13 @@ export function initialize(): void { }, }); - message_list_tooltip(".message_inline_image > a > img", { + message_list_tooltip(".media-image-element", { // Add a short delay so the user can mouseover several inline images without // tooltips showing and hiding rapidly delay: [300, 20], onShow(instance) { - // Some message_inline_images aren't actually images with a title, - // for example youtube videos, so we default to the actual href + // Some message images do not include a title, such as YouTube + // video previews, so we fall back to displaying the href value const title = $(instance.reference).parent().attr("aria-label") ?? $(instance.reference).parent().attr("href"); diff --git a/web/src/message_list_view.ts b/web/src/message_list_view.ts index 14339a0e58..644e921308 100644 --- a/web/src/message_list_view.ts +++ b/web/src/message_list_view.ts @@ -1038,9 +1038,9 @@ export class MessageListView { if (page_params.is_spectator) { // For images that fail to load due to being rate limited or being denied access // by server in general, we tell user to login to be able to view the image. - $message_rows.find(".message_inline_image img").on("error", (e) => { + $message_rows.find(".media-image-element").on("error", (e) => { $(e.target) - .closest(".message_inline_image") + .closest(".message-media-preview-image") .replaceWith($(render_login_to_view_image_button())); }); } diff --git a/web/src/message_parser.ts b/web/src/message_parser.ts index e53a0d552d..1ea6006e01 100644 --- a/web/src/message_parser.ts +++ b/web/src/message_parser.ts @@ -17,7 +17,7 @@ export function message_has_link(message_content: string): boolean { } export function message_has_image(message_content: string): boolean { - return is_element_in_message_content(message_content, ".message_inline_image"); + return is_element_in_message_content(message_content, ".message-media-preview-image"); } export function message_has_attachment(message_content: string): boolean { diff --git a/web/src/postprocess_content.ts b/web/src/postprocess_content.ts index a5cd0e7d7d..8ec243f701 100644 --- a/web/src/postprocess_content.ts +++ b/web/src/postprocess_content.ts @@ -53,12 +53,25 @@ export function postprocess_content(html: string): string { } if (elt.querySelector("img") || elt.querySelector("video")) { + // Rewrite the legacy .message_inline_image class, whose name would add + // confusion when Zulip supports inline images via standard Markdown. + // We further adjust this class below for when the element contains a + // video. + elt.parentElement?.classList.replace( + "message_inline_image", + "message-media-preview-image", + ); // We want a class to refer to media links elt.classList.add("media-anchor-element"); // Add a class to the video, if it exists, including // the .media-image-element class for properly treating // video thumbnails if (elt.querySelector("video")) { + // We use a different class name to distinguish videos from images + elt.parentElement?.classList.replace( + "message-media-preview-image", + "message-media-preview-video", + ); elt .querySelector("video") ?.classList.add("media-video-element", "media-image-element"); @@ -86,7 +99,10 @@ export function postprocess_content(html: string): string { elt.classList.add("message-embed-title-link"); } - if (elt.parentElement?.classList.contains("message_inline_image")) { + if ( + elt.parentElement?.classList.contains("message-media-preview-image") || + elt.parentElement?.classList.contains("message-media-preview-video") + ) { // For inline images we want to handle the tooltips explicitly, and disable // the browser's built in handling of the title attribute. const title = elt.getAttribute("title"); @@ -210,7 +226,7 @@ export function postprocess_content(html: string): string { } for (const inline_img of template.content.querySelectorAll( - "div.message_inline_image > a > img", + ".message-media-preview-image img", )) { inline_img.setAttribute("loading", "lazy"); // We can't just check whether `inline_image.src` starts with @@ -224,7 +240,7 @@ export function postprocess_content(html: string): string { // If the image source URL can't be parsed, likely due to // some historical bug in the Markdown processor, just // drop the invalid image element. - inline_img.closest("div.message_inline_image")!.remove(); + inline_img.closest(".message-media-preview-image")!.remove(); continue; } @@ -246,7 +262,7 @@ export function postprocess_content(html: string): string { // If we're showing a still thumbnail, show a play // button so that users that it can be played. inline_img - .closest(".message_inline_image")! + .closest(".message-media-preview-image")! .classList.add("message_inline_animated_image_still"); } } @@ -255,10 +271,10 @@ export function postprocess_content(html: string): string { } // After all other processing on images has been done, we look for - // adjacent images and tuck them structurally into galleries. - // This will also process uploaded video thumbnails, which likewise - // take the `.message_inline_image` class - for (const elt of template.content.querySelectorAll(".message_inline_image")) { + // adjacent images and videos, and tuck them structurally into galleries. + for (const elt of template.content.querySelectorAll( + ".message-media-preview-image, .message-media-preview-video", + )) { let gallery_element; const is_part_of_open_gallery = elt.previousElementSibling?.classList.contains( diff --git a/web/styles/rendered_markdown.css b/web/styles/rendered_markdown.css index d60ce4ab95..575929264a 100644 --- a/web/styles/rendered_markdown.css +++ b/web/styles/rendered_markdown.css @@ -505,7 +505,8 @@ } .twitter-image, - .message_inline_image { + .message-media-preview-image, + .message-media-preview-video { /* Set a background for the image; the background will be visible behind the width of the transparent border. */ border: solid 3px transparent; @@ -595,7 +596,7 @@ border: none !important; } - .message_inline_image .media-image-element { + .media-image-element { cursor: zoom-in; } diff --git a/web/tests/filter.test.cjs b/web/tests/filter.test.cjs index 91ce9b4352..3d435075da 100644 --- a/web/tests/filter.test.cjs +++ b/web/tests/filter.test.cjs @@ -1330,7 +1330,7 @@ test("predicate_basics", ({override}) => { const img_msg = { content: - '

test.jpeg

', + '

test.jpeg

', }; const link_msg = { @@ -1403,13 +1403,17 @@ test("predicate_basics", ({override}) => { assert.ok(!has_attachment(no_has_filter_matching_msg)); const has_image = get_predicate([["has", "image"]]); - set_find_results_for_msg_content(img_msg, ".message_inline_image", ["stub"]); + set_find_results_for_msg_content(img_msg, ".message-media-preview-image", ["stub"]); assert.ok(has_image(img_msg)); - set_find_results_for_msg_content(non_img_attachment_msg, ".message_inline_image", false); + set_find_results_for_msg_content(non_img_attachment_msg, ".message-media-preview-image", false); assert.ok(!has_image(non_img_attachment_msg)); - set_find_results_for_msg_content(link_msg, ".message_inline_image", false); + set_find_results_for_msg_content(link_msg, ".message-media-preview-image", false); assert.ok(!has_image(link_msg)); - set_find_results_for_msg_content(no_has_filter_matching_msg, ".message_inline_image", false); + set_find_results_for_msg_content( + no_has_filter_matching_msg, + ".message-media-preview-image", + false, + ); assert.ok(!has_image(no_has_filter_matching_msg)); const has_reaction = get_predicate([["has", "reaction"]]); diff --git a/web/tests/postprocess_content.test.cjs b/web/tests/postprocess_content.test.cjs index 97818adb7e..1f1bcadda6 100644 --- a/web/tests/postprocess_content.test.cjs +++ b/web/tests/postprocess_content.test.cjs @@ -59,12 +59,12 @@ run_test("postprocess_media_and_embeds", () => { "", ), '