Update HTML layout and styles

This commit is contained in:
Oleksii Holub 2022-04-09 01:57:35 +03:00
parent 65cc3954b6
commit 5b4d88ca26
7 changed files with 517 additions and 494 deletions

View file

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Core.Discord.Data;
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
// Used for grouping contiguous messages in HTML export
internal partial class MessageGroup
{
public User Author { get; }
public DateTimeOffset Timestamp { get; }
public IReadOnlyList<Message> Messages { get; }
public MessageReference? Reference { get; }
public Message? ReferencedMessage {get; }
public MessageGroup(
User author,
DateTimeOffset timestamp,
MessageReference? reference,
Message? referencedMessage,
IReadOnlyList<Message> messages)
{
Author = author;
Timestamp = timestamp;
Reference = reference;
ReferencedMessage = referencedMessage;
Messages = messages;
}
}
internal partial class MessageGroup
{
public static bool CanJoin(Message message1, Message message2) =>
// Must be from the same author
message1.Author.Id == message2.Author.Id &&
// Author's name must not have changed between messages
string.Equals(message1.Author.FullName, message2.Author.FullName, StringComparison.Ordinal) &&
// Duration between messages must be 7 minutes or less
(message2.Timestamp - message1.Timestamp).Duration().TotalMinutes <= 7 &&
// Other message must not be a reply
message2.Reference is null;
public static MessageGroup Join(IReadOnlyList<Message> messages)
{
var first = messages.First();
return new MessageGroup(
first.Author,
first.Timestamp,
first.Reference,
first.ReferencedMessage,
messages
);
}
}

View file

@ -3,11 +3,14 @@
@using System.Threading.Tasks @using System.Threading.Tasks
@using DiscordChatExporter.Core.Discord.Data @using DiscordChatExporter.Core.Discord.Data
@using DiscordChatExporter.Core.Exporting.Writers.Html; @using DiscordChatExporter.Core.Exporting.Writers.Html;
@using DiscordChatExporter.Core.Utils.Extensions
@namespace DiscordChatExporter.Core.Exporting.Writers.Html @namespace DiscordChatExporter.Core.Exporting.Writers.Html
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext> @inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
@{ @{
var firstMessage = Model.Messages.First();
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url); ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date); string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
@ -16,113 +19,128 @@
string FormatEmbedMarkdown(string markdown) => Model.FormatMarkdown(markdown, false); string FormatEmbedMarkdown(string markdown) => Model.FormatMarkdown(markdown, false);
var userMember = Model.ExportContext.TryGetMember(Model.MessageGroup.Author.Id); var userMember = Model.ExportContext.TryGetMember(firstMessage.Author.Id);
var userColor = Model.ExportContext.TryGetUserColor(Model.MessageGroup.Author.Id); var userColor = Model.ExportContext.TryGetUserColor(firstMessage.Author.Id);
var userColorStyle = userColor is not null var userNick = firstMessage.Author.IsBot
? $"color: rgb({userColor?.R},{userColor?.G},{userColor?.B})" ? firstMessage.Author.Name
: userMember?.Nick ?? firstMessage.Author.Name;
var referencedUserMember = firstMessage.ReferencedMessage is not null
? Model.ExportContext.TryGetMember(firstMessage.ReferencedMessage.Author.Id)
: null; : null;
var userNick = Model.MessageGroup.Author.IsBot var referencedUserColor = firstMessage.ReferencedMessage is not null
? Model.MessageGroup.Author.Name ? Model.ExportContext.TryGetUserColor(firstMessage.ReferencedMessage.Author.Id)
: userMember?.Nick ?? Model.MessageGroup.Author.Name;
var referencedUserMember = Model.MessageGroup.ReferencedMessage is not null
? Model.ExportContext.TryGetMember(Model.MessageGroup.ReferencedMessage.Author.Id)
: null; : null;
var referencedUserColor = Model.MessageGroup.ReferencedMessage is not null var referencedUserNick = firstMessage.ReferencedMessage is not null
? Model.ExportContext.TryGetUserColor(Model.MessageGroup.ReferencedMessage.Author.Id) ? firstMessage.ReferencedMessage.Author.IsBot
: null; ? firstMessage.ReferencedMessage.Author.Name
: referencedUserMember?.Nick ?? firstMessage.ReferencedMessage.Author.Name
var referencedUserColorStyle = referencedUserColor is not null
? $"color: rgb({referencedUserColor?.R},{referencedUserColor?.G},{referencedUserColor?.B})"
: null;
var referencedUserNick = Model.MessageGroup.ReferencedMessage is not null
? Model.MessageGroup.ReferencedMessage.Author.IsBot
? Model.MessageGroup.ReferencedMessage.Author.Name
: referencedUserMember?.Nick ?? Model.MessageGroup.ReferencedMessage.Author.Name
: null; : null;
} }
<div class="chatlog__message-group"> <div class="chatlog__message-group">
@{/* Referenced message */} @for (var i = 0; i < Model.Messages.Count; i++)
@if (Model.MessageGroup.Reference is not null) {
{ var isFirst = i == 0;
<div class="chatlog__reference-symbol"></div> var message = Model.Messages[i];
<div class="chatlog__reference">
@if (Model.MessageGroup.ReferencedMessage is not null)
{
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
<span class="chatlog__reference-name" title="@Model.MessageGroup.ReferencedMessage.Author.FullName" style="@referencedUserColorStyle">@referencedUserNick</span>
<div class="chatlog__reference-content">
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@Model.MessageGroup.ReferencedMessage.Id')">
@if (!string.IsNullOrWhiteSpace(Model.MessageGroup.ReferencedMessage.Content))
{
@Raw(FormatEmbedMarkdown(Model.MessageGroup.ReferencedMessage.Content))
}
else if (Model.MessageGroup.ReferencedMessage.Attachments.Any() || Model.MessageGroup.ReferencedMessage.Embeds.Any())
{
<em>Click to see attachment</em> <span>🖼️</span>
}
else
{
<em>Click to see original message</em>
}
</span>
@if (Model.MessageGroup.ReferencedMessage.EditedTimestamp is not null) <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">
<span class="chatlog__reference-edited-timestamp" title="@FormatDate(Model.MessageGroup.ReferencedMessage.EditedTimestamp.Value)">(edited)</span> @{/* Left side */}
} <div class="chatlog__message-aside">
</div> @if (isFirst)
}
else
{
<span class="chatlog__reference-unknown">
Original message was deleted or could not be loaded.
</span>
}
</div>
}
@{/* Avatar */}
<div class="chatlog__author-avatar-container">
<img class="chatlog__author-avatar" src="@await ResolveUrlAsync(Model.MessageGroup.Author.AvatarUrl)" alt="Avatar" loading="lazy">
</div>
<div class="chatlog__messages">
@{/* Author name */}
<span class="chatlog__author-name" title="@Model.MessageGroup.Author.FullName" data-user-id="@Model.MessageGroup.Author.Id" style="@userColorStyle">@userNick</span>
@{/* Bot tag */}
@if (Model.MessageGroup.Author.IsBot)
{
<span class="chatlog__bot-tag">BOT</span>
}
@{/* Message timestamp */}
<span class="chatlog__timestamp">@FormatDate(Model.MessageGroup.Timestamp)</span>
@{/* Messages in a group */}
@foreach (var message in Model.MessageGroup.Messages)
{
<div class="chatlog__message @(message.IsPinned ? "chatlog__message--pinned" : null)" data-message-id="@message.Id" id="message-@message.Id" title="Message sent: @FormatDate(message.Timestamp)">
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
{ {
<div class="chatlog__content"> // Reference symbol
<div class="markdown"> if (message.Reference is not null)
@{/* Message content */} {
<span class="preserve-whitespace">@Raw(FormatMarkdown(message.Content))</span> <div class="chatlog__reference-symbol"></div>
}
@{/* Edit timestamp */} // Avatar
@if (message.EditedTimestamp is not null) <img class="chatlog__avatar" src="@await ResolveUrlAsync(message.Author.AvatarUrl)" alt="Avatar" loading="lazy">
}
else
{
<div class="chatlog__short-timestamp" title="@FormatDate(message.Timestamp)">@message.Timestamp.ToLocalString("t")</div>
}
</div>
@{/* Right side */}
<div class="chatlog__message-primary">
@if (isFirst)
{
// Reference
if (message.Reference is not null)
{
<div class="chatlog__reference">
@if (message.ReferencedMessage is not null)
{ {
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span> <img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
<div class="chatlog__reference-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserNick</div>
<div class="chatlog__reference-content">
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content))
{
@Raw(FormatEmbedMarkdown(message.ReferencedMessage.Content))
}
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__reference-edited-timestamp" title="@FormatDate(message.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
}
</div>
}
else
{
<div class="chatlog__reference-unknown">
Original message was deleted or could not be loaded.
</div>
} }
</div> </div>
}
// Header
<div class="chatlog__header">
@{/* Author name */}
<span class="chatlog__author" style="@(userColor is not null ? $"color: rgb({userColor.Value.R}, {userColor.Value.G}, {userColor.Value.B})" : null)" title="@message.Author.FullName" data-user-id="@message.Author.Id">@userNick</span>
@{/* Bot label */}
@if (message.Author.IsBot)
{
<span class="chatlog__bot-label">BOT</span>
}
@{/* Timestamp */}
<span class="chatlog__timestamp">@FormatDate(message.Timestamp)</span>
</div>
}
@{/* Content */}
@if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null)
{
<div class="chatlog__content chatlog__markdown">
@{/* Text */}
<span class="chatlog__markdown-preserve">@Raw(FormatMarkdown(message.Content))</span>
@{/* Edited timestamp */}
@if (message.EditedTimestamp is not null)
{
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
}
</div> </div>
} }
@ -159,7 +177,7 @@
{ {
<div class="chatlog__attachment-generic"> <div class="chatlog__attachment-generic">
<svg class="chatlog__attachment-generic-icon"> <svg class="chatlog__attachment-generic-icon">
<use href="#icon-attachment" /> <use href="#attachment-icon"/>
</svg> </svg>
<div class="chatlog__attachment-generic-name"> <div class="chatlog__attachment-generic-name">
<a href="@await ResolveUrlAsync(attachment.Url)"> <a href="@await ResolveUrlAsync(attachment.Url)">
@ -202,7 +220,7 @@
@{/* Color pill */} @{/* Color pill */}
@if (embed.Color is not null) @if (embed.Color is not null)
{ {
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color?.R,@embed.Color?.G,@embed.Color?.B,@embed.Color?.A)"></div> <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 else
{ {
@ -223,10 +241,10 @@
@if (!string.IsNullOrWhiteSpace(embed.Author.Name)) @if (!string.IsNullOrWhiteSpace(embed.Author.Name))
{ {
<span class="chatlog__embed-author-name"> <span class="chatlog__embed-author">
@if (!string.IsNullOrWhiteSpace(embed.Author.Url)) @if (!string.IsNullOrWhiteSpace(embed.Author.Url))
{ {
<a class="chatlog__embed-author-name-link" href="@embed.Author.Url">@embed.Author.Name</a> <a class="chatlog__embed-author-link" href="@embed.Author.Url">@embed.Author.Name</a>
} }
else else
{ {
@ -244,12 +262,12 @@
@if (!string.IsNullOrWhiteSpace(embed.Url)) @if (!string.IsNullOrWhiteSpace(embed.Url))
{ {
<a class="chatlog__embed-title-link" href="@embed.Url"> <a class="chatlog__embed-title-link" href="@embed.Url">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
</a> </a>
} }
else else
{ {
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
} }
</div> </div>
} }
@ -270,7 +288,7 @@
@{/* Color pill */} @{/* Color pill */}
@if (embed.Color is not null) @if (embed.Color is not null)
{ {
<div class="chatlog__embed-color-pill" style="background-color: rgba(@embed.Color?.R,@embed.Color?.G,@embed.Color?.B,@embed.Color?.A)"></div> <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 else
{ {
@ -291,10 +309,10 @@
@if (!string.IsNullOrWhiteSpace(embed.Author.Name)) @if (!string.IsNullOrWhiteSpace(embed.Author.Name))
{ {
<span class="chatlog__embed-author-name"> <span class="chatlog__embed-author">
@if (!string.IsNullOrWhiteSpace(embed.Author.Url)) @if (!string.IsNullOrWhiteSpace(embed.Author.Url))
{ {
<a class="chatlog__embed-author-name-link" href="@embed.Author.Url">@embed.Author.Name</a> <a class="chatlog__embed-author-link" href="@embed.Author.Url">@embed.Author.Name</a>
} }
else else
{ {
@ -312,12 +330,12 @@
@if (!string.IsNullOrWhiteSpace(embed.Url)) @if (!string.IsNullOrWhiteSpace(embed.Url))
{ {
<a class="chatlog__embed-title-link" href="@embed.Url"> <a class="chatlog__embed-title-link" href="@embed.Url">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
</a> </a>
} }
else else
{ {
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Title))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Title))</div>
} }
</div> </div>
} }
@ -326,7 +344,7 @@
@if (!string.IsNullOrWhiteSpace(embed.Description)) @if (!string.IsNullOrWhiteSpace(embed.Description))
{ {
<div class="chatlog__embed-description"> <div class="chatlog__embed-description">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(embed.Description))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(embed.Description))</div>
</div> </div>
} }
@ -340,14 +358,14 @@
@if (!string.IsNullOrWhiteSpace(field.Name)) @if (!string.IsNullOrWhiteSpace(field.Name))
{ {
<div class="chatlog__embed-field-name"> <div class="chatlog__embed-field-name">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(field.Name))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(field.Name))</div>
</div> </div>
} }
@if (!string.IsNullOrWhiteSpace(field.Value)) @if (!string.IsNullOrWhiteSpace(field.Value))
{ {
<div class="chatlog__embed-field-value"> <div class="chatlog__embed-field-value">
<div class="markdown preserve-whitespace">@Raw(FormatEmbedMarkdown(field.Value))</div> <div class="chatlog__markdown chatlog__markdown-preserve">@Raw(FormatEmbedMarkdown(field.Value))</div>
</div> </div>
} }
</div> </div>
@ -434,13 +452,14 @@
@foreach (var reaction in message.Reactions) @foreach (var reaction in message.Reactions)
{ {
<div class="chatlog__reaction" title="@reaction.Emoji.Code"> <div class="chatlog__reaction" title="@reaction.Emoji.Code">
<img class="emoji emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy"> <img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
<span class="chatlog__reaction-count">@reaction.Count</span> <span class="chatlog__reaction-count">@reaction.Count</span>
</div> </div>
} }
</div> </div>
} }
</div> </div>
} </div>
</div> </div>
}
</div> </div>

View file

@ -1,4 +1,6 @@
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors; using System.Collections.Generic;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
namespace DiscordChatExporter.Core.Exporting.Writers.Html; namespace DiscordChatExporter.Core.Exporting.Writers.Html;
@ -6,12 +8,12 @@ internal class MessageGroupTemplateContext
{ {
public ExportContext ExportContext { get; } public ExportContext ExportContext { get; }
public MessageGroup MessageGroup { get; } public IReadOnlyList<Message> Messages { get; }
public MessageGroupTemplateContext(ExportContext exportContext, MessageGroup messageGroup) public MessageGroupTemplateContext(ExportContext exportContext, IReadOnlyList<Message> messages)
{ {
ExportContext = exportContext; ExportContext = exportContext;
MessageGroup = messageGroup; Messages = messages;
} }
public string FormatMarkdown(string? markdown, bool isJumboAllowed = true) => public string FormatMarkdown(string? markdown, bool isJumboAllowed = true) =>

View file

@ -32,36 +32,38 @@
@{/* Styling */} @{/* Styling */}
<style> <style>
@@font-face { @@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(300))); src: url(@await ResolveUrlAsync(GetFontUrl(300)));
font-family: Whitney;
font-weight: 300; font-weight: 300;
} }
@@font-face { @@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(400))); src: url(@await ResolveUrlAsync(GetFontUrl(400)));
font-family: Whitney;
font-weight: 400; font-weight: 400;
} }
@@font-face { @@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(500))); src: url(@await ResolveUrlAsync(GetFontUrl(500)));
font-family: Whitney;
font-weight: 500; font-weight: 500;
} }
@@font-face { @@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(600))); src: url(@await ResolveUrlAsync(GetFontUrl(600)));
font-family: Whitney;
font-weight: 600; font-weight: 600;
} }
@@font-face { @@font-face {
font-family: Whitney;
src: url(@await ResolveUrlAsync(GetFontUrl(700))); src: url(@await ResolveUrlAsync(GetFontUrl(700)));
font-family: Whitney;
font-weight: 700; font-weight: 700;
} }
body { body {
margin: 0;
padding: 0;
background-color: @Themed("#36393e", "#ffffff"); background-color: @Themed("#36393e", "#ffffff");
color: @Themed("#dcddde", "#23262a"); color: @Themed("#dcddde", "#23262a");
font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif;
@ -80,114 +82,15 @@
img { img {
object-fit: contain; object-fit: contain;
} image-rendering: high-quality;
image-rendering: -webkit-optimize-contrast;
.markdown {
max-width: 100%;
line-height: 1.3;
overflow-wrap: break-word;
}
.preserve-whitespace {
white-space: pre-wrap;
}
.spoiler-text {
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
border-radius: 3px;
}
.spoiler-text--hidden {
cursor: pointer;
background-color: @Themed("#202225", "#b9bbbe");
color: rgba(0, 0, 0, 0);
}
.spoiler-text--hidden:hover {
background-color: @Themed("rgba(32, 34, 37, 0.8)", "rgba(185, 187, 190, 0.8)");
}
.spoiler-text--hidden::selection {
color: rgba(0, 0, 0, 0);
}
.quote {
display: flex;
margin: 0.05em 0;
}
.quote-border {
margin-right: 0.5em;
border: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 3px;
}
.pre {
background-color: @Themed("#2f3136", "#f9f9f9");
font-family: "Consolas", "Courier New", Courier, monospace;
}
.pre--multiline {
margin-top: 0.25em;
padding: 0.5em;
border: 2px solid @Themed("#282b30", "#f3f3f3");
border-radius: 5px;
color: @Themed("#b9bbbe", "#657b83");
}
@{/* Override Highlight.js styles with a higher specificity selector */}
.pre--multiline.hljs {
background-color: @Themed("#2f3136", "#f9f9f9");
color: @Themed("#b9bbbe", "#657b83");
}
.pre--inline {
padding: 2px;
border-radius: 3px;
font-size: 0.85em;
}
.mention {
border-radius: 3px;
padding: 0 2px;
color: @Themed("#dee0fc", "#505cdc");
background-color: @Themed("rgba(88, 101, 242, .3)", "rgba(88, 101, 242, .15)");
font-weight: 500;
}
.mention:hover {
background-color: #5865f2;
color: #ffffff
}
.timestamp {
border-radius: 3px;
padding: 0 2px;
background-color: @Themed("rgba(255, 255, 255, 0.06)", "rgba(6, 6, 7, 0.08)");
}
.emoji {
width: 1.325em;
height: 1.325em;
margin: 0 0.06em;
vertical-align: -0.4em;
}
.emoji--small {
width: 1em;
height: 1em;
}
.emoji--large {
width: 2.8em;
height: 2.8em;
} }
.preamble { .preamble {
display: grid; display: grid;
margin: 0 0.3em 0.6em 0.3em;
max-width: 100%;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
max-width: 100%;
padding: 0.6rem;
} }
.preamble__guild-icon-container { .preamble__guild-icon-container {
@ -201,59 +104,114 @@
.preamble__entries-container { .preamble__entries-container {
grid-column: 2; grid-column: 2;
margin-left: 0.6em; margin-left: 0.6rem;
} }
.preamble__entry { .preamble__entry {
font-size: 1.4em; margin-bottom: 0.15rem;
color: @Themed("#ffffff", "#2f3136"); color: @Themed("#ffffff", "#2f3136");
font-size: 1.4rem;
} }
.preamble__entry--small { .preamble__entry--small {
font-size: 1em; font-size: 1rem;
} }
.chatlog { .chatlog {
max-width: 100%; margin-top: 0.6rem;
width: 100%;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
border-bottom: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
} }
.chatlog__message-group { .chatlog__message-group {
margin-bottom: 1rem;
}
.chatlog__message-container {
background-color: transparent;
transition: background-color 1s ease;
}
.chatlog__message-container--highlighted {
background-color: @Themed("rgba(114, 137, 218, 0.2)", "rgba(114, 137, 218, 0.2)");
}
.chatlog__message-container--pinned {
background-color: @Themed("rgba(249, 168, 37, 0.05)", "rgba(249, 168, 37, 0.05)");
}
.chatlog__message {
display: grid; display: grid;
margin: 0 0.6em;
padding: 0.9em 0;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
padding: 0.25rem 0;
direction: ltr;
unicode-bidi: bidi-override;
}
.chatlog__message:hover {
background-color: @Themed("#32353b", "#fafafa");
}
.chatlog__message:hover .chatlog__short-timestamp {
display: block;
}
.chatlog__message-aside {
grid-column: 1;
width: 72px;
padding: 0.15rem 0.15rem 0 0.15rem;
text-align: center;
} }
.chatlog__reference-symbol { .chatlog__reference-symbol {
grid-column: 1; height: 12px;
margin: 8px 4px 4px 18px; margin: 4px 4px 4px 36px;
border-left: 2px solid @Themed("#4f545c", "#c7ccd1"); border-left: 2px solid @Themed("#4f545c", "#c7ccd1");
border-top: 2px solid @Themed("#4f545c", "#c7ccd1"); border-top: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 8px 0 0 0; border-radius: 8px 0 0 0;
} }
.chatlog__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.chatlog__short-timestamp {
display: none;
color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
direction: ltr;
unicode-bidi: bidi-override;
}
.chatlog__message-primary {
grid-column: 2;
min-width: 0;
}
.chatlog__reference { .chatlog__reference {
display: flex; display: flex;
grid-column: 2; margin-bottom: 0.15rem;
margin-bottom: 0.25em; align-items: center;
font-size: 0.875em; color: @Themed("#b5b6b8", "#5f5f60");
font-size: 0.875rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
align-items: center;
color: @Themed("#b5b6b8", "#5f5f60");
} }
.chatlog__reference-avatar { .chatlog__reference-avatar {
border-radius: 50%;
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-right: 0.25em; margin-right: 0.25rem;
border-radius: 50%;
} }
.chatlog__reference-name { .chatlog__reference-author {
margin-right: 0.3em; margin-right: 0.3rem;
font-weight: 600; font-weight: 600;
} }
@ -262,13 +220,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.chatlog__reference-content a:hover {
text-decoration: none;
}
.chatlog__reference-link { .chatlog__reference-link {
cursor: pointer; cursor: pointer;
color: @Themed("#b5b6b8", "#5f5f60");
} }
.chatlog__reference-link * { .chatlog__reference-link * {
@ -276,83 +229,75 @@
pointer-events: none; pointer-events: none;
} }
.chatlog__reference-link .hljs {
display: inline;
}
.chatlog__reference-link:hover { .chatlog__reference-link:hover {
color: @Themed("#ffffff", "#2f3136"); color: @Themed("#ffffff", "#2f3136");
} }
.chatlog__reference-link:hover *:not(.spoiler-text) { .chatlog__reference-link:hover *:not(.chatlog__markdown-spoiler) {
color: @Themed("#ffffff", "#2f3136"); color: inherit;
} }
.chatlog__reference-edited-timestamp { .chatlog__reference-edited-timestamp {
margin-left: 0.25em; margin-left: 0.25rem;
font-size: 0.8em; color: @Themed("#a3a6aa", "#5e6772");
unicode-bidi: bidi-override; font-size: 0.75rem;
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d"); font-weight: 500;
}
.chatlog__author-avatar-container {
grid-column: 1;
width: 40px;
height: 40px;
margin-right: 16px;
}
.chatlog__author-avatar {
border-radius: 50%;
width: 40px;
height: 40px;
image-rendering: high-quality;
}
.chatlog__messages {
grid-column: 2;
min-width: 50%;
direction: ltr; direction: ltr;
unicode-bidi: bidi-override;
} }
.chatlog__author-name { .chatlog__header {
margin-bottom: 0.25rem;
}
.chatlog__author {
font-weight: @Themed("500", "600"); font-weight: @Themed("500", "600");
color: @Themed("#ffffff", "#2f3136"); color: @Themed("#ffffff", "#2f3136");
} }
.chatlog__bot-label {
position: relative;
top: -.1rem;
margin-left: 0.3rem;
padding: 0.05rem 0.3rem;
border-radius: 3px;
background-color: #5865F2;
color: #ffffff;
font-size: 0.625rem;
font-weight: 500;
line-height: 1.3;
}
.chatlog__timestamp { .chatlog__timestamp {
margin-left: 0.3em; margin-left: 0.3rem;
font-size: 0.75em; color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
direction: ltr; direction: ltr;
unicode-bidi: bidi-override; unicode-bidi: bidi-override;
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
}
.chatlog__message {
padding: 0.1em 0.3em;
margin: 0 -0.3em;
background-color: transparent;
transition: background-color 1s ease;
}
.chatlog__message--highlighted {
background-color: @Themed("rgba(114, 137, 218, 0.2)", "rgba(114, 137, 218, 0.2)");
}
.chatlog__message--pinned {
background-color: @Themed("rgba(249, 168, 37, 0.05)", "rgba(249, 168, 37, 0.05)");
} }
.chatlog__content { .chatlog__content {
font-size: 0.95em; padding-right: 1rem;
font-size: 0.95rem;
word-wrap: break-word; word-wrap: break-word;
} }
.chatlog__edited-timestamp { .chatlog__edited-timestamp {
margin-left: 0.15em; margin-left: 0.15rem;
font-size: 0.8em; color: @Themed("#a3a6aa", "#5e6772");
font-size: 0.75rem;
font-weight: 500;
} }
.chatlog__attachment { .chatlog__attachment {
display: inline-block;
position: relative; position: relative;
margin-top: 0.3em; width: fit-content;
margin-top: 0.3rem;
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
} }
@ -368,18 +313,18 @@
.chatlog__attachment-spoiler-caption { .chatlog__attachment-spoiler-caption {
display: none; display: none;
z-index: 999;
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); z-index: 999;
padding: 0.4em 0.8em; padding: 0.4rem 0.8rem;
background-color: rgba(0, 0, 0, 0.9);
border-radius: 20px; border-radius: 20px;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.9);
color: #dcddde; color: #dcddde;
font-size: 0.9em; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.05em; letter-spacing: 0.05rem;
} }
.chatlog__attachment--hidden .chatlog__attachment-spoiler-caption { .chatlog__attachment--hidden .chatlog__attachment-spoiler-caption {
@ -391,9 +336,9 @@
} }
.chatlog__attachment-media { .chatlog__attachment-media {
vertical-align: top;
max-width: 45vw; max-width: 45vw;
max-height: 500px; max-height: 500px;
vertical-align: top;
border-radius: 3px; border-radius: 3px;
} }
@ -402,13 +347,13 @@
} }
.chatlog__attachment-generic { .chatlog__attachment-generic {
width: 100%;
max-width: 520px; max-width: 520px;
width: 100%;
height: 40px; height: 40px;
padding: 10px; padding: 10px;
background-color: @Themed("#2f3136", "#f2f3f5");
border: 1px solid @Themed("#292b2f", "#ebedef"); border: 1px solid @Themed("#292b2f", "#ebedef");
border-radius: 3px; border-radius: 3px;
background-color: @Themed("#2f3136", "#f2f3f5");
overflow: hidden; overflow: hidden;
} }
@ -434,19 +379,15 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.chatlog__edited-timestamp {
color: @Themed("rgba(255, 255, 255, 0.2)", "#747f8d");
}
.chatlog__embed { .chatlog__embed {
display: flex; display: flex;
margin-top: 0.3em; margin-top: 0.3rem;
max-width: 520px; max-width: 520px;
} }
.chatlog__embed-color-pill { .chatlog__embed-color-pill {
flex-shrink: 0; flex-shrink: 0;
width: 0.25em; width: 0.25rem;
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
} }
@ -458,11 +399,11 @@
.chatlog__embed-content-container { .chatlog__embed-content-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5em 0.6em; padding: 0.5rem 0.6rem;
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
border: 1px solid @Themed("rgba(46, 48, 54, 0.6)", "rgba(204, 204, 204, 0.3)"); border: 1px solid @Themed("rgba(46, 48, 54, 0.6)", "rgba(204, 204, 204, 0.3)");
border-top-right-radius: 3px; border-top-right-radius: 3px;
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
background-color: @Themed("rgba(46, 48, 54, 0.3)", "rgba(249, 249, 249, 0.3)");
} }
.chatlog__embed-content { .chatlog__embed-content {
@ -476,83 +417,83 @@
.chatlog__embed-author { .chatlog__embed-author {
display: flex; display: flex;
margin-bottom: 0.3em; margin-bottom: 0.3rem;
align-items: center; align-items: center;
} }
.chatlog__embed-author-icon { .chatlog__embed-author-icon {
margin-right: 0.5em;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 0.5rem;
border-radius: 50%; border-radius: 50%;
} }
.chatlog__embed-author-name { .chatlog__embed-author {
font-size: 0.875em; color: @Themed("#ffffff", "#4f545c")
font-size: 0.875rem;
font-weight: 600; font-weight: 600;
direction: ltr; direction: ltr;
unicode-bidi: bidi-override; unicode-bidi: bidi-override;
color: @Themed("#ffffff", "#4f545c")
} }
.chatlog__embed-author-name-link { .chatlog__embed-author-link {
color: @Themed("#ffffff", "#4f545c"); color: @Themed("#ffffff", "#4f545c");
} }
.chatlog__embed-title { .chatlog__embed-title {
margin-bottom: 0.2em; margin-bottom: 0.75rem;
font-size: 0.875em;
font-weight: 600;
color: @Themed("#ffffff", "#4f545c"); color: @Themed("#ffffff", "#4f545c");
font-size: 0.875rem;
font-weight: 600;
} }
.chatlog__embed-description { .chatlog__embed-description {
font-weight: 500;
font-size: 0.85em;
color: @Themed("#dcddde", "#2e3338"); color: @Themed("#dcddde", "#2e3338");
font-weight: 500;
font-size: 0.85rem;
} }
.chatlog__embed-fields { .chatlog__embed-fields {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0 0.5em; gap: 0 0.5rem;
} }
.chatlog__embed-field { .chatlog__embed-field {
flex: 0; flex: 0;
min-width: 100%; min-width: 100%;
max-width: 506px; max-width: 506px;
padding-top: 0.6em; padding-top: 0.6rem;
font-size: 0.875em; font-size: 0.875rem;
} }
.chatlog__embed-field--inline { .chatlog__embed-field--inline {
flex: 1; flex: 1;
flex-basis: auto; flex-basis: auto;
min-width: 150px; min-width: 50px;
} }
.chatlog__embed-field-name { .chatlog__embed-field-name {
margin-bottom: 0.2em; margin-bottom: 0.2rem;
font-weight: 600;
color: @Themed("#ffffff", "#36393e"); color: @Themed("#ffffff", "#36393e");
font-weight: 600;
} }
.chatlog__embed-field-value { .chatlog__embed-field-value {
font-weight: 500;
color: @Themed("#dcddde", "#2e3338"); color: @Themed("#dcddde", "#2e3338");
font-weight: 500;
} }
.chatlog__embed-thumbnail { .chatlog__embed-thumbnail {
flex: 0; flex: 0;
margin-left: 1.2em;
max-width: 80px; max-width: 80px;
max-height: 80px; max-height: 80px;
margin-left: 1.2rem;
border-radius: 3px; border-radius: 3px;
} }
.chatlog__embed-image-container { .chatlog__embed-image-container {
margin-top: 0.6em; margin-top: 0.6rem;
} }
.chatlog__embed-image { .chatlog__embed-image {
@ -562,28 +503,28 @@
} }
.chatlog__embed-footer { .chatlog__embed-footer {
margin-top: 0.6em; margin-top: 0.6rem;
color: @Themed("#dcddde", "#2e3338"); color: @Themed("#dcddde", "#2e3338");
} }
.chatlog__embed-footer-icon { .chatlog__embed-footer-icon {
margin-right: 0.2em;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 0.2rem;
border-radius: 50%; border-radius: 50%;
vertical-align: middle; vertical-align: middle;
} }
.chatlog__embed-footer-text { .chatlog__embed-footer-text {
vertical-align: middle; vertical-align: middle;
font-size: 0.75em; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
} }
.chatlog__embed-plainimage { .chatlog__embed-plainimage {
vertical-align: top;
max-width: 45vw; max-width: 45vw;
max-height: 500px; max-height: 500px;
vertical-align: top;
border-radius: 3px; border-radius: 3px;
} }
@ -592,7 +533,7 @@
} }
.chatlog__embed-youtube-container { .chatlog__embed-youtube-container {
margin-top: 0.6em; margin-top: 0.6rem;
} }
.chatlog__embed-youtube { .chatlog__embed-youtube {
@ -616,51 +557,132 @@
.chatlog__reaction { .chatlog__reaction {
display: flex; display: flex;
align-items: center; margin: 0.35rem 0.1rem 0.1rem 0;
margin: 0.35em 0.1em 0.1em 0; padding: 0.125rem 0.375rem;
padding: .125rem .375rem; border: 1px solid transparent;
background-color: @Themed("#2f3136", "#f2f3f5");
border-radius: 8px; border-radius: 8px;
border-color: transparent; background-color: @Themed("#2f3136", "#f2f3f5");
border-width: 1px; align-items: center;
border-style: solid;
} }
.chatlog__reaction:hover { .chatlog__reaction:hover {
border-color: @Themed("hsla(0,0%,100%,.2)", "rgba(0, 0, 0, 0.2)"); border: 1px solid @Themed("hsla(0,0%,100%,.2)", "rgba(0, 0, 0, 0.2)");
border-width: 1px;
border-style: solid;
background-color: @Themed("transparent", "white"); background-color: @Themed("transparent", "white");
} }
.chatlog__reaction-count { .chatlog__reaction-count {
min-width: 9px; min-width: 9px;
margin-left: 0.35em; margin-left: 0.35rem;
font-size: 0.875em;
color: @Themed("#b9bbbe", "#4f5660"); color: @Themed("#b9bbbe", "#4f5660");
font-size: 0.875rem;
} }
.chatlog__reaction:hover .chatlog__reaction-count { .chatlog__reaction:hover .chatlog__reaction-count {
color: @Themed("#dcddde", "#2e3338"); color: @Themed("#dcddde", "#2e3338");
} }
.chatlog__bot-tag { .chatlog__markdown {
position: relative; max-width: 100%;
top: -.1em;
margin-left: 0.3em;
padding: 0.05em 0.3em;
border-radius: 3px;
line-height: 1.3; line-height: 1.3;
background-color: #5865F2; overflow-wrap: break-word;
color: #ffffff; }
font-size: 0.625em;
.chatlog__markdown-preserve {
white-space: pre-wrap;
}
.chatlog__markdown-spoiler {
background-color: @Themed("rgba(255, 255, 255, 0.1)", "rgba(0, 0, 0, 0.1)");
border-radius: 3px;
}
.chatlog__markdown-spoiler--hidden {
cursor: pointer;
background-color: @Themed("#202225", "#b9bbbe");
color: rgba(0, 0, 0, 0);
}
.chatlog__markdown-spoiler--hidden:hover {
background-color: @Themed("rgba(32, 34, 37, 0.8)", "rgba(185, 187, 190, 0.8)");
}
.chatlog__markdown-spoiler--hidden::selection {
color: rgba(0, 0, 0, 0);
}
.chatlog__markdown-quote {
display: flex;
margin: 0.05rem 0;
}
.chatlog__markdown-quote-border {
margin-right: 0.5rem;
border: 2px solid @Themed("#4f545c", "#c7ccd1");
border-radius: 3px;
}
.chatlog__markdown-pre {
background-color: @Themed("#2f3136", "#f9f9f9");
font-family: "Consolas", "Courier New", Courier, monospace;
}
.chatlog__markdown-pre--multiline {
margin-top: 0.25rem;
padding: 0.5rem;
border: 2px solid @Themed("#282b30", "#f3f3f3");
border-radius: 5px;
color: @Themed("#b9bbbe", "#657b83");
}
.chatlog__markdown-pre--multiline.hljs {
background-color: inherit;
color: inherit;
}
.chatlog__markdown-pre--inline {
padding: 2px;
border-radius: 3px;
font-size: 0.85rem;
}
.chatlog__markdown-mention{
border-radius: 3px;
padding: 0 2px;
background-color: @Themed("rgba(88, 101, 242, .3)", "rgba(88, 101, 242, .15)");
color: @Themed("#dee0fc", "#505cdc");
font-weight: 500; font-weight: 500;
} }
.chatlog__markdown-mention:hover {
background-color: #5865f2;
color: #ffffff
}
.chatlog__markdown-timestamp{
border-radius: 3px;
padding: 0 2px;
color: @Themed("#a3a6aa", "#5e6772");
}
.chatlog__emoji {
width: 1.325rem;
height: 1.325rem;
margin: 0 0.06rem;
vertical-align: -0.4rem;
}
.chatlog__emoji--small {
width: 1rem;
height: 1rem;
}
.chatlog__emoji--large {
width: 2.8rem;
height: 2.8rem;
}
.postamble { .postamble {
margin: 1.4em 0.3em 0.6em 0.3em; padding: 1.25rem;
padding: 1em;
border-top: 1px solid @Themed("rgba(255, 255, 255, 0.1)", "#eceeef");
} }
.postamble__entry { .postamble__entry {
@ -669,16 +691,16 @@
</style> </style>
@{/* Syntax highlighting */} @{/* Syntax highlighting */}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-@(Model.ThemeName.ToLowerInvariant()).min.css"> <link rel="stylesheet" href="@ResolveUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{Model.ThemeName.ToLowerInvariant()}.min.css")">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script> <script src="@ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js")"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.pre--multiline').forEach(e => hljs.highlightBlock(e)); document.querySelectorAll('.chatlog__markdown-pre--multiline').forEach(e => hljs.highlightBlock(e));
}); });
</script> </script>
@{/* Lottie animation support */} @{/* Lottie animation support */}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js"></script> <script src="@ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js")"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => { document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
@ -702,43 +724,46 @@
@{/* Scripts */} @{/* Scripts */}
<script> <script>
function scrollToMessage(event, id) { function scrollToMessage(event, id) {
var element = document.getElementById('message-' + id); var element = document.getElementById('chatlog__message-container-' + id);
if (!element)
return;
if (element) { event.preventDefault();
event.preventDefault(); element.classList.add('chatlog__message-container--highlighted');
element.classList.add('chatlog__message--highlighted'); window.scrollTo({
top: element.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (window.innerHeight / 2),
behavior: 'smooth'
});
window.scrollTo({ window.setTimeout(function() {
top: element.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (window.innerHeight / 2), element.classList.remove('chatlog__message-container--highlighted');
behavior: 'smooth' }, 2000);
});
window.setTimeout(function() {
element.classList.remove('chatlog__message--highlighted');
}, 2000);
}
} }
function showSpoiler(event, element) { function showSpoiler(event, element) {
if (element && element.classList.contains('spoiler-text--hidden')) { if (!element)
event.preventDefault(); return;
element.classList.remove('spoiler-text--hidden');
} if (element.classList.contains('chatlog__attachment--hidden')) {
if (element && element.classList.contains('chatlog__attachment--hidden')) {
event.preventDefault(); event.preventDefault();
element.classList.remove('chatlog__attachment--hidden'); element.classList.remove('chatlog__attachment--hidden');
} }
if (element.classList.contains('chatlog__markdown-spoiler--hidden')) {
event.preventDefault();
element.classList.remove('chatlog__markdown-spoiler--hidden');
}
} }
</script> </script>
@{/* Icons */} @{/* Icons */}
<svg style="display: none"> <svg style="display: none">
<symbol id="icon-attachment" viewBox="0 0 720 960"> <symbol id="attachment-icon" viewBox="0 0 720 960">
<path fill="#f4f5fb" d="M50,935a25,25,0,0,1-25-25V50A25,25,0,0,1,50,25H519.6L695,201.32V910a25,25,0,0,1-25,25Z" /> <path fill="#f4f5fb" d="M50,935a25,25,0,0,1-25-25V50A25,25,0,0,1,50,25H519.6L695,201.32V910a25,25,0,0,1-25,25Z"/>
<path fill="#7789c4" d="M509.21,50,670,211.63V910H50V50H509.21M530,0H50A50,50,0,0,0,0,50V910a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50h0V191Z" /> <path fill="#7789c4" d="M509.21,50,670,211.63V910H50V50H509.21M530,0H50A50,50,0,0,0,0,50V910a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50h0V191Z"/>
<path fill="#f4f5fb" d="M530,215a25,25,0,0,1-25-25V50a25,25,0,0,1,16.23-23.41L693.41,198.77A25,25,0,0,1,670,215Z" /> <path fill="#f4f5fb" d="M530,215a25,25,0,0,1-25-25V50a25,25,0,0,1,16.23-23.41L693.41,198.77A25,25,0,0,1,670,215Z"/>
<path fill="#7789c4" d="M530,70.71,649.29,190H530V70.71M530,0a50,50,0,0,0-50,50V190a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50Z" /> <path fill="#7789c4" d="M530,70.71,649.29,190H530V70.71M530,0a50,50,0,0,0-50,50V190a50,50,0,0,0,50,50H670a50,50,0,0,0,50-50Z"/>
</symbol> </symbol>
</svg> </svg>
</head> </head>
@ -760,22 +785,22 @@
@if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null) @if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null)
{ {
<div class="preamble__entry preamble__entry--small"> <div class="preamble__entry preamble__entry--small">
@if (Model.ExportContext.Request.After is not null && Model.ExportContext.Request.Before is not null) @if (Model.ExportContext.Request.After is not null && Model.ExportContext.Request.Before is not null)
{ {
@($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}") @($"Between {FormatDate(Model.ExportContext.Request.After.Value.ToDate())} and {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
} }
else if (Model.ExportContext.Request.After is not null) else if (Model.ExportContext.Request.After is not null)
{ {
@($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}") @($"After {FormatDate(Model.ExportContext.Request.After.Value.ToDate())}")
} }
else if (Model.ExportContext.Request.Before is not null) else if (Model.ExportContext.Request.Before is not null)
{ {
@($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}") @($"Before {FormatDate(Model.ExportContext.Request.Before.Value.ToDate())}")
} }
</div> </div>
} }
</div> </div>
</div> </div>
@{/* Preamble cuts off at this point */} @{/* Preamble cuts off at this point */}
<div class="chatlog"> <div class="chatlog">

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -13,7 +14,7 @@ internal class HtmlMessageWriter : MessageWriter
private readonly TextWriter _writer; private readonly TextWriter _writer;
private readonly string _themeName; private readonly string _themeName;
private readonly List<Message> _messageGroupBuffer = new(); private readonly List<Message> _messageGroup = new();
public HtmlMessageWriter(Stream stream, ExportContext context, string themeName) public HtmlMessageWriter(Stream stream, ExportContext context, string themeName)
: base(stream, context) : base(stream, context)
@ -22,6 +23,23 @@ internal class HtmlMessageWriter : MessageWriter
_themeName = themeName; _themeName = themeName;
} }
private bool CanJoinGroup(Message message)
{
var lastMessage = _messageGroup.LastOrDefault();
if (lastMessage is null)
return true;
return
// Must be from the same author
lastMessage.Author.Id == message.Author.Id &&
// Author's name must not have changed between messages
string.Equals(lastMessage.Author.FullName, message.Author.FullName, StringComparison.Ordinal) &&
// Duration between messages must be 7 minutes or less
(message.Timestamp - lastMessage.Timestamp).Duration().TotalMinutes <= 7 &&
// Other message must not be a reply
message.Reference is null;
}
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default)
{ {
var templateContext = new PreambleTemplateContext(Context, _themeName); var templateContext = new PreambleTemplateContext(Context, _themeName);
@ -34,10 +52,10 @@ internal class HtmlMessageWriter : MessageWriter
} }
private async ValueTask WriteMessageGroupAsync( private async ValueTask WriteMessageGroupAsync(
MessageGroup messageGroup, IReadOnlyList<Message> messages,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var templateContext = new MessageGroupTemplateContext(Context, messageGroup); var templateContext = new MessageGroupTemplateContext(Context, messages);
// We are not writing directly to output because Razor // We are not writing directly to output because Razor
// does not actually do asynchronous writes to stream. // does not actually do asynchronous writes to stream.
@ -52,31 +70,26 @@ internal class HtmlMessageWriter : MessageWriter
{ {
await base.WriteMessageAsync(message, cancellationToken); await base.WriteMessageAsync(message, cancellationToken);
// If message group is empty or the given message can be grouped, buffer the given message // If the message can be grouped, buffer it for now
if (!_messageGroupBuffer.Any() || MessageGroup.CanJoin(_messageGroupBuffer.Last(), message)) if (CanJoinGroup( message))
{ {
_messageGroupBuffer.Add(message); _messageGroup.Add(message);
} }
// Otherwise, flush the group and render messages // Otherwise, flush the group and render messages
else else
{ {
await WriteMessageGroupAsync(MessageGroup.Join(_messageGroupBuffer), cancellationToken); await WriteMessageGroupAsync(_messageGroup, cancellationToken);
_messageGroupBuffer.Clear(); _messageGroup.Clear();
_messageGroupBuffer.Add(message); _messageGroup.Add(message);
} }
} }
public override async ValueTask WritePostambleAsync(CancellationToken cancellationToken = default) public override async ValueTask WritePostambleAsync(CancellationToken cancellationToken = default)
{ {
// Flush current message group // Flush current message group
if (_messageGroupBuffer.Any()) if (_messageGroup.Any())
{ await WriteMessageGroupAsync(_messageGroup, cancellationToken);
await WriteMessageGroupAsync(
MessageGroup.Join(_messageGroupBuffer),
cancellationToken
);
}
var templateContext = new PostambleTemplateContext(Context, MessagesWritten); var templateContext = new PostambleTemplateContext(Context, MessagesWritten);

View file

@ -33,15 +33,37 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
{ {
var (tagOpen, tagClose) = formatting.Kind switch var (tagOpen, tagClose) = formatting.Kind switch
{ {
FormattingKind.Bold => ("<strong>", "</strong>"), FormattingKind.Bold => (
FormattingKind.Italic => ("<em>", "</em>"), "<strong>",
FormattingKind.Underline => ("<u>", "</u>"), "</strong>"
FormattingKind.Strikethrough => ("<s>", "</s>"), ),
FormattingKind.Italic => (
"<em>",
"</em>"
),
FormattingKind.Underline => (
"<u>",
"</u>"
),
FormattingKind.Strikethrough => (
"<s>",
"</s>"
),
FormattingKind.Spoiler => ( FormattingKind.Spoiler => (
"<span class=\"spoiler-text spoiler-text--hidden\" onclick=\"showSpoiler(event, this)\">", "</span>"), "<span class=\"chatlog__markdown-spoiler chatlog__markdown-spoiler--hidden\" onclick=\"showSpoiler(event, this)\">",
"</span>"
),
FormattingKind.Quote => ( FormattingKind.Quote => (
"<div class=\"quote\"><div class=\"quote-border\"></div><div class=\"quote-content\">", "</div></div>"), "<div class=\"chatlog__markdown-quote\"><div class=\"chatlog__markdown-quote-border\"></div><div class=\"chatlog__markdown-quote-content\">",
_ => throw new ArgumentOutOfRangeException(nameof(formatting.Kind)) "</div></div>"
),
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
}; };
_buffer.Append(tagOpen); _buffer.Append(tagOpen);
@ -54,7 +76,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
protected override MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) protected override MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock)
{ {
_buffer _buffer
.Append("<span class=\"pre pre--inline\">") .Append("<span class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
.Append(HtmlEncode(inlineCodeBlock.Code)) .Append(HtmlEncode(inlineCodeBlock.Code))
.Append("</span>"); .Append("</span>");
@ -68,7 +90,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
: "nohighlight"; : "nohighlight";
_buffer _buffer
.Append($"<div class=\"pre pre--multiline {highlightCssClass}\">") .Append($"<div class=\"chatlog__markdown-pre chatlog__markdown-pre--multiline {highlightCssClass}\">")
.Append(HtmlEncode(multiLineCodeBlock.Code)) .Append(HtmlEncode(multiLineCodeBlock.Code))
.Append("</div>"); .Append("</div>");
@ -98,10 +120,10 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
protected override MarkdownNode VisitEmoji(EmojiNode emoji) protected override MarkdownNode VisitEmoji(EmojiNode emoji)
{ {
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated); var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
var jumboClass = _isJumbo ? "emoji--large" : ""; var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
_buffer _buffer
.Append($"<img loading=\"lazy\" class=\"emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">"); .Append($"<img loading=\"lazy\" class=\"chatlog__emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
return base.VisitEmoji(emoji); return base.VisitEmoji(emoji);
} }
@ -111,14 +133,14 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
if (mention.Kind == MentionKind.Everyone) if (mention.Kind == MentionKind.Everyone)
{ {
_buffer _buffer
.Append("<span class=\"mention\">") .Append("<span class=\"chatlog__markdown-mention\">")
.Append("@everyone") .Append("@everyone")
.Append("</span>"); .Append("</span>");
} }
else if (mention.Kind == MentionKind.Here) else if (mention.Kind == MentionKind.Here)
{ {
_buffer _buffer
.Append("<span class=\"mention\">") .Append("<span class=\"chatlog__markdown-mention\">")
.Append("@here") .Append("@here")
.Append("</span>"); .Append("</span>");
} }
@ -129,7 +151,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var nick = member?.Nick ?? "Unknown"; var nick = member?.Nick ?? "Unknown";
_buffer _buffer
.Append($"<span class=\"mention\" title=\"{HtmlEncode(fullName)}\">") .Append($"<span class=\"chatlog__markdown-mention\" title=\"{HtmlEncode(fullName)}\">")
.Append('@').Append(HtmlEncode(nick)) .Append('@').Append(HtmlEncode(nick))
.Append("</span>"); .Append("</span>");
} }
@ -140,7 +162,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var name = channel?.Name ?? "deleted-channel"; var name = channel?.Name ?? "deleted-channel";
_buffer _buffer
.Append("<span class=\"mention\">") .Append("<span class=\"chatlog__markdown-mention\">")
.Append(symbol).Append(HtmlEncode(name)) .Append(symbol).Append(HtmlEncode(name))
.Append("</span>"); .Append("</span>");
} }
@ -151,11 +173,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
var color = role?.Color; var color = role?.Color;
var style = color is not null var style = color is not null
? $"color: rgb({color?.R}, {color?.G}, {color?.B}); background-color: rgba({color?.R}, {color?.G}, {color?.B}, 0.1);" ? $"color: rgb({color.Value.R}, {color.Value.G}, {color.Value.B}); " +
$"background-color: rgba({color.Value.R}, {color.Value.G}, {color.Value.B}, 0.1);"
: ""; : "";
_buffer _buffer
.Append($"<span class=\"mention\" style=\"{style}\">") .Append($"<span class=\"chatlog__markdown-mention\" style=\"{style}\">")
.Append('@').Append(HtmlEncode(name)) .Append('@').Append(HtmlEncode(name))
.Append("</span>"); .Append("</span>");
} }
@ -175,7 +198,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
: "Invalid date"; : "Invalid date";
_buffer _buffer
.Append($"<span class=\"timestamp\" title=\"{HtmlEncode(longDateString)}\">") .Append($"<span class=\"chatlog__markdown-timestamp\" title=\"{HtmlEncode(longDateString)}\">")
.Append(HtmlEncode(dateString)) .Append(HtmlEncode(dateString))
.Append("</span>"); .Append("</span>");

View file

@ -77,8 +77,9 @@ internal static partial class MarkdownParser
// Capture any character until the end of the line // Capture any character until the end of the line
// Opening 'greater than' character must be followed by whitespace // Opening 'greater than' character must be followed by whitespace
// Text content is optional
private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>( private static readonly IMatcher<MarkdownNode> SingleLineQuoteNodeMatcher = new RegexMatcher<MarkdownNode>(
new Regex("^>\\s(.+\n?)", DefaultRegexOptions), new Regex("^>\\s(.*\n?)", DefaultRegexOptions),
(s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1]))) (s, m) => new FormattingNode(FormattingKind.Quote, Parse(s.Relocate(m.Groups[1])))
); );
@ -86,7 +87,7 @@ internal static partial class MarkdownParser
// This one is tricky as it ends up producing multiple separate captures which need to be joined // This one is tricky as it ends up producing multiple separate captures which need to be joined
private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher = private static readonly IMatcher<MarkdownNode> RepeatedSingleLineQuoteNodeMatcher =
new RegexMatcher<MarkdownNode>( new RegexMatcher<MarkdownNode>(
new Regex("(?:^>\\s(.+\n?)){2,}", DefaultRegexOptions), new Regex("(?:^>\\s(.*\n?)){2,}", DefaultRegexOptions),
(_, m) => (_, m) =>
{ {
var content = string.Concat(m.Groups[1].Captures.Select(c => c.Value)); var content = string.Concat(m.Groups[1].Captures.Select(c => c.Value));