mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2025-05-29 14:05:18 -04:00
Improve performance (#162)
This commit is contained in:
parent
359278afec
commit
4bfb2ec7fd
86 changed files with 1242 additions and 900 deletions
112
DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs
Normal file
112
DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
using DiscordChatExporter.Core.Markdown.Nodes;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public class CsvChatLogRenderer : IChatLogRenderer
|
||||
{
|
||||
private readonly ChatLog _chatLog;
|
||||
private readonly string _dateFormat;
|
||||
|
||||
public CsvChatLogRenderer(ChatLog chatLog, string dateFormat)
|
||||
{
|
||||
_chatLog = chatLog;
|
||||
_dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatMarkdown(Node node)
|
||||
{
|
||||
// Formatted node
|
||||
if (node is FormattedNode formattedNode)
|
||||
{
|
||||
// Recursively get inner text
|
||||
var innerText = FormatMarkdown(formattedNode.Children);
|
||||
|
||||
return $"{formattedNode.Token}{innerText}{formattedNode.Token}";
|
||||
}
|
||||
|
||||
// Non-meta mention node
|
||||
if (node is MentionNode mentionNode && mentionNode.Type != MentionType.Meta)
|
||||
{
|
||||
// User mention node
|
||||
if (mentionNode.Type == MentionType.User)
|
||||
{
|
||||
var user = _chatLog.Mentionables.GetUser(mentionNode.Id);
|
||||
return $"@{user.Name}";
|
||||
}
|
||||
|
||||
// Channel mention node
|
||||
if (mentionNode.Type == MentionType.Channel)
|
||||
{
|
||||
var channel = _chatLog.Mentionables.GetChannel(mentionNode.Id);
|
||||
return $"#{channel.Name}";
|
||||
}
|
||||
|
||||
// Role mention node
|
||||
if (mentionNode.Type == MentionType.Role)
|
||||
{
|
||||
var role = _chatLog.Mentionables.GetRole(mentionNode.Id);
|
||||
return $"@{role.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
// Custom emoji node
|
||||
if (node is EmojiNode emojiNode && emojiNode.IsCustomEmoji)
|
||||
{
|
||||
return $":{emojiNode.Name}:";
|
||||
}
|
||||
|
||||
// All other nodes - simply return source
|
||||
return node.Source;
|
||||
}
|
||||
|
||||
private string FormatMarkdown(IEnumerable<Node> nodes) => nodes.Select(FormatMarkdown).JoinToString("");
|
||||
|
||||
private string FormatMarkdown(string markdown) => FormatMarkdown(MarkdownParser.Parse(markdown));
|
||||
|
||||
private async Task RenderFieldAsync(TextWriter writer, string value)
|
||||
{
|
||||
var encodedValue = value.Replace("\"", "\"\"");
|
||||
await writer.WriteAsync($"\"{encodedValue}\";");
|
||||
}
|
||||
|
||||
private async Task RenderMessageAsync(TextWriter writer, Message message)
|
||||
{
|
||||
// Author
|
||||
await RenderFieldAsync(writer, message.Author.FullName);
|
||||
|
||||
// Timestamp
|
||||
await RenderFieldAsync(writer, FormatDate(message.Timestamp));
|
||||
|
||||
// Content
|
||||
await RenderFieldAsync(writer, FormatMarkdown(message.Content));
|
||||
|
||||
// Attachments
|
||||
var formattedAttachments = message.Attachments.Select(a => a.Url).JoinToString(",");
|
||||
await RenderFieldAsync(writer, formattedAttachments);
|
||||
|
||||
// Line break
|
||||
await writer.WriteLineAsync();
|
||||
}
|
||||
|
||||
public async Task RenderAsync(TextWriter writer)
|
||||
{
|
||||
// Headers
|
||||
await writer.WriteLineAsync("Author;Date;Content;Attachments;");
|
||||
|
||||
// Log
|
||||
foreach (var message in _chatLog.Messages)
|
||||
await RenderMessageAsync(writer, message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\HtmlDark.css" />
|
||||
<EmbeddedResource Include="Resources\HtmlDark.html" />
|
||||
<EmbeddedResource Include="Resources\HtmlLight.css" />
|
||||
<EmbeddedResource Include="Resources\HtmlLight.html" />
|
||||
<EmbeddedResource Include="Resources\HtmlShared.css" />
|
||||
<EmbeddedResource Include="Resources\HtmlShared.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Scriban" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DiscordChatExporter.Core.Markdown\DiscordChatExporter.Core.Markdown.csproj" />
|
||||
<ProjectReference Include="..\DiscordChatExporter.Core.Models\DiscordChatExporter.Core.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,25 @@
|
|||
using DiscordChatExporter.Core.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public partial class HtmlChatLogRenderer
|
||||
{
|
||||
private class MessageGroup
|
||||
{
|
||||
public User Author { get; }
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public IReadOnlyList<Message> Messages { get; }
|
||||
|
||||
public MessageGroup(User author, DateTime timestamp, IReadOnlyList<Message> messages)
|
||||
{
|
||||
Author = author;
|
||||
Timestamp = timestamp;
|
||||
Messages = messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using Scriban.Parsing;
|
||||
using Scriban.Runtime;
|
||||
using Scriban;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public partial class HtmlChatLogRenderer
|
||||
{
|
||||
private class TemplateLoader : ITemplateLoader
|
||||
{
|
||||
private const string ResourceRootNamespace = "DiscordChatExporter.Core.Rendering.Resources";
|
||||
|
||||
public string Load(string templatePath) =>
|
||||
Assembly.GetExecutingAssembly().GetManifestResourceString($"{ResourceRootNamespace}.{templatePath}");
|
||||
|
||||
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName) => templateName;
|
||||
|
||||
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath) => Load(templatePath);
|
||||
|
||||
public ValueTask<string> LoadAsync(TemplateContext context, SourceSpan callerSpan, string templatePath) =>
|
||||
new ValueTask<string>(Load(templatePath));
|
||||
}
|
||||
}
|
||||
}
|
197
DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs
Normal file
197
DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs
Normal file
|
@ -0,0 +1,197 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
using DiscordChatExporter.Core.Markdown.Nodes;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Scriban;
|
||||
using Scriban.Runtime;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public partial class HtmlChatLogRenderer : IChatLogRenderer
|
||||
{
|
||||
private readonly ChatLog _chatLog;
|
||||
private readonly string _themeName;
|
||||
private readonly string _dateFormat;
|
||||
|
||||
public HtmlChatLogRenderer(ChatLog chatLog, string themeName, string dateFormat)
|
||||
{
|
||||
_chatLog = chatLog;
|
||||
_themeName = themeName;
|
||||
_dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
private string HtmlEncode(string s) => WebUtility.HtmlEncode(s);
|
||||
|
||||
private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
private IEnumerable<MessageGroup> GroupMessages(IEnumerable<Message> messages) =>
|
||||
messages.GroupContiguous((buffer, message) =>
|
||||
{
|
||||
// Break group if the author changed
|
||||
if (buffer.Last().Author.Id != message.Author.Id)
|
||||
return false;
|
||||
|
||||
// Break group if last message was more than 7 minutes ago
|
||||
if ((message.Timestamp - buffer.Last().Timestamp).TotalMinutes > 7)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}).Select(g => new MessageGroup(g.First().Author, g.First().Timestamp, g));
|
||||
|
||||
private string FormatMarkdown(Node node, bool isTopLevel, bool isSingle)
|
||||
{
|
||||
// Text node
|
||||
if (node is TextNode textNode)
|
||||
{
|
||||
// Return HTML-encoded text
|
||||
return HtmlEncode(textNode.Text);
|
||||
}
|
||||
|
||||
// Formatted node
|
||||
if (node is FormattedNode formattedNode)
|
||||
{
|
||||
// Recursively get inner html
|
||||
var innerHtml = FormatMarkdown(formattedNode.Children, false);
|
||||
|
||||
// Bold
|
||||
if (formattedNode.Formatting == TextFormatting.Bold)
|
||||
return $"<strong>{innerHtml}</strong>";
|
||||
|
||||
// Italic
|
||||
if (formattedNode.Formatting == TextFormatting.Italic)
|
||||
return $"<em>{innerHtml}</em>";
|
||||
|
||||
// Underline
|
||||
if (formattedNode.Formatting == TextFormatting.Underline)
|
||||
return $"<u>{innerHtml}</u>";
|
||||
|
||||
// Strikethrough
|
||||
if (formattedNode.Formatting == TextFormatting.Strikethrough)
|
||||
return $"<s>{innerHtml}</s>";
|
||||
|
||||
// Spoiler
|
||||
if (formattedNode.Formatting == TextFormatting.Spoiler)
|
||||
return $"<span class=\"spoiler\">{innerHtml}</span>";
|
||||
}
|
||||
|
||||
// Inline code block node
|
||||
if (node is InlineCodeBlockNode inlineCodeBlockNode)
|
||||
{
|
||||
return $"<span class=\"pre pre--inline\">{HtmlEncode(inlineCodeBlockNode.Code)}</span>";
|
||||
}
|
||||
|
||||
// Multi-line code block node
|
||||
if (node is MultilineCodeBlockNode multilineCodeBlockNode)
|
||||
{
|
||||
// Set language class for syntax highlighting
|
||||
var languageCssClass = multilineCodeBlockNode.Language != null
|
||||
? "language-" + multilineCodeBlockNode.Language
|
||||
: null;
|
||||
|
||||
return $"<div class=\"pre pre--multiline {languageCssClass}\">{HtmlEncode(multilineCodeBlockNode.Code)}</div>";
|
||||
}
|
||||
|
||||
// Mention node
|
||||
if (node is MentionNode mentionNode)
|
||||
{
|
||||
// Meta mention node
|
||||
if (mentionNode.Type == MentionType.Meta)
|
||||
{
|
||||
return $"<span class=\"mention\">@{HtmlEncode(mentionNode.Id)}</span>";
|
||||
}
|
||||
|
||||
// User mention node
|
||||
if (mentionNode.Type == MentionType.User)
|
||||
{
|
||||
var user = _chatLog.Mentionables.GetUser(mentionNode.Id);
|
||||
return $"<span class=\"mention\" title=\"{HtmlEncode(user.FullName)}\">@{HtmlEncode(user.Name)}</span>";
|
||||
}
|
||||
|
||||
// Channel mention node
|
||||
if (mentionNode.Type == MentionType.Channel)
|
||||
{
|
||||
var channel = _chatLog.Mentionables.GetChannel(mentionNode.Id);
|
||||
return $"<span class=\"mention\">#{HtmlEncode(channel.Name)}</span>";
|
||||
}
|
||||
|
||||
// Role mention node
|
||||
if (mentionNode.Type == MentionType.Role)
|
||||
{
|
||||
var role = _chatLog.Mentionables.GetRole(mentionNode.Id);
|
||||
return $"<span class=\"mention\">@{HtmlEncode(role.Name)}</span>";
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji node
|
||||
if (node is EmojiNode emojiNode)
|
||||
{
|
||||
// Get emoji image URL
|
||||
var emojiImageUrl = Emoji.GetImageUrl(emojiNode.Id, emojiNode.Name, emojiNode.IsAnimated);
|
||||
|
||||
// Emoji can be jumboable if it's the only top-level node
|
||||
var jumboableCssClass = isTopLevel && isSingle ? "emoji--large" : null;
|
||||
|
||||
return $"<img class=\"emoji {jumboableCssClass}\" alt=\"{emojiNode.Name}\" title=\"{emojiNode.Name}\" src=\"{emojiImageUrl}\" />";
|
||||
}
|
||||
|
||||
// Link node
|
||||
if (node is LinkNode linkNode)
|
||||
{
|
||||
return $"<a href=\"{Uri.EscapeUriString(linkNode.Url)}\">{HtmlEncode(linkNode.Title)}</a>";
|
||||
}
|
||||
|
||||
// All other nodes - simply return source
|
||||
return node.Source;
|
||||
}
|
||||
|
||||
private string FormatMarkdown(IReadOnlyList<Node> nodes, bool isTopLevel)
|
||||
{
|
||||
var isSingle = nodes.Count == 1;
|
||||
return nodes.Select(n => FormatMarkdown(n, isTopLevel, isSingle)).JoinToString("");
|
||||
}
|
||||
|
||||
private string FormatMarkdown(string markdown) => FormatMarkdown(MarkdownParser.Parse(markdown), true);
|
||||
|
||||
public async Task RenderAsync(TextWriter writer)
|
||||
{
|
||||
// Create template loader
|
||||
var loader = new TemplateLoader();
|
||||
|
||||
// Get template
|
||||
var templateCode = loader.Load($"Html{_themeName}.html");
|
||||
var template = Template.Parse(templateCode);
|
||||
|
||||
// Create template context
|
||||
var context = new TemplateContext
|
||||
{
|
||||
TemplateLoader = loader,
|
||||
MemberRenamer = m => m.Name,
|
||||
MemberFilter = m => true,
|
||||
LoopLimit = int.MaxValue,
|
||||
StrictVariables = true
|
||||
};
|
||||
|
||||
// Create template model
|
||||
var model = new ScriptObject();
|
||||
model.SetValue("Model", _chatLog, true);
|
||||
model.Import(nameof(GroupMessages), new Func<IEnumerable<Message>, IEnumerable<MessageGroup>>(GroupMessages));
|
||||
model.Import(nameof(FormatDate), new Func<DateTime, string>(FormatDate));
|
||||
model.Import(nameof(FormatMarkdown), new Func<string, string>(FormatMarkdown));
|
||||
context.PushGlobal(model);
|
||||
|
||||
// Configure output
|
||||
context.PushOutput(new TextWriterOutput(writer));
|
||||
|
||||
// HACK: Render output in a separate thread
|
||||
// (even though Scriban has async API, it still makes a lot of blocking CPU-bound calls)
|
||||
await Task.Run(async () => await context.EvaluateAsync(template.Page));
|
||||
}
|
||||
}
|
||||
}
|
10
DiscordChatExporter.Core.Rendering/IChatLogRenderer.cs
Normal file
10
DiscordChatExporter.Core.Rendering/IChatLogRenderer.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public interface IChatLogRenderer
|
||||
{
|
||||
Task RenderAsync(TextWriter writer);
|
||||
}
|
||||
}
|
128
DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs
Normal file
128
DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs
Normal file
|
@ -0,0 +1,128 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Markdown;
|
||||
using DiscordChatExporter.Core.Markdown.Nodes;
|
||||
using DiscordChatExporter.Core.Models;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Core.Rendering
|
||||
{
|
||||
public class PlainTextChatLogRenderer : IChatLogRenderer
|
||||
{
|
||||
private readonly ChatLog _chatLog;
|
||||
private readonly string _dateFormat;
|
||||
|
||||
public PlainTextChatLogRenderer(ChatLog chatLog, string dateFormat)
|
||||
{
|
||||
_chatLog = chatLog;
|
||||
_dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
private string FormatDateRange(DateTime? from, DateTime? to)
|
||||
{
|
||||
// Both 'from' and 'to'
|
||||
if (from.HasValue && to.HasValue)
|
||||
return $"{FormatDate(from.Value)} to {FormatDate(to.Value)}";
|
||||
|
||||
// Just 'from'
|
||||
if (from.HasValue)
|
||||
return $"after {FormatDate(from.Value)}";
|
||||
|
||||
// Just 'to'
|
||||
if (to.HasValue)
|
||||
return $"before {FormatDate(to.Value)}";
|
||||
|
||||
// Neither
|
||||
return null;
|
||||
}
|
||||
|
||||
private string FormatMarkdown(Node node)
|
||||
{
|
||||
// Formatted node
|
||||
if (node is FormattedNode formattedNode)
|
||||
{
|
||||
// Recursively get inner text
|
||||
var innerText = FormatMarkdown(formattedNode.Children);
|
||||
|
||||
return $"{formattedNode.Token}{innerText}{formattedNode.Token}";
|
||||
}
|
||||
|
||||
// Non-meta mention node
|
||||
if (node is MentionNode mentionNode && mentionNode.Type != MentionType.Meta)
|
||||
{
|
||||
// User mention node
|
||||
if (mentionNode.Type == MentionType.User)
|
||||
{
|
||||
var user = _chatLog.Mentionables.GetUser(mentionNode.Id);
|
||||
return $"@{user.Name}";
|
||||
}
|
||||
|
||||
// Channel mention node
|
||||
if (mentionNode.Type == MentionType.Channel)
|
||||
{
|
||||
var channel = _chatLog.Mentionables.GetChannel(mentionNode.Id);
|
||||
return $"#{channel.Name}";
|
||||
}
|
||||
|
||||
// Role mention node
|
||||
if (mentionNode.Type == MentionType.Role)
|
||||
{
|
||||
var role = _chatLog.Mentionables.GetRole(mentionNode.Id);
|
||||
return $"@{role.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
// Custom emoji node
|
||||
if (node is EmojiNode emojiNode && emojiNode.IsCustomEmoji)
|
||||
{
|
||||
return $":{emojiNode.Name}:";
|
||||
}
|
||||
|
||||
// All other nodes - simply return source
|
||||
return node.Source;
|
||||
}
|
||||
|
||||
private string FormatMarkdown(IEnumerable<Node> nodes) => nodes.Select(FormatMarkdown).JoinToString("");
|
||||
|
||||
private string FormatMarkdown(string markdown) => FormatMarkdown(MarkdownParser.Parse(markdown));
|
||||
|
||||
private async Task RenderMessageAsync(TextWriter writer, Message message)
|
||||
{
|
||||
// Timestamp and author
|
||||
await writer.WriteLineAsync($"[{FormatDate(message.Timestamp)}] {message.Author.FullName}");
|
||||
|
||||
// Content
|
||||
await writer.WriteLineAsync(FormatMarkdown(message.Content));
|
||||
|
||||
// Attachments
|
||||
foreach (var attachment in message.Attachments)
|
||||
await writer.WriteLineAsync(attachment.Url);
|
||||
}
|
||||
|
||||
public async Task RenderAsync(TextWriter writer)
|
||||
{
|
||||
// Metadata
|
||||
await writer.WriteLineAsync('='.Repeat(62));
|
||||
await writer.WriteLineAsync($"Guild: {_chatLog.Guild.Name}");
|
||||
await writer.WriteLineAsync($"Channel: {_chatLog.Channel.Name}");
|
||||
await writer.WriteLineAsync($"Topic: {_chatLog.Channel.Topic}");
|
||||
await writer.WriteLineAsync($"Messages: {_chatLog.Messages.Count:N0}");
|
||||
await writer.WriteLineAsync($"Range: {FormatDateRange(_chatLog.From, _chatLog.To)}");
|
||||
await writer.WriteLineAsync('='.Repeat(62));
|
||||
await writer.WriteLineAsync();
|
||||
|
||||
// Log
|
||||
foreach (var message in _chatLog.Messages)
|
||||
{
|
||||
await RenderMessageAsync(writer, message);
|
||||
await writer.WriteLineAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
DiscordChatExporter.Core.Rendering/Resources/HtmlDark.css
Normal file
100
DiscordChatExporter.Core.Rendering/Resources/HtmlDark.css
Normal file
|
@ -0,0 +1,100 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #36393e;
|
||||
color: #dcddde;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096cf;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pre {
|
||||
background-color: #2f3136 !important;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
border-color: #282b30 !important;
|
||||
color: #839496 !important;
|
||||
}
|
||||
|
||||
.mention {
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
background-color: rgba(46, 48, 54, 0.3);
|
||||
border-color: rgba(46, 48, 54, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name-link {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{{~ ThemeStyleSheet = include "HtmlDark.css" ~}}
|
||||
{{~ HighlightJsStyleName = "solarized-dark" ~}}
|
||||
{{~ include "HtmlShared.html" ~}}
|
101
DiscordChatExporter.Core.Rendering/Resources/HtmlLight.css
Normal file
101
DiscordChatExporter.Core.Rendering/Resources/HtmlLight.css
Normal file
|
@ -0,0 +1,101 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
color: #747f8d;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00b0f4;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pre {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
border-color: #f3f3f3 !important;
|
||||
color: #657b83 !important;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: #f1f3fb;
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info__guild-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog__message-group {
|
||||
border-color: #eceeef;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
color: #2f3136;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
color: #99aab5;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
color: #99aab5;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
background-color: rgba(249, 249, 249, 0.3);
|
||||
border-color: rgba(204, 204, 204, 0.3);
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name-link {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
color: #4f545c;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
color: #737f8d;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
color: #36393e;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
color: #737f8d;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
color: rgba(79, 83, 91, 0.4);
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
background-color: rgba(79, 84, 92, 0.06);
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
color: #99aab5;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{{~ ThemeStyleSheet = include "HtmlLight.css" ~}}
|
||||
{{~ HighlightJsStyleName = "solarized-light" ~}}
|
||||
{{~ include "HtmlShared.html" ~}}
|
312
DiscordChatExporter.Core.Rendering/Resources/HtmlShared.css
Normal file
312
DiscordChatExporter.Core.Rendering/Resources/HtmlShared.css
Normal file
|
@ -0,0 +1,312 @@
|
|||
/* === GENERAL === */
|
||||
|
||||
body {
|
||||
font-family: "Whitney", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pre {
|
||||
font-family: "Consolas", "Courier New", Courier, Monospace;
|
||||
}
|
||||
|
||||
.pre--multiline {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
border: 2px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.pre--inline {
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1.45em;
|
||||
height: 1.45em;
|
||||
margin: 0 1px;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
|
||||
.emoji--small {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.emoji--large {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/* === INFO === */
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin: 0 5px 10px 5px;
|
||||
}
|
||||
|
||||
.info__guild-icon-container {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.info__guild-icon {
|
||||
max-width: 88px;
|
||||
max-height: 88px;
|
||||
}
|
||||
|
||||
.info__metadata {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.info__guild-name {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.info__channel-name {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.info__channel-topic {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info__channel-message-count {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info__channel-date-range {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* === CHATLOG === */
|
||||
|
||||
.chatlog {
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chatlog__message-group {
|
||||
display: flex;
|
||||
margin: 0 10px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar-container {
|
||||
flex: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.chatlog__author-avatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.chatlog__messages {
|
||||
flex: 1;
|
||||
min-width: 50%;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.chatlog__author-name {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatlog__timestamp {
|
||||
margin-left: 5px;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.chatlog__content {
|
||||
padding-top: 5px;
|
||||
font-size: .9375em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chatlog__edited-timestamp {
|
||||
margin-left: 3px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.chatlog__attachment {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.chatlog__attachment-thumbnail {
|
||||
max-width: 50%;
|
||||
max-height: 500px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed {
|
||||
display: flex;
|
||||
max-width: 520px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-color-pill {
|
||||
flex-shrink: 0;
|
||||
width: 4px;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatlog__embed-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatlog__embed-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 9px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chatlog__embed-author-name {
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-description {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chatlog__embed-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chatlog__embed-field {
|
||||
flex: 0;
|
||||
min-width: 100%;
|
||||
max-width: 506px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field--inline {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-name {
|
||||
margin-bottom: 4px;
|
||||
font-size: .875em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatlog__embed-field-value {
|
||||
font-size: .875em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatlog__embed-thumbnail {
|
||||
flex: 0;
|
||||
margin-left: 20px;
|
||||
max-width: 80px;
|
||||
max-height: 80px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-image {
|
||||
max-width: 500px;
|
||||
max-height: 400px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-icon {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chatlog__embed-footer-text {
|
||||
font-weight: 600;
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.chatlog__reactions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatlog__reaction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 6px 2px 2px 2px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chatlog__reaction-count {
|
||||
min-width: 9px;
|
||||
margin-left: 6px;
|
||||
font-size: .875em;
|
||||
}
|
225
DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html
Normal file
225
DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html
Normal file
|
@ -0,0 +1,225 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{{~ # Metadata ~}}
|
||||
<title>{{ Model.Guild.Name | html.escape }} - {{ Model.Channel.Name | html.escape }}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
{{~ # Styles ~}}
|
||||
<style>
|
||||
{{ include "HtmlShared.css" }}
|
||||
</style>
|
||||
<style>
|
||||
{{ ThemeStyleSheet }}
|
||||
</style>
|
||||
|
||||
{{~ # Syntax highlighting ~}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/{{HighlightJsStyleName}}.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.pre--multiline').forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{~ # Info ~}}
|
||||
<div class="info">
|
||||
<div class="info__guild-icon-container">
|
||||
<img class="info__guild-icon" src="{{ Model.Guild.IconUrl }}" />
|
||||
</div>
|
||||
<div class="info__metadata">
|
||||
<div class="info__guild-name">{{ Model.Guild.Name | html.escape }}</div>
|
||||
<div class="info__channel-name">{{ Model.Channel.Name | html.escape }}</div>
|
||||
|
||||
{{~ if Model.Channel.Topic ~}}
|
||||
<div class="info__channel-topic">{{ Model.Channel.Topic | html.escape }}</div>
|
||||
{{~ end ~}}
|
||||
|
||||
<div class="info__channel-message-count">{{ Model.Messages | array.size | object.format "N0" }} messages</div>
|
||||
|
||||
{{~ if Model.From || Model.To ~}}
|
||||
<div class="info__channel-date-range">
|
||||
{{~ if Model.From && Model.To ~}}
|
||||
Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }}
|
||||
{{~ else if Model.From ~}}
|
||||
After {{ Model.From | FormatDate | html.escape }}
|
||||
{{~ else if Model.To ~}}
|
||||
Before {{ Model.To | FormatDate | html.escape }}
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{~ # Log ~}}
|
||||
<div class="chatlog">
|
||||
{{~ for group in Model.Messages | GroupMessages ~}}
|
||||
<div class="chatlog__message-group">
|
||||
{{~ # Avatar ~}}
|
||||
<div class="chatlog__author-avatar-container">
|
||||
<img class="chatlog__author-avatar" src="{{ group.Author.AvatarUrl }}" />
|
||||
</div>
|
||||
<div class="chatlog__messages">
|
||||
{{~ # Author name and timestamp ~}}
|
||||
<span class="chatlog__author-name" title="{{ group.Author.FullName | html.escape }}">{{ group.Author.Name | html.escape }}</span>
|
||||
<span class="chatlog__timestamp">{{ group.Timestamp | FormatDate | html.escape }}</span>
|
||||
|
||||
{{~ # Messages ~}}
|
||||
{{~ for message in group.Messages ~}}
|
||||
{{~ # Content ~}}
|
||||
{{~ if message.Content ~}}
|
||||
<div class="chatlog__content">
|
||||
<span class="markdown">{{ message.Content | FormatMarkdown }}</span>
|
||||
|
||||
{{~ # Edited timestamp ~}}
|
||||
{{~ if message.EditedTimestamp ~}}
|
||||
<span class="chatlog__edited-timestamp" title="{{ message.EditedTimestamp | FormatDate | html.escape }}">(edited)</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Attachments ~}}
|
||||
{{~ for attachment in message.Attachments ~}}
|
||||
<div class="chatlog__attachment">
|
||||
<a href="{{ attachment.Url }}">
|
||||
{{ # Image }}
|
||||
{{~ if attachment.IsImage ~}}
|
||||
<img class="chatlog__attachment-thumbnail" src="{{ attachment.Url }}" />
|
||||
{{~ # Non-image ~}}
|
||||
{{~ else ~}}
|
||||
Attachment: {{ attachment.FileName }} ({{ attachment.FileSize }})
|
||||
{{~ end ~}}
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Embeds ~}}
|
||||
{{~ for embed in message.Embeds ~}}
|
||||
<div class="chatlog__embed">
|
||||
<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-content-container">
|
||||
<div class="chatlog__embed-content">
|
||||
<div class="chatlog__embed-text">
|
||||
{{~ # Author ~}}
|
||||
{{~ if embed.Author ~}}
|
||||
<div class="chatlog__embed-author">
|
||||
{{~ if embed.Author.IconUrl ~}}
|
||||
<img class="chatlog__embed-author-icon" src="{{ embed.Author.IconUrl }}" />
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ if embed.Author.Name ~}}
|
||||
<span class="chatlog__embed-author-name">
|
||||
{{~ if embed.Author.Url ~}}
|
||||
<a class="chatlog__embed-author-name-link" href="{{ embed.Author.Url }}">{{ embed.Author.Name | html.escape }}</a>
|
||||
{{~ else ~}}
|
||||
{{ embed.Author.Name | html.escape }}
|
||||
{{~ end ~}}
|
||||
</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Title ~}}
|
||||
{{~ if embed.Title ~}}
|
||||
<div class="chatlog__embed-title">
|
||||
{{~ if embed.Url ~}}
|
||||
<a class="chatlog__embed-title-link" href="{{ embed.Url }}"><span class="markdown">{{ embed.Title | FormatMarkdown }}</span></a>
|
||||
{{~ else ~}}
|
||||
<span class="markdown">{{ embed.Title | FormatMarkdown }}</span>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Description ~}}
|
||||
{{~ if embed.Description ~}}
|
||||
<div class="chatlog__embed-description"><span class="markdown">{{ embed.Description | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Fields ~}}
|
||||
{{~ if embed.Fields | array.size > 0 ~}}
|
||||
<div class="chatlog__embed-fields">
|
||||
{{~ for field in embed.Fields ~}}
|
||||
<div class="chatlog__embed-field {{ if field.IsInline }} chatlog__embed-field--inline {{ end }}">
|
||||
{{~ if field.Name ~}}
|
||||
<div class="chatlog__embed-field-name"><span class="markdown">{{ field.Name | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
{{~ if field.Value ~}}
|
||||
<div class="chatlog__embed-field-value"><span class="markdown">{{ field.Value | FormatMarkdown }}</span></div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
{{~ # Thumbnail ~}}
|
||||
{{~ if embed.Thumbnail ~}}
|
||||
<div class="chatlog__embed-thumbnail-container">
|
||||
<a class="chatlog__embed-thumbnail-link" href="{{ embed.Thumbnail.Url }}">
|
||||
<img class="chatlog__embed-thumbnail" src="{{ embed.Thumbnail.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
{{~ # Image ~}}
|
||||
{{~ if embed.Image ~}}
|
||||
<div class="chatlog__embed-image-container">
|
||||
<a class="chatlog__embed-image-link" href="{{ embed.Image.Url }}">
|
||||
<img class="chatlog__embed-image" src="{{ embed.Image.Url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Footer ~}}
|
||||
{{~ if embed.Footer || embed.Timestamp ~}}
|
||||
<div class="chatlog__embed-footer">
|
||||
{{~ if embed.Footer ~}}
|
||||
{{~ if embed.Footer.Text && embed.Footer.IconUrl ~}}
|
||||
<img class="chatlog__embed-footer-icon" src="{{ embed.Footer.IconUrl }}" />
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
|
||||
<span class="chatlog__embed-footer-text">
|
||||
{{~ if embed.Footer ~}}
|
||||
{{~ if embed.Footer.Text ~}}
|
||||
{{ embed.Footer.Text | html.escape }}
|
||||
{{ if embed.Timestamp }} • {{ end }}
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ if embed.Timestamp ~}}
|
||||
{{ embed.Timestamp | FormatDate | html.escape }}
|
||||
{{~ end ~}}
|
||||
</span>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
|
||||
{{~ # Reactions ~}}
|
||||
{{~ if message.Reactions | array.size > 0 ~}}
|
||||
<div class="chatlog__reactions">
|
||||
{{~ for reaction in message.Reactions ~}}
|
||||
<div class="chatlog__reaction">
|
||||
<img class="emoji emoji--small" alt="{{ reaction.Emoji.Name }}" title="{{ reaction.Emoji.Name }}" src="{{ reaction.Emoji.ImageUrl }}" />
|
||||
<span class="chatlog__reaction-count">{{ reaction.Count }}</span>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
</div>
|
||||
{{~ end ~}}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue