You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
667 lines
40 KiB
667 lines
40 KiB
@using System
|
|
@using System.Collections.Generic
|
|
@using System.Globalization
|
|
@using System.Linq
|
|
@using System.Threading.Tasks
|
|
@using DiscordChatExporter.Core.Discord.Data
|
|
@using DiscordChatExporter.Core.Discord.Data.Embeds
|
|
@using DiscordChatExporter.Core.Markdown.Parsing
|
|
@using DiscordChatExporter.Core.Utils.Extensions
|
|
|
|
@inherits RazorBlade.HtmlTemplate
|
|
|
|
@functions {
|
|
public required ExportContext Context { get; init; }
|
|
|
|
public required IReadOnlyList<Message> Messages { get; init; }
|
|
}
|
|
|
|
@{
|
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
|
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
|
|
|
string FormatDate(DateTimeOffset instant) =>
|
|
Context.FormatDate(instant);
|
|
|
|
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
|
Context.Request.ShouldFormatMarkdown
|
|
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)
|
|
: markdown;
|
|
|
|
async ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>
|
|
Context.Request.ShouldFormatMarkdown
|
|
? await HtmlMarkdownVisitor.FormatAsync(Context, markdown, false, CancellationToken)
|
|
: markdown;
|
|
}
|
|
|
|
<div class="chatlog__message-group">
|
|
@foreach (var (message, i) in Messages.WithIndex())
|
|
{
|
|
var isFirst = i == 0;
|
|
|
|
var authorMember = Context.TryGetMember(message.Author.Id);
|
|
var authorColor = Context.TryGetUserColor(message.Author.Id);
|
|
var authorDisplayName = message.Author.IsBot
|
|
? message.Author.DisplayName
|
|
: authorMember?.DisplayName ?? message.Author.DisplayName;
|
|
|
|
<div id="chatlog__message-container-@message.Id" class="chatlog__message-container @(message.IsPinned ? "chatlog__message-container--pinned" : null)" data-message-id="@message.Id">
|
|
<div class="chatlog__message">
|
|
@* System notification *@
|
|
@if (message.Kind.IsSystemNotification())
|
|
{
|
|
<div class="chatlog__message-aside">
|
|
<svg class="chatlog__system-notification-icon">
|
|
@{
|
|
var icon = message.Kind switch {
|
|
MessageKind.RecipientAdd => "join-icon",
|
|
MessageKind.RecipientRemove => "leave-icon",
|
|
MessageKind.Call => "call-icon",
|
|
MessageKind.ChannelNameChange => "pencil-icon",
|
|
MessageKind.ChannelIconChange => "pencil-icon",
|
|
MessageKind.ChannelPinnedMessage => "pin-icon",
|
|
MessageKind.GuildMemberJoin => "join-icon",
|
|
MessageKind.ThreadCreated => "thread-icon",
|
|
_ => "pencil-icon"
|
|
};
|
|
}
|
|
|
|
<use href="#@icon"></use>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="chatlog__message-primary">
|
|
@* Author name *@
|
|
<span class="chatlog__system-notification-author" style="@(authorColor is not null ? $"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@authorDisplayName</span>
|
|
|
|
@* Space out the content *@
|
|
<span> </span>
|
|
|
|
@* System notification content *@
|
|
<span class="chatlog__system-notification-content">
|
|
@if (message.Kind == MessageKind.RecipientAdd && message.MentionedUsers.Any())
|
|
{
|
|
<span>added </span>
|
|
<a class="chatlog__system-notification-link" title="@message.MentionedUsers.First().FullName">@message.MentionedUsers.First().DisplayName</a>
|
|
<span> to the group.</span>
|
|
}
|
|
else if (message.Kind == MessageKind.RecipientRemove && message.MentionedUsers.Any())
|
|
{
|
|
if (message.Author.Id == message.MentionedUsers.First().Id)
|
|
{
|
|
<span>left the group.</span>
|
|
}
|
|
else
|
|
{
|
|
<span>removed </span>
|
|
<a class="chatlog__system-notification-link" title="@message.MentionedUsers.First().FullName">@message.MentionedUsers.First().DisplayName</a>
|
|
<span> from the group.</span>
|
|
}
|
|
}
|
|
else if (message.Kind == MessageKind.Call)
|
|
{
|
|
<span>started a call that lasted @(((message.CallEndedTimestamp ?? message.Timestamp) - message.Timestamp).TotalMinutes.ToString("n0", CultureInfo.InvariantCulture)) minutes</span>
|
|
}
|
|
else if (message.Kind == MessageKind.ChannelNameChange)
|
|
{
|
|
<span>changed the channel name: </span>
|
|
<span class="chatlog__system-notification-link">@message.Content</span>
|
|
}
|
|
else if (message.Kind == MessageKind.ChannelIconChange)
|
|
{
|
|
<span>changed the channel icon.</span>
|
|
}
|
|
else if (message.Kind == MessageKind.ChannelPinnedMessage && message.Reference is not null)
|
|
{
|
|
<span>pinned </span>
|
|
<a class="chatlog__system-notification-link" href="#chatlog__message-container-@message.Reference.MessageId">a message</a>
|
|
<span> to this channel.</span>
|
|
}
|
|
else if (message.Kind == MessageKind.ThreadCreated)
|
|
{
|
|
<span>started a thread.</span>
|
|
}
|
|
else if (message.Kind == MessageKind.GuildMemberJoin)
|
|
{
|
|
<span>joined the server.</span>
|
|
}
|
|
else
|
|
{
|
|
<span>@message.Content.ToLowerInvariant()</span>
|
|
}
|
|
</span>
|
|
|
|
@* Timestamp *@
|
|
<span class="chatlog__system-notification-timestamp">
|
|
<a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a>
|
|
</span>
|
|
</div>
|
|
}
|
|
// Regular message
|
|
else
|
|
{
|
|
<div class="chatlog__message-aside">
|
|
@if (isFirst)
|
|
{
|
|
// Reply symbol
|
|
if (message.IsReplyLike)
|
|
{
|
|
<div class="chatlog__reply-symbol"></div>
|
|
}
|
|
|
|
// Avatar
|
|
<img class="chatlog__avatar" src="@await ResolveAssetUrlAsync(authorMember?.AvatarUrl ?? message.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__short-timestamp" title="@FormatDate(message.Timestamp)">@message.Timestamp.ToLocalString("t")</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="chatlog__message-primary">
|
|
@if (isFirst)
|
|
{
|
|
// Message referenced by the reply
|
|
if (message.IsReplyLike)
|
|
{
|
|
<div class="chatlog__reply">
|
|
@if (message.ReferencedMessage is not null)
|
|
{
|
|
var referencedUserMember = Context.TryGetMember(message.ReferencedMessage.Author.Id);
|
|
var referencedUserColor = Context.TryGetUserColor(message.ReferencedMessage.Author.Id);
|
|
var referencedUserDisplayName = message.ReferencedMessage.Author.IsBot
|
|
? 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">
|
|
<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">
|
|
<span class="chatlog__reply-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
|
|
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden())
|
|
{
|
|
<!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(message.ReferencedMessage.Content))<!--/wmm:ignore-->
|
|
}
|
|
else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())
|
|
{
|
|
<em>Click to see attachment</em>
|
|
<span>🖼️</span>
|
|
}
|
|
else
|
|
{
|
|
<em>Click to see original message</em>
|
|
}
|
|
</span>
|
|
|
|
@if (message.ReferencedMessage.EditedTimestamp is not null)
|
|
{
|
|
<span class="chatlog__reply-edited-timestamp" title="@FormatDate(message.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
|
|
}
|
|
</div>
|
|
}
|
|
else if (message.Interaction is not null)
|
|
{
|
|
var interactionUserMember = Context.TryGetMember(message.Interaction.User.Id);
|
|
var interactionUserColor = Context.TryGetUserColor(message.Interaction.User.Id);
|
|
var interactionUserDisplayName = message.Interaction.User.IsBot
|
|
? 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">
|
|
<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">
|
|
used /@message.Interaction.Name
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__reply-unknown">
|
|
Original message was deleted or could not be loaded.
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// Header
|
|
<div class="chatlog__header">
|
|
@* Author name *@
|
|
<span class="chatlog__author" style="@(authorColor is not null ? $"color: rgb({authorColor.Value.R}, {authorColor.Value.G}, {authorColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@authorDisplayName</span>
|
|
|
|
@* Bot tag *@
|
|
@if (message.Author.IsBot)
|
|
{
|
|
// For cross-posts, the BOT tag is replaced with the SERVER tag
|
|
if (message.Flags.HasFlag(MessageFlags.CrossPost))
|
|
{
|
|
<span class="chatlog__author-tag">SERVER</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="chatlog__author-tag">BOT</span>
|
|
}
|
|
}
|
|
|
|
@* Timestamp *@
|
|
<span class="chatlog__timestamp"><a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a></span>
|
|
</div>
|
|
}
|
|
|
|
@* Content *@
|
|
@if ((!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden()) || message.EditedTimestamp is not null)
|
|
{
|
|
<div class="chatlog__content chatlog__markdown">
|
|
@* Text *@
|
|
@if (!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden())
|
|
{
|
|
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatMarkdownAsync(message.Content))<!--/wmm:ignore--></span>
|
|
}
|
|
|
|
@* Edited timestamp *@
|
|
@if (message.EditedTimestamp is not null)
|
|
{
|
|
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Attachments *@
|
|
@foreach (var attachment in message.Attachments)
|
|
{
|
|
<div class="chatlog__attachment @(attachment.IsSpoiler ? "chatlog__attachment--hidden" : null)" onclick="@(attachment.IsSpoiler ? "showSpoiler(event, this)" : null)">
|
|
@* Spoiler caption *@
|
|
@if (attachment.IsSpoiler)
|
|
{
|
|
<div class="chatlog__attachment-spoiler-caption">SPOILER</div>
|
|
}
|
|
|
|
@* Attachment preview *@
|
|
@if (attachment.IsImage)
|
|
{
|
|
<a href="@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>
|
|
}
|
|
else if (attachment.IsVideo)
|
|
{
|
|
<video class="chatlog__attachment-media" controls>
|
|
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)">
|
|
</video>
|
|
}
|
|
else if (attachment.IsAudio)
|
|
{
|
|
<audio class="chatlog__attachment-media" controls>
|
|
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)">
|
|
</audio>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__attachment-generic">
|
|
<svg class="chatlog__attachment-generic-icon">
|
|
<use href="#attachment-icon"/>
|
|
</svg>
|
|
<div class="chatlog__attachment-generic-name">
|
|
<a href="@await ResolveAssetUrlAsync(attachment.Url)">
|
|
@attachment.FileName
|
|
</a>
|
|
</div>
|
|
<div class="chatlog__attachment-generic-size">
|
|
@attachment.FileSize
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Invites *@
|
|
@{
|
|
var inviteCodes = MarkdownParser
|
|
.ExtractLinks(message.Content)
|
|
.Select(l => l.Url)
|
|
.Select(Invite.TryGetCodeFromUrl)
|
|
.WhereNotNull()
|
|
.ToArray();
|
|
|
|
foreach (var inviteCode in inviteCodes)
|
|
{
|
|
var invite = await Context.Discord.TryGetInviteAsync(inviteCode, CancellationToken);
|
|
if (invite is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
<div class="chatlog__embed">
|
|
<div class="chatlog__embed-invite-container">
|
|
<div class="chatlog__embed-invite-title">@(invite.Channel?.Kind.IsDirect() == true ? "Invite to join a group DM" : "Invite to join a server")</div>
|
|
<div class="chatlog__embed-invite">
|
|
<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">
|
|
</div>
|
|
<div class="chatlog__embed-invite-info">
|
|
<div class="chatlog__embed-invite-guild-name">
|
|
<a href="https://discord.gg/@invite.Code">
|
|
@(invite.Guild.Name)
|
|
</a>
|
|
</div>
|
|
<div class="chatlog__embed-invite-channel-name">
|
|
<svg class="chatlog__embed-invite-channel-icon">
|
|
<use href="#channel-icon"></use>
|
|
</svg>
|
|
<span> @(invite.Channel?.Name ?? "Unknown Channel")</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
@* Embeds *@
|
|
@foreach (var embed in message.Embeds)
|
|
{
|
|
// Spotify embed
|
|
if (embed.TryGetSpotifyTrack() is { } spotifyTrackEmbed)
|
|
{
|
|
<div class="chatlog__embed">
|
|
<div class="chatlog__embed-spotify-container">
|
|
<iframe class="chatlog__embed-spotify" src="@spotifyTrackEmbed.Url" width="400" height="80" allowtransparency="true" allow="encrypted-media"></iframe>
|
|
</div>
|
|
</div>
|
|
}
|
|
// YouTube embed
|
|
else if (embed.TryGetYouTubeVideo() is { } youTubeVideoEmbed)
|
|
{
|
|
<div class="chatlog__embed">
|
|
@* Color pill *@
|
|
@if (embed.Color is not null)
|
|
{
|
|
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)"></div>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__embed-color-pill chatlog__embed-color-pill--default"></div>
|
|
}
|
|
|
|
<div class="chatlog__embed-content-container">
|
|
<div class="chatlog__embed-content">
|
|
<div class="chatlog__embed-text">
|
|
@* Embed author *@
|
|
@if (embed.Author is not null)
|
|
{
|
|
<div class="chatlog__embed-author-container">
|
|
@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'">
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(embed.Author.Url))
|
|
{
|
|
<a class="chatlog__embed-author-link" href="@embed.Author.Url">
|
|
<div class="chatlog__embed-author">@embed.Author.Name</div>
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__embed-author">@embed.Author.Name</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Embed title *@
|
|
@if (!string.IsNullOrWhiteSpace(embed.Title))
|
|
{
|
|
<div class="chatlog__embed-title">
|
|
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
|
{
|
|
<a class="chatlog__embed-title-link" href="@embed.Url">
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Video player *@
|
|
<div class="chatlog__embed-youtube-container">
|
|
<iframe class="chatlog__embed-youtube" src="@youTubeVideoEmbed.Url" width="400" height="225"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
// Generic image embed
|
|
else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url))
|
|
{
|
|
var embedImageUrl =
|
|
embed.Image?.ProxyUrl ?? embed.Image?.Url ??
|
|
embed.Thumbnail?.ProxyUrl ?? embed.Thumbnail?.Url ??
|
|
embed.Url;
|
|
|
|
<div class="chatlog__embed">
|
|
<a href="@await ResolveAssetUrlAsync(embedImageUrl)">
|
|
<img class="chatlog__embed-generic-image" src="@await ResolveAssetUrlAsync(embedImageUrl)" alt="Embedded image" loading="lazy">
|
|
</a>
|
|
</div>
|
|
}
|
|
// Generic video embed
|
|
else if (embed.Kind == EmbedKind.Video && !string.IsNullOrWhiteSpace(embed.Url))
|
|
{
|
|
var embedVideoUrl =
|
|
embed.Video?.ProxyUrl ?? embed.Video?.Url ??
|
|
embed.Url;
|
|
|
|
<div class="chatlog__embed">
|
|
<video class="chatlog__embed-generic-video" width="@embed.Video?.Width" height="@embed.Video?.Height" controls>
|
|
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded video">
|
|
</video>
|
|
</div>
|
|
}
|
|
// Generic gifv embed
|
|
else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Url))
|
|
{
|
|
var embedVideoUrl =
|
|
embed.Video?.ProxyUrl ?? embed.Video?.Url ??
|
|
embed.Url;
|
|
|
|
<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()">
|
|
<source src="@await ResolveAssetUrlAsync(embedVideoUrl)" alt="Embedded gifv">
|
|
</video>
|
|
</div>
|
|
}
|
|
// Rich embed
|
|
else
|
|
{
|
|
<div class="chatlog__embed">
|
|
@* Color pill *@
|
|
@if (embed.Color is not null)
|
|
{
|
|
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color.Value.R, @embed.Color.Value.G, @embed.Color.Value.B, @embed.Color.Value.A)"></div>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__embed-color-pill chatlog__embed-color-pill--default"></div>
|
|
}
|
|
|
|
<div class="chatlog__embed-content-container">
|
|
<div class="chatlog__embed-content">
|
|
<div class="chatlog__embed-text">
|
|
@* Embed author *@
|
|
@if (embed.Author is not null)
|
|
{
|
|
<div class="chatlog__embed-author-container">
|
|
@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'">
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(embed.Author.Url))
|
|
{
|
|
<a class="chatlog__embed-author-link" href="@embed.Author.Url">
|
|
<div class="chatlog__embed-author">@embed.Author.Name</div>
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__embed-author">@embed.Author.Name</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Embed title *@
|
|
@if (!string.IsNullOrWhiteSpace(embed.Title))
|
|
{
|
|
<div class="chatlog__embed-title">
|
|
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
|
{
|
|
<a class="chatlog__embed-title-link" href="@embed.Url">
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Embed description *@
|
|
@if (!string.IsNullOrWhiteSpace(embed.Description))
|
|
{
|
|
<div class="chatlog__embed-description">
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(embed.Description))<!--/wmm:ignore--></div>
|
|
</div>
|
|
}
|
|
|
|
@* Embed fields *@
|
|
@if (embed.Fields.Any())
|
|
{
|
|
<div class="chatlog__embed-fields">
|
|
@foreach (var field in embed.Fields)
|
|
{
|
|
<div class="chatlog__embed-field @(field.IsInline ? "chatlog__embed-field--inline" : null)">
|
|
@if (!string.IsNullOrWhiteSpace(field.Name))
|
|
{
|
|
<div class="chatlog__embed-field-name">
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Name))<!--/wmm:ignore--></div>
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(field.Value))
|
|
{
|
|
<div class="chatlog__embed-field-value">
|
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Html.Raw(await FormatEmbedMarkdownAsync(field.Value))<!--/wmm:ignore--></div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@* Embed content *@
|
|
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url))
|
|
{
|
|
<div class="chatlog__embed-thumbnail-container">
|
|
<a class="chatlog__embed-thumbnail-link" href="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)">
|
|
<img class="chatlog__embed-thumbnail" src="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail" loading="lazy">
|
|
</a>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@* Embed images *@
|
|
@if (embed.Images.Any())
|
|
{
|
|
<div class="chatlog__embed-images @(embed.Images.Count == 1 ? "chatlog__embed-images--single" : null)">
|
|
@foreach (var image in embed.Images)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(image.Url))
|
|
{
|
|
<div class="chatlog__embed-image-container">
|
|
<a class="chatlog__embed-image-link" href="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)">
|
|
<img class="chatlog__embed-image" src="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)" alt="Image" loading="lazy">
|
|
</a>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Embed footer & icon *@
|
|
@if (embed.Footer is not null || embed.Timestamp is not null)
|
|
{
|
|
<div class="chatlog__embed-footer">
|
|
@* Footer icon *@
|
|
@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">
|
|
}
|
|
|
|
<span class="chatlog__embed-footer-text">
|
|
@* Footer text *@
|
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
|
{
|
|
@embed.Footer.Text
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.Text) && embed.Timestamp is not null)
|
|
{
|
|
@(" • ")
|
|
}
|
|
|
|
@* Embed timestamp *@
|
|
@if (embed.Timestamp is not null)
|
|
{
|
|
@FormatDate(embed.Timestamp.Value)
|
|
}
|
|
</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
@* Stickers *@
|
|
@foreach (var sticker in message.Stickers)
|
|
{
|
|
<div class="chatlog__sticker" title="@sticker.Name">
|
|
@if (sticker.Format is StickerFormat.Png or StickerFormat.Apng)
|
|
{
|
|
<img class="chatlog__sticker--media" src="@await ResolveAssetUrlAsync(sticker.SourceUrl)" alt="Sticker">
|
|
}
|
|
else if (sticker.Format == StickerFormat.Lottie)
|
|
{
|
|
<div class="chatlog__sticker--media" data-source="@await ResolveAssetUrlAsync(sticker.SourceUrl)"></div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Message reactions *@
|
|
@if (message.Reactions.Any())
|
|
{
|
|
<div class="chatlog__reactions">
|
|
@foreach (var reaction in message.Reactions)
|
|
{
|
|
<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">
|
|
<span class="chatlog__reaction-count">@reaction.Count</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div> |