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.
This commit is contained in:
Karl Stolley
2025-09-17 12:32:41 -05:00
committed by Tim Abbott
parent 1b3ea708be
commit a64276c58f
10 changed files with 65 additions and 41 deletions

View File

@@ -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") ||

View File

@@ -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 = $<HTMLMediaElement | HTMLImageElement>(
".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<HTMLMediaElement | HTMLImageElement>(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();

View File

@@ -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),

View File

@@ -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");

View File

@@ -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()));
});
}

View File

@@ -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 {

View File

@@ -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<HTMLImageElement>(
"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(

View File

@@ -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;
}

View File

@@ -1330,7 +1330,7 @@ test("predicate_basics", ({override}) => {
const img_msg = {
content:
'<p><a href="/user_uploads/randompath/test.jpeg">test.jpeg</a></p><div class="message_inline_image"><a href="/user_uploads/randompath/test.jpeg" title="test.jpeg"><img src="/user_uploads/randompath/test.jpeg"></a></div>',
'<p><a href="/user_uploads/randompath/test.jpeg">test.jpeg</a></p><div class="message-media-preview-image"><a href="/user_uploads/randompath/test.jpeg" title="test.jpeg"><img src="/user_uploads/randompath/test.jpeg"></a></div>',
};
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"]]);

View File

@@ -59,12 +59,12 @@ run_test("postprocess_media_and_embeds", () => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image message_inline_video">' +
'<div class="message-media-preview-video message_inline_video">' +
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.mp4" target="_blank" rel="noopener noreferrer" class="media-anchor-element">' +
'<video src="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.mp4" class="media-video-element media-image-element"></video>' +
"</a>" +
"</div>" +
'<div class="youtube-video message_inline_image">' +
'<div class="youtube-video message-media-preview-image">' +
'<a class="media-anchor-element" href="https://www.youtube.com/watch?v=tyKJueEk0XM" target="_blank" rel="noopener noreferrer">' +
'<img src="https://i.ytimg.com/vi/tyKJueEk0XM/mqdefault.jpg" class="media-image-element" loading="lazy">' +
"</a>" +
@@ -122,12 +122,12 @@ run_test("inline_image_galleries", ({override}) => {
),
"<p>Message text</p>" +
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="1000x2000" src="/user_uploads/thumbnail/path/to/image.png/840x560.webp" class="media-image-element portrait-thumbnail" width="1000" height="2000" style="width: 5em;" loading="lazy">' +
"</a>" +
"</div>" +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="2000x1000" src="/user_uploads/thumbnail/path/to/image.png/840x560.webp" class="media-image-element landscape-thumbnail" width="2000" height="1000" style="width: 20em;" loading="lazy">' +
"</a>" +
@@ -135,7 +135,7 @@ run_test("inline_image_galleries", ({override}) => {
"</div>" +
"<p>Message text</p>" +
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="1000x1000" src="/user_uploads/thumbnail/path/to/image.png/840x560.webp" class="media-image-element portrait-thumbnail" width="1000" height="1000" style="width: 10em;" loading="lazy">' +
"</a>" +
@@ -196,7 +196,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="3264x2448" src="/user_uploads/thumbnail/path/to/image.png/300x200.webp" class="media-image-element landscape-thumbnail" width="3264" height="2448" style="width: 13.333333333333334em;" loading="lazy">' +
"</a>" +
@@ -214,7 +214,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="100x200" src="/user_uploads/thumbnail/path/to/image.png/300x200.webp" class="media-image-element portrait-thumbnail" width="100" height="200" style="width: 5em;" loading="lazy">' +
"</a>" +
@@ -232,7 +232,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="1x10" src="/user_uploads/thumbnail/path/to/image.png/300x200.webp" class="media-image-element dinky-thumbnail extreme-aspect-ratio portrait-thumbnail" width="1" height="10" style="width: 1px;" loading="lazy">' +
"</a>" +
@@ -251,7 +251,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="3264x2448" src="/user_uploads/thumbnail/path/to/image.png/300x200-anim.webp" data-animated="true" class="media-image-element landscape-thumbnail" width="3264" height="2448" style="width: 13.333333333333334em;" loading="lazy">' +
"</a>" +
@@ -270,7 +270,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image message_inline_animated_image_still">' +
'<div class="message-media-preview-image message_inline_animated_image_still">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="3264x2448" src="/user_uploads/thumbnail/path/to/image.png/300x200.webp" data-animated="true" class="media-image-element landscape-thumbnail" width="3264" height="2448" style="width: 13.333333333333334em;" loading="lazy">' +
"</a>" +
@@ -288,7 +288,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
"</div>",
),
'<div class="message-thumbnail-gallery">' +
'<div class="message_inline_image message_inline_animated_image_still">' +
'<div class="message-media-preview-image message_inline_animated_image_still">' +
'<a href="/user_uploads/path/to/image.png" target="_blank" rel="noopener noreferrer" class="media-anchor-element" aria-label="image.png">' +
'<img data-original-dimensions="3264x2448" src="/user_uploads/thumbnail/path/to/image.png/300x200.webp" data-animated="true" class="media-image-element landscape-thumbnail" width="3264" height="2448" style="width: 13.333333333333334em;" loading="lazy">' +
"</a>" +
@@ -301,7 +301,7 @@ run_test("message_inline_animated_image_still", ({override}) => {
// history.
assert.equal(
postprocess_content(
'<div class="message_inline_image">' +
'<div class="message-media-preview-image">' +
'<a href="https://zulip.%20[Click%20to%20join%20video%20call](https://meeting.example.com/abcd1234)%20example.com/user_uploads/2/ab/abcd1234/image.png" target="_blank" title="image.png">' +
'<img src="https://zulip.%20[Click%20to%20join%20video%20call](https://meeting.example.com/abcd1234)%20example.com/user_uploads/2/ab/abcd1234/image.png">' +
"</a>" +