diff --git a/DiscordChatExporter.Core/Discord/Data/MessageKind.cs b/DiscordChatExporter.Core/Discord/Data/MessageKind.cs index 67fb609..c24dedb 100644 --- a/DiscordChatExporter.Core/Discord/Data/MessageKind.cs +++ b/DiscordChatExporter.Core/Discord/Data/MessageKind.cs @@ -1,6 +1,4 @@ -using System.Text.RegularExpressions; - -namespace DiscordChatExporter.Core.Discord.Data; +namespace DiscordChatExporter.Core.Discord.Data; // https://discord.com/developers/docs/resources/channel#message-object-message-types public enum MessageKind @@ -18,10 +16,6 @@ public enum MessageKind public static class MessageKindExtensions { - public static bool IsSystemMessage(this MessageKind c) => + public static bool IsSystemNotification(this MessageKind c) => c is not MessageKind.Default and not MessageKind.Reply; - - public static string ToCssIdFormat(this MessageKind c) => - string.Join("-", Regex.Split(c.ToString(), @"(? - + diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml index 01d2e1a..992f68f 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/MessageGroupTemplate.cshtml @@ -46,129 +46,126 @@
@foreach (var (message, i) in Model.Messages.WithIndex()) { - var isSystemMessage = message.Kind.IsSystemMessage(); - var isFirst = i == 0;
- @{/* Left side */} -
- @if (isSystemMessage) - { - - } - else if(isFirst) - { - // Reference symbol - if (message.Reference is not null) - { -
- } - - // Avatar - Avatar - } - else - { -
@message.Timestamp.ToLocalString("t")
- } -
- - @{/* Right side */} -
- @if (isSystemMessage) - { - - // system messages are grouped even if the message author is different - // that's why we have to update the user values with the author of the current message - userMember = Model.ExportContext.TryGetMember(message.Author.Id); - - userColor = Model.ExportContext.TryGetUserColor(message.Author.Id); - - userNick = message.Author.IsBot - ? message.Author.Name - : userMember?.Nick ?? message.Author.Name; - + @{/* System notification */} + @if (message.Kind.IsSystemNotification()) + { + // System notifications are grouped even if the message author is different. + // That's why we have to update the user values with the author of the current message. + userMember = Model.ExportContext.TryGetMember(message.Author.Id); + userColor = Model.ExportContext.TryGetUserColor(message.Author.Id); + userNick = message.Author.IsBot + ? message.Author.Name + : userMember?.Nick ?? message.Author.Name; + +
+ +
+ +
@{/* Author name */} - @userNick - - @{/* System message content */} + @userNick + + @{/* System notification content */} @if(message.Kind == MessageKind.ChannelPinnedMessage) { - pinned a message to this channel. + pinned a message to this channel. } else { - @Raw(FormatMarkdown(Char.ToLowerInvariant(message.Content[0]) + message.Content.Substring(1))) + @(char.ToLowerInvariant(message.Content[0]) + message.Content[1..]) } @{/* Timestamp */} @FormatDate(message.Timestamp)
- } - else if (isFirst) - { - // Reference - if (message.Reference is not null) +
+ } + // Regular message + else + { +
+ @if (isFirst) { -
- @if (message.ReferencedMessage is not null) - { - Avatar -
@referencedUserNick
-
- - @if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden()) - { - @Raw(FormatEmbedMarkdown(message.ReferencedMessage.Content)) - } - else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any()) - { - Click to see attachment - 🖼️ - } - else + // Reference symbol + if (message.Reference is not null) + { +
+ } + + // Avatar + Avatar + } + else + { +
@message.Timestamp.ToLocalString("t")
+ } +
+ +
+ @if (isFirst) + { + // Reference + if (message.Reference is not null) + { +
+ @if (message.ReferencedMessage is not null) + { + Avatar +
@referencedUserNick
+
+ + @if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden()) + { + @Raw(FormatEmbedMarkdown(message.ReferencedMessage.Content)) + } + else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any()) + { + Click to see attachment + 🖼️ + } + else + { + Click to see original message + } + + + @if (message.ReferencedMessage.EditedTimestamp is not null) { - Click to see original message + (edited) } - +
+ } + else + { +
+ Original message was deleted or could not be loaded. +
+ } +
+ } - @if (message.ReferencedMessage.EditedTimestamp is not null) - { - (edited) - } -
- } - else + // Header +
+ @{/* Author name */} + @userNick + + @{/* Bot label */} + @if (message.Author.IsBot) { -
- Original message was deleted or could not be loaded. -
+ BOT } + + @{/* Timestamp */} + @FormatDate(message.Timestamp)
} - // Header -
- @{/* Author name */} - @userNick - - @{/* Bot label */} - @if (message.Author.IsBot) - { - BOT - } - - @{/* Timestamp */} - @FormatDate(message.Timestamp) -
- } - - @{/* Content */} - @if (!isSystemMessage) - { + @{/* Content */} @if (!string.IsNullOrWhiteSpace(message.Content) || message.EditedTimestamp is not null) {
@@ -185,340 +182,340 @@ }
} - } - - @{/* Attachments */} - @foreach (var attachment in message.Attachments) - { -
- @{/* Spoiler caption */} - @if (attachment.IsSpoiler) - { -
SPOILER
- } - - @{/* Attachment preview */} - @if (attachment.IsImage) - { - - @(attachment.Description ?? - - } - else if (attachment.IsVideo) - { - - } - else if (attachment.IsAudio) - { - - } - else - { -
- - - - -
- @attachment.FileSize -
-
- } -
- } - @{/* Embeds */} - @foreach (var embed in message.Embeds) - { - // Spotify embed - if (embed.TryGetSpotifyTrack() is { } spotifyTrackEmbed) + @{/* Attachments */} + @foreach (var attachment in message.Attachments) { -
-
- -
-
- } - // YouTube embed - else if (embed.TryGetYouTubeVideo() is { } youTubeVideoEmbed) - { -
- @{/* Color pill */} - @if (embed.Color is not null) +
+ @{/* Spoiler caption */} + @if (attachment.IsSpoiler) { -
+
SPOILER
+ } + + @{/* Attachment preview */} + @if (attachment.IsImage) + { + + @(attachment.Description ?? + + } + else if (attachment.IsVideo) + { + + } + else if (attachment.IsAudio) + { + } else { -
+
+ + + + +
+ @attachment.FileSize +
+
} +
+ } -
-
-
- @{/* Embed author */} - @if (embed.Author is not null) - { -
- @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) - { - Author icon - } + @{/* Embeds */} + @foreach (var embed in message.Embeds) + { + // Spotify embed + if (embed.TryGetSpotifyTrack() is { } spotifyTrackEmbed) + { +
+
+ +
+
+ } + // YouTube embed + else if (embed.TryGetYouTubeVideo() is { } youTubeVideoEmbed) + { +
+ @{/* Color pill */} + @if (embed.Color is not null) + { +
+ } + else + { +
+ } - @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) - { - if (!string.IsNullOrWhiteSpace(embed.Author.Url)) +
+
+
+ @{/* Embed author */} + @if (embed.Author is not null) + { +
+ @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) { - + Author icon + } + + @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) + { + if (!string.IsNullOrWhiteSpace(embed.Author.Url)) + { + +
@embed.Author.Name
+
+ } + else + {
@embed.Author.Name
+ } + } +
+ } + + @{/* Embed title */} + @if (!string.IsNullOrWhiteSpace(embed.Title)) + { +
+ @if (!string.IsNullOrWhiteSpace(embed.Url)) + { + +
@Raw(FormatEmbedMarkdown(embed.Title))
} else { -
@embed.Author.Name
+
@Raw(FormatEmbedMarkdown(embed.Title))
} - } -
- } +
+ } - @{/* Embed title */} - @if (!string.IsNullOrWhiteSpace(embed.Title)) - { -
- @if (!string.IsNullOrWhiteSpace(embed.Url)) - { - -
@Raw(FormatEmbedMarkdown(embed.Title))
-
- } - else - { -
@Raw(FormatEmbedMarkdown(embed.Title))
- } + @{/* Video player */} +
+
- } - - @{/* Video player */} -
-
-
- } - // Generic image embed - else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url)) - { -
- - Embedded image - -
- } - // Generic gifv embed - else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Video?.Url)) - { -
- -
- } - // Rich embed - else - { -
- @{/* Color pill */} - @if (embed.Color is not null) - { -
- } - else - { -
- } + } + // Generic image embed + else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url)) + { +
+ + Embedded image + +
+ } + // Generic gifv embed + else if (embed.Kind == EmbedKind.Gifv && !string.IsNullOrWhiteSpace(embed.Video?.Url)) + { +
+ +
+ } + // Rich embed + else + { +
+ @{/* Color pill */} + @if (embed.Color is not null) + { +
+ } + else + { +
+ } -
-
-
- @{/* Embed author */} - @if (embed.Author is not null) - { -
- @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) - { - Author icon - } +
+
+
+ @{/* Embed author */} + @if (embed.Author is not null) + { +
+ @if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl)) + { + Author icon + } - @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) - { - if (!string.IsNullOrWhiteSpace(embed.Author.Url)) + @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) { - + if (!string.IsNullOrWhiteSpace(embed.Author.Url)) + { + +
@embed.Author.Name
+
+ } + else + {
@embed.Author.Name
+ } + } +
+ } + + @{/* Embed title */} + @if (!string.IsNullOrWhiteSpace(embed.Title)) + { +
+ @if (!string.IsNullOrWhiteSpace(embed.Url)) + { + +
@Raw(FormatEmbedMarkdown(embed.Title))
} else { -
@embed.Author.Name
+
@Raw(FormatEmbedMarkdown(embed.Title))
} - } -
- } +
+ } - @{/* Embed title */} - @if (!string.IsNullOrWhiteSpace(embed.Title)) - { -
- @if (!string.IsNullOrWhiteSpace(embed.Url)) - { - -
@Raw(FormatEmbedMarkdown(embed.Title))
-
- } - else - { -
@Raw(FormatEmbedMarkdown(embed.Title))
- } -
- } + @{/* Embed description */} + @if (!string.IsNullOrWhiteSpace(embed.Description)) + { +
+
@Raw(FormatEmbedMarkdown(embed.Description))
+
+ } - @{/* Embed description */} - @if (!string.IsNullOrWhiteSpace(embed.Description)) - { -
-
@Raw(FormatEmbedMarkdown(embed.Description))
-
- } + @{/* Embed fields */} + @if (embed.Fields.Any()) + { +
+ @foreach (var field in embed.Fields) + { +
+ @if (!string.IsNullOrWhiteSpace(field.Name)) + { +
+
@Raw(FormatEmbedMarkdown(field.Name))
+
+ } + + @if (!string.IsNullOrWhiteSpace(field.Value)) + { +
+
@Raw(FormatEmbedMarkdown(field.Value))
+
+ } +
+ } +
+ } +
- @{/* Embed fields */} - @if (embed.Fields.Any()) + @{/* Embed content */} + @if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url)) { -
- @foreach (var field in embed.Fields) - { -
- @if (!string.IsNullOrWhiteSpace(field.Name)) - { -
-
@Raw(FormatEmbedMarkdown(field.Name))
-
- } - - @if (!string.IsNullOrWhiteSpace(field.Value)) - { -
-
@Raw(FormatEmbedMarkdown(field.Value))
-
- } -
- } + }
- @{/* Embed content */} - @if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url)) + @{/* Embed images */} + @if (embed.Images.Any()) { -
- - Thumbnail - +
+ @foreach (var image in embed.Images) + { + if (!string.IsNullOrWhiteSpace(image.Url)) + { +
+ + Image + +
+ } + }
} -
- @{/* Embed images */} - @if (embed.Images.Any()) - { -
- @foreach (var image in embed.Images) - { - if (!string.IsNullOrWhiteSpace(image.Url)) + @{/* Embed footer & icon */} + @if (embed.Footer is not null || embed.Timestamp is not null) + { + - } - @{/* Embed footer & icon */} - @if (embed.Footer is not null || embed.Timestamp is not null) - { - - } + @{/* Embed timestamp */} + @if (embed.Timestamp is not null) + { + @FormatDate(embed.Timestamp.Value) + } + +
+ } +
-
+ } } - } - @{/* Stickers */} - @foreach (var sticker in message.Stickers) - { -
- @if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated) - { - Sticker - } - else if (sticker.Format == StickerFormat.Lottie) - { -
- } -
- } + @{/* Stickers */} + @foreach (var sticker in message.Stickers) + { +
+ @if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated) + { + Sticker + } + else if (sticker.Format == StickerFormat.Lottie) + { +
+ } +
+ } - @{/* Message reactions */} - @if (message.Reactions.Any()) - { -
- @foreach (var reaction in message.Reactions) - { -
- @reaction.Emoji.Name - @reaction.Count -
- } -
- } -
+ @{/* Message reactions */} + @if (message.Reactions.Any()) + { +
+ @foreach (var reaction in message.Reactions) + { +
+ @reaction.Emoji.Name + @reaction.Count +
+ } +
+ } +
+ }
} diff --git a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml index b7dc92c..97379a3 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/Writers/Html/PreambleTemplate.cshtml @@ -254,19 +254,18 @@ unicode-bidi: bidi-override; } - .chatlog__system-message{ - color: #96989D + .chatlog__system-notification { + color: @Themed("rgb(150, 152, 157)", "rgb(94, 103, 114)") } - .chatlog__system-message-reference-link{ + .chatlog__system-notification-reference-link { font-weight: 500; color: #ffffff } - .chatlog__system-message-icon{ + .chatlog__system-notification-icon { width: 18px; height: 18px; - margin-right: 0.25rem; } .chatlog__header { @@ -804,22 +803,19 @@ - - - - + + + + - - + + - + - - - - + diff --git a/DiscordChatExporter.Core/Exporting/Writers/HtmlMessageWriter.cs b/DiscordChatExporter.Core/Exporting/Writers/HtmlMessageWriter.cs index afb26dc..b821639 100644 --- a/DiscordChatExporter.Core/Exporting/Writers/HtmlMessageWriter.cs +++ b/DiscordChatExporter.Core/Exporting/Writers/HtmlMessageWriter.cs @@ -27,29 +27,43 @@ internal class HtmlMessageWriter : MessageWriter private bool CanJoinGroup(Message message) { - // First message in the group can always join - if(_messageGroup.LastOrDefault() is not { } lastMessage) - { + // If the group is empty, any message can join it + if (_messageGroup.LastOrDefault() is not { } lastMessage) return true; - } - // Group system messages with other system messages, regardless of author - if (message.Kind.IsSystemMessage()) + // Reply messages cannot join existing groups because they need to appear first + if (message.Kind == MessageKind.Reply) + return false; + + // Grouping for system notifications + if (message.Kind.IsSystemNotification()) + { + // Can only be grouped with other system notifications + if (!lastMessage.Kind.IsSystemNotification()) + return false; + } + // Grouping for normal messages + else { - return lastMessage.Kind.IsSystemMessage(); + // Can only be grouped with other normal messages + if (lastMessage.Kind.IsSystemNotification()) + return false; + + // Messages must be within 7 minutes of each other + if ((message.Timestamp - lastMessage.Timestamp).Duration().TotalMinutes > 7) + return false; + + // Messages must be from the same author + if (message.Author.Id != lastMessage.Author.Id) + return false; + + // If the user changed their name after the last message, their new messages + // cannot join an existing group. + if (!string.Equals(message.Author.FullName, lastMessage.Author.FullName, StringComparison.Ordinal)) + return false; } - return - // Must be a non system message - !message.Kind.IsSystemMessage() && - // 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; + return true; } // Use to preserve blocks of code inside the templates diff --git a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs index 336b7db..8868355 100644 --- a/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs +++ b/DiscordChatExporter.Core/Utils/Extensions/StringExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; namespace DiscordChatExporter.Core.Utils.Extensions; @@ -25,6 +26,9 @@ public static class StringExtensions } } + public static string ToDashCase(this string str) => + Regex.Replace(str, @"(\p{Ll})(\p{Lu})", "$1-$2"); + public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) => builder.Length > 0 ? builder.Append(value)