mirror of https://github.com/zulip/zulip.git
lightbox: Update caching to work with thumbnails.
This moves the cache key for parse_media_data to being the "canonical" image URL, not the displayed `img src`. We may not even have the `img src`, if the thumbnail has not yet been generated.
This commit is contained in:
parent
60fc9eae2f
commit
14880b2b59
|
@ -23,8 +23,10 @@ type Payload = {
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_open = false;
|
let is_open = false;
|
||||||
// the asset map is a map of all retrieved images and YouTube videos that are
|
|
||||||
// memoized instead of being looked up multiple times.
|
// The asset map is a map of all retrieved images and YouTube videos that are memoized instead of
|
||||||
|
// being looked up multiple times. It is keyed by the asset's "canonical URL," which is likely the
|
||||||
|
// `src` used in the message feed, but for thumbnailed images is the full-resolution original URL.
|
||||||
const asset_map = new Map<string, Payload>();
|
const asset_map = new Map<string, Payload>();
|
||||||
|
|
||||||
export class PanZoomControl {
|
export class PanZoomControl {
|
||||||
|
@ -200,7 +202,15 @@ export function clear_for_testing(): void {
|
||||||
asset_map.clear();
|
asset_map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render_lightbox_media_list(preview_source: string): void {
|
export function canonical_url_of_media(media: HTMLElement): string {
|
||||||
|
let media_src = media.getAttribute("src");
|
||||||
|
if (!media_src || media_src.startsWith("/user_uploads/thumbnail/")) {
|
||||||
|
media_src = media.parentElement!.getAttribute("href")!;
|
||||||
|
}
|
||||||
|
return media_src;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_lightbox_media_list(displayed_source: string): void {
|
||||||
if (!is_open) {
|
if (!is_open) {
|
||||||
const media_list = $(
|
const media_list = $(
|
||||||
".focused-message-list .message_inline_image img, .focused-message-list .message_inline_video video",
|
".focused-message-list .message_inline_image img, .focused-message-list .message_inline_video video",
|
||||||
|
@ -208,17 +218,22 @@ export function render_lightbox_media_list(preview_source: string): void {
|
||||||
const $media_list = $("#lightbox_overlay .image-list").empty();
|
const $media_list = $("#lightbox_overlay .image-list").empty();
|
||||||
|
|
||||||
for (const media of media_list) {
|
for (const media of media_list) {
|
||||||
const unverified_src = media.getAttribute("src")!;
|
const src = canonical_url_of_media(media);
|
||||||
const src = util.is_valid_url(unverified_src) ? unverified_src : "";
|
const className = displayed_source === src ? "image selected" : "image";
|
||||||
const className = preview_source === src ? "image selected" : "image";
|
|
||||||
const is_video = media.tagName === "VIDEO";
|
const is_video = media.tagName === "VIDEO";
|
||||||
|
|
||||||
|
// We parse the data for each image to show in the list,
|
||||||
|
// while we still have its original DOM element handy, so
|
||||||
|
// that navigating within the list only needs the `src`
|
||||||
|
// attribute used to construct the node object above.
|
||||||
|
const payload = parse_media_data(media);
|
||||||
|
|
||||||
let $node: JQuery;
|
let $node: JQuery;
|
||||||
if (is_video) {
|
if (is_video) {
|
||||||
$node = $("<div>")
|
$node = $("<div>")
|
||||||
.addClass(className)
|
.addClass(className)
|
||||||
.addClass("lightbox_video")
|
.addClass("lightbox_video")
|
||||||
.attr("data-src", src);
|
.attr("data-url", payload.url);
|
||||||
|
|
||||||
const $video = $("<video>");
|
const $video = $("<video>");
|
||||||
$video.attr("src", src);
|
$video.attr("src", src);
|
||||||
|
@ -228,23 +243,19 @@ export function render_lightbox_media_list(preview_source: string): void {
|
||||||
} else {
|
} else {
|
||||||
$node = $("<div>")
|
$node = $("<div>")
|
||||||
.addClass(className)
|
.addClass(className)
|
||||||
.attr("data-src", src)
|
.attr("data-url", payload.url)
|
||||||
.css({backgroundImage: `url(${CSS.escape(src)})`});
|
.css({
|
||||||
|
backgroundImage: `url(${CSS.escape(payload.preview || payload.source)})`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$media_list.append($node);
|
$media_list.append($node);
|
||||||
|
|
||||||
// We parse the data for each image to show in the list,
|
|
||||||
// while we still have its original DOM element handy, so
|
|
||||||
// that navigating within the list only needs the `src`
|
|
||||||
// attribute used to construct the node object above.
|
|
||||||
parse_media_data(media);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function display_image(payload: Payload): void {
|
function display_image(payload: Payload): void {
|
||||||
render_lightbox_media_list(payload.preview);
|
render_lightbox_media_list(payload.source);
|
||||||
|
|
||||||
$(".player-container, .video-player").hide();
|
$(".player-container, .video-player").hide();
|
||||||
$(".image-preview, .media-actions, .media-description, .download, .lightbox-zoom-reset").show();
|
$(".image-preview, .media-actions, .media-description, .download, .lightbox-zoom-reset").show();
|
||||||
|
@ -282,7 +293,7 @@ function display_image(payload: Payload): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function display_video(payload: Payload): void {
|
function display_video(payload: Payload): void {
|
||||||
render_lightbox_media_list(payload.preview);
|
render_lightbox_media_list(payload.source);
|
||||||
|
|
||||||
$(
|
$(
|
||||||
"#lightbox_overlay .image-preview, .media-description, .download, .lightbox-zoom-reset, .video-player",
|
"#lightbox_overlay .image-preview, .media-description, .download, .lightbox-zoom-reset, .video-player",
|
||||||
|
@ -357,29 +368,9 @@ export function build_open_media_function(
|
||||||
}
|
}
|
||||||
|
|
||||||
return function ($media: JQuery): void {
|
return function ($media: JQuery): void {
|
||||||
// if the asset_map already contains the metadata required to display the
|
// This is used both for clicking on media in the messagelist, as well as clicking on images
|
||||||
// asset, just recall that metadata.
|
// in the media list under the lightbox when it is open.
|
||||||
let $preview_src = $media.attr("src")!;
|
const payload = parse_media_data($media[0]!);
|
||||||
let payload = asset_map.get($preview_src);
|
|
||||||
if (payload === undefined) {
|
|
||||||
if ($preview_src.endsWith("&size=full")) {
|
|
||||||
// while fetching an image for canvas, `src` attribute supplies
|
|
||||||
// full-sized image instead of thumbnail, so we have to replace
|
|
||||||
// `size=full` with `size=thumbnail`.
|
|
||||||
//
|
|
||||||
// TODO: This is a hack to work around the fact that for
|
|
||||||
// the lightbox canvas, the `src` is the data-fullsize-src
|
|
||||||
// for the image, not the original thumbnail used to open
|
|
||||||
// the lightbox. A better fix will be to check a
|
|
||||||
// `data-thumbnail-src` attribute that we add to the
|
|
||||||
// canvas elements.
|
|
||||||
$preview_src = $preview_src.slice(0, -"full".length) + "thumbnail";
|
|
||||||
payload = asset_map.get($preview_src);
|
|
||||||
}
|
|
||||||
if (payload === undefined) {
|
|
||||||
payload = parse_media_data($media[0]!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(payload !== undefined);
|
assert(payload !== undefined);
|
||||||
if (payload.type.match("-video")) {
|
if (payload.type.match("-video")) {
|
||||||
|
@ -461,16 +452,16 @@ export function show_from_selected_message(): void {
|
||||||
|
|
||||||
// retrieve the metadata from the DOM and store into the asset_map.
|
// retrieve the metadata from the DOM and store into the asset_map.
|
||||||
export function parse_media_data(media: HTMLElement): Payload {
|
export function parse_media_data(media: HTMLElement): Payload {
|
||||||
const $media = $(media);
|
const canonical_url = canonical_url_of_media(media);
|
||||||
const preview_src = $media.attr("src")!;
|
if (asset_map.has(canonical_url)) {
|
||||||
|
// Use the cached value
|
||||||
if (asset_map.has(preview_src)) {
|
const payload = asset_map.get(canonical_url);
|
||||||
// check if media's data is already present in asset_map.
|
|
||||||
const payload = asset_map.get(preview_src);
|
|
||||||
assert(payload !== undefined);
|
assert(payload !== undefined);
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $media = $(media);
|
||||||
|
|
||||||
// if wrapped in the .youtube-video class, it will be length = 1, and therefore
|
// if wrapped in the .youtube-video class, it will be length = 1, and therefore
|
||||||
// cast to true.
|
// cast to true.
|
||||||
const is_youtube_video = Boolean($media.closest(".youtube-video").length);
|
const is_youtube_video = Boolean($media.closest(".youtube-video").length);
|
||||||
|
@ -485,6 +476,13 @@ export function parse_media_data(media: HTMLElement): Payload {
|
||||||
let type: string;
|
let type: string;
|
||||||
let source;
|
let source;
|
||||||
const url = $parent.attr("href");
|
const url = $parent.attr("href");
|
||||||
|
|
||||||
|
let preview_src = $media.attr("src");
|
||||||
|
const is_loading_placeholder = $media.hasClass("image-loading-placeholder");
|
||||||
|
if (is_loading_placeholder) {
|
||||||
|
preview_src = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (is_inline_video) {
|
if (is_inline_video) {
|
||||||
type = "inline-video";
|
type = "inline-video";
|
||||||
// Render video from original source to reduce load on our own servers.
|
// Render video from original source to reduce load on our own servers.
|
||||||
|
@ -509,10 +507,8 @@ export function parse_media_data(media: HTMLElement): Payload {
|
||||||
type = "image";
|
type = "image";
|
||||||
if ($media.attr("data-src-fullsize")) {
|
if ($media.attr("data-src-fullsize")) {
|
||||||
source = $media.attr("data-src-fullsize");
|
source = $media.attr("data-src-fullsize");
|
||||||
} else if ($media.attr("src")?.startsWith("/user_uploads/thumbnail/")) {
|
|
||||||
source = url;
|
|
||||||
} else {
|
} else {
|
||||||
source = preview_src;
|
source = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sender_full_name;
|
let sender_full_name;
|
||||||
|
@ -532,12 +528,14 @@ export function parse_media_data(media: HTMLElement): Payload {
|
||||||
user: sender_full_name,
|
user: sender_full_name,
|
||||||
title: $parent.attr("aria-label") ?? $parent.attr("href"),
|
title: $parent.attr("aria-label") ?? $parent.attr("href"),
|
||||||
type,
|
type,
|
||||||
preview: util.is_valid_url(preview_src) ? preview_src : "",
|
preview: preview_src && util.is_valid_url(preview_src) ? preview_src : "",
|
||||||
source: source && util.is_valid_url(source) ? source : "",
|
source: source && util.is_valid_url(source) ? source : "",
|
||||||
url: url && util.is_valid_url(url) ? url : "",
|
url: url && util.is_valid_url(url) ? url : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
asset_map.set(preview_src, payload);
|
if (!is_loading_placeholder) {
|
||||||
|
asset_map.set(canonical_url, payload);
|
||||||
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,11 +609,11 @@ export function initialize(): void {
|
||||||
const is_video = $(this).hasClass("lightbox_video");
|
const is_video = $(this).hasClass("lightbox_video");
|
||||||
if (is_video) {
|
if (is_video) {
|
||||||
$original_media_element = $(
|
$original_media_element = $(
|
||||||
`.message_row video[src='${CSS.escape($(this).attr("data-src")!)}']`,
|
`.message_row a[href='${CSS.escape($(this).attr("data-url")!)}'] video`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$original_media_element = $(
|
$original_media_element = $(
|
||||||
`.message_row img[src='${CSS.escape($(this).attr("data-src")!)}']`,
|
`.message_row a[href='${CSS.escape($(this).attr("data-url")!)}'] img`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue