feat: preserve original URLs in HTML and JSON exports

Add data-original-url attributes to HTML media elements and originalUrl
fields to JSON output, allowing users to access unmodified Discord CDN URLs
even when assets are downloaded locally. This enables better URL tracking
and archival workflows.

Fixes #1414
This commit is contained in:
Teal Bauer 2025-11-18 18:48:46 +01:00
parent 8fff0f4445
commit f6a4417261
2 changed files with 129 additions and 85 deletions

View file

@ -64,13 +64,11 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
await WriteRolesAsync(Context.GetUserRoles(user.Id), cancellationToken); await WriteRolesAsync(Context.GetUserRoles(user.Id), cancellationToken);
} }
_writer.WriteString( var avatarUrl = Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl;
"avatarUrl", var resolvedAvatarUrl = await Context.ResolveAssetUrlAsync(avatarUrl, cancellationToken);
await Context.ResolveAssetUrlAsync( _writer.WriteString("avatarUrl", resolvedAvatarUrl);
Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl, if (resolvedAvatarUrl != avatarUrl)
cancellationToken _writer.WriteString("originalAvatarUrl", avatarUrl);
)
);
_writer.WriteEndObject(); _writer.WriteEndObject();
await _writer.FlushAsync(cancellationToken); await _writer.FlushAsync(cancellationToken);
@ -87,10 +85,10 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteString("name", emoji.Name); _writer.WriteString("name", emoji.Name);
_writer.WriteString("code", emoji.Code); _writer.WriteString("code", emoji.Code);
_writer.WriteBoolean("isAnimated", emoji.IsAnimated); _writer.WriteBoolean("isAnimated", emoji.IsAnimated);
_writer.WriteString( var resolvedImageUrl = await Context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken);
"imageUrl", _writer.WriteString("imageUrl", resolvedImageUrl);
await Context.ResolveAssetUrlAsync(emoji.ImageUrl, cancellationToken) if (resolvedImageUrl != emoji.ImageUrl)
); _writer.WriteString("originalImageUrl", emoji.ImageUrl);
_writer.WriteEndObject(); _writer.WriteEndObject();
await _writer.FlushAsync(cancellationToken); await _writer.FlushAsync(cancellationToken);
@ -131,13 +129,12 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
if (!string.IsNullOrWhiteSpace(embedAuthor.IconUrl)) if (!string.IsNullOrWhiteSpace(embedAuthor.IconUrl))
{ {
_writer.WriteString( var iconUrl = embedAuthor.IconProxyUrl ?? embedAuthor.IconUrl;
"iconUrl", var originalIconUrl = embedAuthor.IconUrl ?? embedAuthor.IconProxyUrl;
await Context.ResolveAssetUrlAsync( var resolvedIconUrl = await Context.ResolveAssetUrlAsync(iconUrl, cancellationToken);
embedAuthor.IconProxyUrl ?? embedAuthor.IconUrl, _writer.WriteString("iconUrl", resolvedIconUrl);
cancellationToken if (resolvedIconUrl != originalIconUrl)
) _writer.WriteString("originalIconUrl", originalIconUrl);
);
} }
_writer.WriteEndObject(); _writer.WriteEndObject();
@ -153,13 +150,12 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
if (!string.IsNullOrWhiteSpace(embedImage.Url)) if (!string.IsNullOrWhiteSpace(embedImage.Url))
{ {
_writer.WriteString( var imageUrl = embedImage.ProxyUrl ?? embedImage.Url;
"url", var originalUrl = embedImage.Url ?? embedImage.ProxyUrl;
await Context.ResolveAssetUrlAsync( var resolvedUrl = await Context.ResolveAssetUrlAsync(imageUrl, cancellationToken);
embedImage.ProxyUrl ?? embedImage.Url, _writer.WriteString("url", resolvedUrl);
cancellationToken if (resolvedUrl != originalUrl)
) _writer.WriteString("originalUrl", originalUrl);
);
} }
_writer.WriteNumber("width", embedImage.Width); _writer.WriteNumber("width", embedImage.Width);
@ -178,13 +174,12 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
if (!string.IsNullOrWhiteSpace(embedVideo.Url)) if (!string.IsNullOrWhiteSpace(embedVideo.Url))
{ {
_writer.WriteString( var videoUrl = embedVideo.ProxyUrl ?? embedVideo.Url;
"url", var originalUrl = embedVideo.Url ?? embedVideo.ProxyUrl;
await Context.ResolveAssetUrlAsync( var resolvedUrl = await Context.ResolveAssetUrlAsync(videoUrl, cancellationToken);
embedVideo.ProxyUrl ?? embedVideo.Url, _writer.WriteString("url", resolvedUrl);
cancellationToken if (resolvedUrl != originalUrl)
) _writer.WriteString("originalUrl", originalUrl);
);
} }
_writer.WriteNumber("width", embedVideo.Width); _writer.WriteNumber("width", embedVideo.Width);
@ -205,13 +200,12 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
if (!string.IsNullOrWhiteSpace(embedFooter.IconUrl)) if (!string.IsNullOrWhiteSpace(embedFooter.IconUrl))
{ {
_writer.WriteString( var iconUrl = embedFooter.IconProxyUrl ?? embedFooter.IconUrl;
"iconUrl", var originalIconUrl = embedFooter.IconUrl ?? embedFooter.IconProxyUrl;
await Context.ResolveAssetUrlAsync( var resolvedIconUrl = await Context.ResolveAssetUrlAsync(iconUrl, cancellationToken);
embedFooter.IconProxyUrl ?? embedFooter.IconUrl, _writer.WriteString("iconUrl", resolvedIconUrl);
cancellationToken if (resolvedIconUrl != originalIconUrl)
) _writer.WriteString("originalIconUrl", originalIconUrl);
);
} }
_writer.WriteEndObject(); _writer.WriteEndObject();
@ -339,10 +333,11 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteString("id", Context.Request.Guild.Id.ToString()); _writer.WriteString("id", Context.Request.Guild.Id.ToString());
_writer.WriteString("name", Context.Request.Guild.Name); _writer.WriteString("name", Context.Request.Guild.Name);
_writer.WriteString( var guildIconUrl = Context.Request.Guild.IconUrl;
"iconUrl", var resolvedGuildIconUrl = await Context.ResolveAssetUrlAsync(guildIconUrl, cancellationToken);
await Context.ResolveAssetUrlAsync(Context.Request.Guild.IconUrl, cancellationToken) _writer.WriteString("iconUrl", resolvedGuildIconUrl);
); if (resolvedGuildIconUrl != guildIconUrl)
_writer.WriteString("originalIconUrl", guildIconUrl);
_writer.WriteEndObject(); _writer.WriteEndObject();
@ -360,13 +355,11 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
if (!string.IsNullOrWhiteSpace(Context.Request.Channel.IconUrl)) if (!string.IsNullOrWhiteSpace(Context.Request.Channel.IconUrl))
{ {
_writer.WriteString( var channelIconUrl = Context.Request.Channel.IconUrl;
"iconUrl", var resolvedChannelIconUrl = await Context.ResolveAssetUrlAsync(channelIconUrl, cancellationToken);
await Context.ResolveAssetUrlAsync( _writer.WriteString("iconUrl", resolvedChannelIconUrl);
Context.Request.Channel.IconUrl, if (resolvedChannelIconUrl != channelIconUrl)
cancellationToken _writer.WriteString("originalIconUrl", channelIconUrl);
)
);
} }
_writer.WriteEndObject(); _writer.WriteEndObject();
@ -433,10 +426,10 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteStartObject(); _writer.WriteStartObject();
_writer.WriteString("id", attachment.Id.ToString()); _writer.WriteString("id", attachment.Id.ToString());
_writer.WriteString( var resolvedUrl = await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken);
"url", _writer.WriteString("url", resolvedUrl);
await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken) if (resolvedUrl != attachment.Url)
); _writer.WriteString("originalUrl", attachment.Url);
_writer.WriteString("fileName", attachment.FileName); _writer.WriteString("fileName", attachment.FileName);
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes); _writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
@ -463,10 +456,10 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteString("id", sticker.Id.ToString()); _writer.WriteString("id", sticker.Id.ToString());
_writer.WriteString("name", sticker.Name); _writer.WriteString("name", sticker.Name);
_writer.WriteString("format", sticker.Format.ToString()); _writer.WriteString("format", sticker.Format.ToString());
_writer.WriteString( var resolvedSourceUrl = await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken);
"sourceUrl", _writer.WriteString("sourceUrl", resolvedSourceUrl);
await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken) if (resolvedSourceUrl != sticker.SourceUrl)
); _writer.WriteString("originalSourceUrl", sticker.SourceUrl);
_writer.WriteEndObject(); _writer.WriteEndObject();
} }

View file

@ -149,7 +149,11 @@
} }
// Avatar // Avatar
<img class="chatlog__avatar" src="@await ResolveAssetUrlAsync(authorMember?.AvatarUrl ?? message.Author.AvatarUrl)" alt="Avatar" loading="lazy"> {
var avatarUrl = authorMember?.AvatarUrl ?? message.Author.AvatarUrl;
var resolvedAvatarUrl = await ResolveAssetUrlAsync(avatarUrl);
<img class="chatlog__avatar" src="@resolvedAvatarUrl" @(resolvedAvatarUrl != avatarUrl ? $"data-original-url=\"{avatarUrl}\"" : null) alt="Avatar" loading="lazy">
}
} }
else else
{ {
@ -172,7 +176,9 @@
? message.ReferencedMessage.Author.DisplayName ? message.ReferencedMessage.Author.DisplayName
: referencedUserMember?.DisplayName ?? message.ReferencedMessage.Author.DisplayName; : referencedUserMember?.DisplayName ?? message.ReferencedMessage.Author.DisplayName;
<img class="chatlog__reply-avatar" src="@await ResolveAssetUrlAsync(referencedUserMember?.AvatarUrl ?? message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy"> var replyAvatarUrl = referencedUserMember?.AvatarUrl ?? message.ReferencedMessage.Author.AvatarUrl;
var resolvedReplyAvatarUrl = await ResolveAssetUrlAsync(replyAvatarUrl);
<img class="chatlog__reply-avatar" src="@resolvedReplyAvatarUrl" @(resolvedReplyAvatarUrl != replyAvatarUrl ? $"data-original-url=\"{replyAvatarUrl}\"" : null) alt="Avatar" loading="lazy">
<div class="chatlog__reply-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserDisplayName</div> <div class="chatlog__reply-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserDisplayName</div>
<div class="chatlog__reply-content"> <div class="chatlog__reply-content">
<span class="chatlog__reply-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')"> <span class="chatlog__reply-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
@ -205,7 +211,9 @@
? message.Interaction.User.DisplayName ? message.Interaction.User.DisplayName
: interactionUserMember?.DisplayName ?? message.Interaction.User.DisplayName; : interactionUserMember?.DisplayName ?? message.Interaction.User.DisplayName;
<img class="chatlog__reply-avatar" src="@await ResolveAssetUrlAsync(interactionUserMember?.AvatarUrl ?? message.Interaction.User.AvatarUrl)" alt="Avatar" loading="lazy"> var interactionAvatarUrl = interactionUserMember?.AvatarUrl ?? message.Interaction.User.AvatarUrl;
var resolvedInteractionAvatarUrl = await ResolveAssetUrlAsync(interactionAvatarUrl);
<img class="chatlog__reply-avatar" src="@resolvedInteractionAvatarUrl" @(resolvedInteractionAvatarUrl != interactionAvatarUrl ? $"data-original-url=\"{interactionAvatarUrl}\"" : null) alt="Avatar" loading="lazy">
<div class="chatlog__reply-author" style="@(interactionUserColor is not null ? $"color: rgb({interactionUserColor.Value.R}, {interactionUserColor.Value.G}, {interactionUserColor.Value.B})" : null)" title="@message.Interaction.User.FullName">@interactionUserDisplayName</div> <div class="chatlog__reply-author" style="@(interactionUserColor is not null ? $"color: rgb({interactionUserColor.Value.R}, {interactionUserColor.Value.G}, {interactionUserColor.Value.B})" : null)" title="@message.Interaction.User.FullName">@interactionUserDisplayName</div>
<div class="chatlog__reply-content"> <div class="chatlog__reply-content">
used /@message.Interaction.Name used /@message.Interaction.Name
@ -275,20 +283,23 @@
@* Attachment preview *@ @* Attachment preview *@
@if (attachment.IsImage) @if (attachment.IsImage)
{ {
<a href="@await ResolveAssetUrlAsync(attachment.Url)"> var resolvedUrl = await ResolveAssetUrlAsync(attachment.Url);
<img class="chatlog__attachment-media" src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Image attachment")" title="Image: @attachment.FileName (@attachment.FileSize)" loading="lazy"> <a href="@resolvedUrl">
<img class="chatlog__attachment-media" src="@resolvedUrl" @(resolvedUrl != attachment.Url ? $"data-original-url=\"{attachment.Url}\"" : null) alt="@(attachment.Description ?? "Image attachment")" title="Image: @attachment.FileName (@attachment.FileSize)" loading="lazy">
</a> </a>
} }
else if (attachment.IsVideo) else if (attachment.IsVideo)
{ {
<video class="chatlog__attachment-media" controls> var resolvedUrl = await ResolveAssetUrlAsync(attachment.Url);
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)"> <video class="chatlog__attachment-media" controls @(resolvedUrl != attachment.Url ? $"data-original-url=\"{attachment.Url}\"" : null)>
<source src="@resolvedUrl" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)">
</video> </video>
} }
else if (attachment.IsAudio) else if (attachment.IsAudio)
{ {
<audio class="chatlog__attachment-media" controls> var resolvedUrl = await ResolveAssetUrlAsync(attachment.Url);
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)"> <audio class="chatlog__attachment-media" controls @(resolvedUrl != attachment.Url ? $"data-original-url=\"{attachment.Url}\"" : null)>
<source src="@resolvedUrl" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)">
</audio> </audio>
} }
else else
@ -330,9 +341,13 @@
<div class="chatlog__embed"> <div class="chatlog__embed">
<div class="chatlog__embed-invite-container"> <div class="chatlog__embed-invite-container">
<div class="chatlog__embed-invite-title">@(invite.Channel?.IsDirect == true ? "Invite to join a group DM" : "Invite to join a server")</div> <div class="chatlog__embed-invite-title">@(invite.Channel?.IsDirect == true ? "Invite to join a group DM" : "Invite to join a server")</div>
@{
var inviteIconUrl = invite.Channel?.IconUrl ?? invite.Guild.IconUrl;
var resolvedInviteIconUrl = await ResolveAssetUrlAsync(inviteIconUrl);
}
<div class="chatlog__embed-invite"> <div class="chatlog__embed-invite">
<div class="chatlog__embed-invite-guild-icon-container"> <div class="chatlog__embed-invite-guild-icon-container">
<img class="chatlog__embed-invite-guild-icon" src="@await ResolveAssetUrlAsync(invite.Channel?.IconUrl ?? invite.Guild.IconUrl)" alt="Guild icon" loading="lazy"> <img class="chatlog__embed-invite-guild-icon" src="@resolvedInviteIconUrl" @(resolvedInviteIconUrl != inviteIconUrl ? $"data-original-url=\"{inviteIconUrl}\"" : null) alt="Guild icon" loading="lazy">
</div> </div>
<div class="chatlog__embed-invite-info"> <div class="chatlog__embed-invite-info">
<div class="chatlog__embed-invite-guild-name"> <div class="chatlog__embed-invite-guild-name">
@ -388,7 +403,10 @@
<div class="chatlog__embed-author-container"> <div class="chatlog__embed-author-container">
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
{ {
<img class="chatlog__embed-author-icon" src="@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'"> var authorIconUrl = embed.Author.IconProxyUrl ?? embed.Author.IconUrl;
var originalAuthorIconUrl = embed.Author.IconUrl ?? embed.Author.IconProxyUrl;
var resolvedAuthorIconUrl = await ResolveAssetUrlAsync(authorIconUrl);
<img class="chatlog__embed-author-icon" src="@resolvedAuthorIconUrl" @(resolvedAuthorIconUrl != originalAuthorIconUrl ? $"data-original-url=\"{originalAuthorIconUrl}\"" : null) alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
} }
@if (!string.IsNullOrWhiteSpace(embed.Author.Name)) @if (!string.IsNullOrWhiteSpace(embed.Author.Name))
@ -441,9 +459,16 @@
embed.Thumbnail?.ProxyUrl ?? embed.Thumbnail?.Url ?? embed.Thumbnail?.ProxyUrl ?? embed.Thumbnail?.Url ??
embed.Url; embed.Url;
var embedImageOriginalUrl =
embed.Image?.Url ?? embed.Image?.ProxyUrl ??
embed.Thumbnail?.Url ?? embed.Thumbnail?.ProxyUrl ??
embed.Url;
var resolvedEmbedImageUrl = await ResolveAssetUrlAsync(embedImageUrl);
<div class="chatlog__embed"> <div class="chatlog__embed">
<a href="@await ResolveAssetUrlAsync(embedImageUrl)"> <a href="@resolvedEmbedImageUrl">
<img class="chatlog__embed-generic-image" src="@await ResolveAssetUrlAsync(embedImageUrl)" alt="Embedded image" loading="lazy"> <img class="chatlog__embed-generic-image" src="@resolvedEmbedImageUrl" @(resolvedEmbedImageUrl != embedImageOriginalUrl ? $"data-original-url=\"{embedImageOriginalUrl}\"" : null) alt="Embedded image" loading="lazy">
</a> </a>
</div> </div>
} }
@ -457,9 +482,15 @@
embed.Video?.ProxyUrl ?? embed.Video?.Url ?? embed.Video?.ProxyUrl ?? embed.Video?.Url ??
embed.Url; embed.Url;
var embedVideoOriginalUrl =
embed.Video?.Url ?? embed.Video?.ProxyUrl ??
embed.Url;
var resolvedEmbedVideoUrl = await ResolveAssetUrlAsync(embedVideoUrl);
<div class="chatlog__embed"> <div class="chatlog__embed">
<video class="chatlog__embed-generic-video" width="@embed.Video?.Width" height="@embed.Video?.Height" controls> <video class="chatlog__embed-generic-video" width="@embed.Video?.Width" height="@embed.Video?.Height" controls @(resolvedEmbedVideoUrl != embedVideoOriginalUrl ? $"data-original-url=\"{embedVideoOriginalUrl}\"" : null)>
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded video"> <source src="@resolvedEmbedVideoUrl" alt="Embedded video">
</video> </video>
</div> </div>
} }
@ -470,9 +501,15 @@
embed.Video?.ProxyUrl ?? embed.Video?.Url ?? embed.Video?.ProxyUrl ?? embed.Video?.Url ??
embed.Url; embed.Url;
var embedVideoOriginalUrl =
embed.Video?.Url ?? embed.Video?.ProxyUrl ??
embed.Url;
var resolvedEmbedVideoUrl = await ResolveAssetUrlAsync(embedVideoUrl);
<div class="chatlog__embed"> <div class="chatlog__embed">
<video class="chatlog__embed-generic-gifv" width="@embed.Video?.Width" height="@embed.Video?.Height" loop onmouseover="this.play()" onmouseout="this.pause()"> <video class="chatlog__embed-generic-gifv" width="@embed.Video?.Width" height="@embed.Video?.Height" loop onmouseover="this.play()" onmouseout="this.pause()" @(resolvedEmbedVideoUrl != embedVideoOriginalUrl ? $"data-original-url=\"{embedVideoOriginalUrl}\"" : null)>
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded gifv"> <source src="@resolvedEmbedVideoUrl" alt="Embedded gifv">
</video> </video>
</div> </div>
} }
@ -499,7 +536,10 @@
<div class="chatlog__embed-author-container"> <div class="chatlog__embed-author-container">
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
{ {
<img class="chatlog__embed-author-icon" src="@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'"> var authorIconUrl = embed.Author.IconProxyUrl ?? embed.Author.IconUrl;
var originalAuthorIconUrl = embed.Author.IconUrl ?? embed.Author.IconProxyUrl;
var resolvedAuthorIconUrl = await ResolveAssetUrlAsync(authorIconUrl);
<img class="chatlog__embed-author-icon" src="@resolvedAuthorIconUrl" @(resolvedAuthorIconUrl != originalAuthorIconUrl ? $"data-original-url=\"{originalAuthorIconUrl}\"" : null) alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
} }
@if (!string.IsNullOrWhiteSpace(embed.Author.Name)) @if (!string.IsNullOrWhiteSpace(embed.Author.Name))
@ -572,9 +612,12 @@
@* Embed content *@ @* Embed content *@
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url)) @if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url))
{ {
var thumbnailUrl = embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url;
var originalThumbnailUrl = embed.Thumbnail.Url ?? embed.Thumbnail.ProxyUrl;
var resolvedThumbnailUrl = await ResolveAssetUrlAsync(thumbnailUrl);
<div class="chatlog__embed-thumbnail-container"> <div class="chatlog__embed-thumbnail-container">
<a class="chatlog__embed-thumbnail-link" href="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)"> <a class="chatlog__embed-thumbnail-link" href="@resolvedThumbnailUrl">
<img class="chatlog__embed-thumbnail" src="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail" loading="lazy"> <img class="chatlog__embed-thumbnail" src="@resolvedThumbnailUrl" @(resolvedThumbnailUrl != originalThumbnailUrl ? $"data-original-url=\"{originalThumbnailUrl}\"" : null) alt="Thumbnail" loading="lazy">
</a> </a>
</div> </div>
} }
@ -588,9 +631,12 @@
{ {
if (!string.IsNullOrWhiteSpace(image.Url)) if (!string.IsNullOrWhiteSpace(image.Url))
{ {
var imageUrl = image.ProxyUrl ?? image.Url;
var originalImageUrl = image.Url ?? image.ProxyUrl;
var resolvedImageUrl = await ResolveAssetUrlAsync(imageUrl);
<div class="chatlog__embed-image-container"> <div class="chatlog__embed-image-container">
<a class="chatlog__embed-image-link" href="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)"> <a class="chatlog__embed-image-link" href="@resolvedImageUrl">
<img class="chatlog__embed-image" src="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)" alt="Image" loading="lazy"> <img class="chatlog__embed-image" src="@resolvedImageUrl" @(resolvedImageUrl != originalImageUrl ? $"data-original-url=\"{originalImageUrl}\"" : null) alt="Image" loading="lazy">
</a> </a>
</div> </div>
} }
@ -605,7 +651,10 @@
@* Footer icon *@ @* Footer icon *@
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl)) @if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
{ {
<img class="chatlog__embed-footer-icon" src="@await ResolveAssetUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)" alt="Footer icon" loading="lazy"> var footerIconUrl = embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl;
var originalFooterIconUrl = embed.Footer.IconUrl ?? embed.Footer.IconProxyUrl;
var resolvedFooterIconUrl = await ResolveAssetUrlAsync(footerIconUrl);
<img class="chatlog__embed-footer-icon" src="@resolvedFooterIconUrl" @(resolvedFooterIconUrl != originalFooterIconUrl ? $"data-original-url=\"{originalFooterIconUrl}\"" : null) alt="Footer icon" loading="lazy">
} }
<span class="chatlog__embed-footer-text"> <span class="chatlog__embed-footer-text">
@ -636,14 +685,15 @@
@* Stickers *@ @* Stickers *@
@foreach (var sticker in message.Stickers) @foreach (var sticker in message.Stickers)
{ {
var resolvedStickerUrl = await ResolveAssetUrlAsync(sticker.SourceUrl);
<div class="chatlog__sticker" title="@sticker.Name"> <div class="chatlog__sticker" title="@sticker.Name">
@if (sticker.IsImage) @if (sticker.IsImage)
{ {
<img class="chatlog__sticker--media" src="@await ResolveAssetUrlAsync(sticker.SourceUrl)" alt="Sticker"> <img class="chatlog__sticker--media" src="@resolvedStickerUrl" @(resolvedStickerUrl != sticker.SourceUrl ? $"data-original-url=\"{sticker.SourceUrl}\"" : null) alt="Sticker">
} }
else if (sticker.Format == StickerFormat.Lottie) else if (sticker.Format == StickerFormat.Lottie)
{ {
<div class="chatlog__sticker--media" data-source="@await ResolveAssetUrlAsync(sticker.SourceUrl)"></div> <div class="chatlog__sticker--media" data-source="@resolvedStickerUrl" @(resolvedStickerUrl != sticker.SourceUrl ? $"data-original-url=\"{sticker.SourceUrl}\"" : null)></div>
} }
</div> </div>
} }
@ -654,8 +704,9 @@
<div class="chatlog__reactions"> <div class="chatlog__reactions">
@foreach (var reaction in message.Reactions) @foreach (var reaction in message.Reactions)
{ {
var resolvedEmojiUrl = await ResolveAssetUrlAsync(reaction.Emoji.ImageUrl);
<div class="chatlog__reaction" title="@reaction.Emoji.Code"> <div class="chatlog__reaction" title="@reaction.Emoji.Code">
<img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveAssetUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy"> <img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@resolvedEmojiUrl" @(resolvedEmojiUrl != reaction.Emoji.ImageUrl ? $"data-original-url=\"{reaction.Emoji.ImageUrl}\"" : null) loading="lazy">
<span class="chatlog__reaction-count">@reaction.Count</span> <span class="chatlog__reaction-count">@reaction.Count</span>
</div> </div>
} }