From a7426b008b4e480a12bf239b8233d0b4a6c24884 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Wed, 1 May 2024 16:47:31 +0100 Subject: [PATCH] feat: show legacy disqus comments beneath gisqus No Disqus connection here. I just saved them to DB --- OliverBooth/Data/Blog/BlogContext.cs | 7 +++ .../LegacyCommentConfiguration.cs | 21 ++++++++ OliverBooth/Data/Blog/ILegacyComment.cs | 54 +++++++++++++++++++ OliverBooth/Data/Blog/LegacyComment.cs | 33 ++++++++++++ .../TutorialArticleConfiguration.cs | 1 + OliverBooth/Data/Web/ITutorialArticle.cs | 6 +++ OliverBooth/Data/Web/TutorialArticle.cs | 3 ++ OliverBooth/Pages/Blog/Article.cshtml | 53 ++++++++++++++++++ OliverBooth/Pages/Tutorials/Article.cshtml | 54 +++++++++++++++++++ OliverBooth/Services/BlogPostService.cs | 21 ++++++++ OliverBooth/Services/IBlogPostService.cs | 21 ++++++++ OliverBooth/Services/ITutorialService.cs | 22 ++++++++ OliverBooth/Services/TutorialService.cs | 41 +++++++++++++- src/scss/app.scss | 32 +++++++++++ 14 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 OliverBooth/Data/Blog/Configuration/LegacyCommentConfiguration.cs create mode 100644 OliverBooth/Data/Blog/ILegacyComment.cs create mode 100644 OliverBooth/Data/Blog/LegacyComment.cs diff --git a/OliverBooth/Data/Blog/BlogContext.cs b/OliverBooth/Data/Blog/BlogContext.cs index ed9f56f..398bd66 100644 --- a/OliverBooth/Data/Blog/BlogContext.cs +++ b/OliverBooth/Data/Blog/BlogContext.cs @@ -25,6 +25,12 @@ internal sealed class BlogContext : DbContext /// The collection of blog posts. public DbSet BlogPosts { get; private set; } = null!; + /// + /// Gets the collection of legacy comments in the database. + /// + /// The collection of legacy comments. + public DbSet LegacyComments { get; private set; } = null!; + /// /// Gets the collection of users in the database. /// @@ -43,6 +49,7 @@ internal sealed class BlogContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new BlogPostConfiguration()); + modelBuilder.ApplyConfiguration(new LegacyCommentConfiguration()); modelBuilder.ApplyConfiguration(new UserConfiguration()); } } diff --git a/OliverBooth/Data/Blog/Configuration/LegacyCommentConfiguration.cs b/OliverBooth/Data/Blog/Configuration/LegacyCommentConfiguration.cs new file mode 100644 index 0000000..5c1c100 --- /dev/null +++ b/OliverBooth/Data/Blog/Configuration/LegacyCommentConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace OliverBooth.Data.Blog.Configuration; + +internal sealed class LegacyCommentConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("LegacyComment"); + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id).IsRequired(); + builder.Property(e => e.PostId).IsRequired(); + builder.Property(e => e.Author).IsRequired().HasMaxLength(50); + builder.Property(e => e.Avatar).IsRequired(false).HasMaxLength(32767); + builder.Property(e => e.Body).IsRequired().HasMaxLength(32767); + builder.Property(e => e.ParentComment).IsRequired(false); + } +} diff --git a/OliverBooth/Data/Blog/ILegacyComment.cs b/OliverBooth/Data/Blog/ILegacyComment.cs new file mode 100644 index 0000000..2f43dc2 --- /dev/null +++ b/OliverBooth/Data/Blog/ILegacyComment.cs @@ -0,0 +1,54 @@ +namespace OliverBooth.Data.Blog; + +/// +/// Represents a comment that was posted on a legacy comment framework. +/// +public interface ILegacyComment +{ + /// + /// Gets the PNG-encoded avatar of the author. + /// + /// The author's avatar. + string? Avatar { get; } + + /// + /// Gets the name of the comment's author. + /// + /// The author's name. + string Author { get; } + + /// + /// Gets the body of the comment. + /// + /// The comment body. + string Body { get; } + + /// + /// Gets the date and time at which this comment was posted. + /// + /// The creation timestamp. + DateTimeOffset CreatedAt { get; } + + /// + /// Gets the ID of this comment. + /// + Guid Id { get; } + + /// + /// Gets the ID of the comment this comment is replying to. + /// + /// The parent comment ID, or if this comment is not a reply. + Guid? ParentComment { get; } + + /// + /// Gets the ID of the post to which this comment was posted. + /// + /// The post ID. + Guid PostId { get; } + + /// + /// Gets the avatar URL of the comment's author. + /// + /// The avatar URL. + string GetAvatarUrl(); +} diff --git a/OliverBooth/Data/Blog/LegacyComment.cs b/OliverBooth/Data/Blog/LegacyComment.cs new file mode 100644 index 0000000..a020979 --- /dev/null +++ b/OliverBooth/Data/Blog/LegacyComment.cs @@ -0,0 +1,33 @@ +using System.Web; + +namespace OliverBooth.Data.Blog; + +internal sealed class LegacyComment : ILegacyComment +{ + /// + public string? Avatar { get; private set; } + + /// + public string Author { get; private set; } = string.Empty; + + /// + public string Body { get; private set; } = string.Empty; + + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + public Guid Id { get; private set; } + + /// + public Guid? ParentComment { get; private set; } + + /// + public Guid PostId { get; private set; } + + /// + public string GetAvatarUrl() + { + return Avatar ?? $"https://ui-avatars.com/api/?name={HttpUtility.UrlEncode(Author)}"; + } +} diff --git a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs index c5c0ea1..b86a5b5 100644 --- a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs @@ -24,6 +24,7 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration e.PreviewImageUrl).HasConversion(); builder.Property(e => e.NextPart); builder.Property(e => e.PreviousPart); + builder.Property(e => e.RedirectFrom).IsRequired(false); builder.Property(e => e.Visibility).HasConversion>(); builder.Property(e => e.EnableComments).IsRequired(); } diff --git a/OliverBooth/Data/Web/ITutorialArticle.cs b/OliverBooth/Data/Web/ITutorialArticle.cs index 5e2f6c6..a53b289 100644 --- a/OliverBooth/Data/Web/ITutorialArticle.cs +++ b/OliverBooth/Data/Web/ITutorialArticle.cs @@ -67,6 +67,12 @@ public interface ITutorialArticle /// The publish timestamp. DateTimeOffset Published { get; } + /// + /// Gets the ID of the post that was redirected to this article. + /// + /// The source redirect post ID. + Guid? RedirectFrom { get; } + /// /// Gets the slug of this article. /// diff --git a/OliverBooth/Data/Web/TutorialArticle.cs b/OliverBooth/Data/Web/TutorialArticle.cs index 52fbb90..43ef096 100644 --- a/OliverBooth/Data/Web/TutorialArticle.cs +++ b/OliverBooth/Data/Web/TutorialArticle.cs @@ -38,6 +38,9 @@ internal sealed class TutorialArticle : IEquatable, ITutorialAr /// public DateTimeOffset Published { get; private set; } + /// + public Guid? RedirectFrom { get; private set; } + /// public string Slug { get; private set; } = string.Empty; diff --git a/OliverBooth/Pages/Blog/Article.cshtml b/OliverBooth/Pages/Blog/Article.cshtml index fe6be41..d29588b 100644 --- a/OliverBooth/Pages/Blog/Article.cshtml +++ b/OliverBooth/Pages/Blog/Article.cshtml @@ -1,9 +1,11 @@ @page "/blog/{year:int}/{month:int}/{day:int}/{slug}" @using Humanizer +@using Markdig @using OliverBooth.Data @using OliverBooth.Data.Blog @using OliverBooth.Services @inject IBlogPostService BlogPostService +@inject MarkdownPipeline MarkdownPipeline @model Article @if (Model.ShowPasswordPrompt) @@ -145,6 +147,57 @@ async> } + + int commentCount = BlogPostService.GetLegacyCommentCount(post); + if (commentCount > 0) + { +
+ + var nestLevelMap = new Dictionary(); + IReadOnlyList legacyComments = BlogPostService.GetLegacyComments(post); + var commentStack = new Stack(legacyComments.OrderByDescending(c => c.CreatedAt)); +

+ @("legacy comment".ToQuantity(commentCount)) +

+

+ Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity. +

+ + while (commentStack.Count > 0) + { + ILegacyComment comment = commentStack.Pop(); + foreach (ILegacyComment reply in BlogPostService.GetLegacyReplies(comment).OrderByDescending(c => c.CreatedAt)) + { + if (nestLevelMap.TryGetValue(comment, out int currentLevel)) + { + nestLevelMap[reply] = currentLevel + 1; + } + else + { + nestLevelMap[reply] = 1; + } + + commentStack.Push(reply); + } + + int padding = 0; + if (nestLevelMap.TryGetValue(comment, out int nestLevel)) + { + padding = 50 * nestLevel; + } + +
+ @comment.Author + @comment.Author • + + + @comment.CreatedAt.Humanize() + + +
@Html.Raw(Markdown.ToHtml(comment.Body, MarkdownPipeline))
+
+ } + } } else { diff --git a/OliverBooth/Pages/Tutorials/Article.cshtml b/OliverBooth/Pages/Tutorials/Article.cshtml index 7919c37..2970273 100644 --- a/OliverBooth/Pages/Tutorials/Article.cshtml +++ b/OliverBooth/Pages/Tutorials/Article.cshtml @@ -1,10 +1,13 @@ @page "/tutorial/{**slug}" @using Humanizer +@using Markdig @using Microsoft.AspNetCore.Mvc.TagHelpers @using OliverBooth.Data +@using OliverBooth.Data.Blog @using OliverBooth.Data.Web @using OliverBooth.Services @inject ITutorialService TutorialService +@inject MarkdownPipeline MarkdownPipeline @model Article @if (Model.CurrentArticle is not { } article) @@ -117,6 +120,57 @@ async> } + + int commentCount = TutorialService.GetLegacyCommentCount(article); + if (commentCount > 0) + { +
+ + var nestLevelMap = new Dictionary(); + IReadOnlyList legacyComments = TutorialService.GetLegacyComments(article); + var commentStack = new Stack(legacyComments.OrderByDescending(c => c.CreatedAt)); +

+ @("legacy comment".ToQuantity(commentCount)) +

+

+ Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity. +

+ + while (commentStack.Count > 0) + { + ILegacyComment comment = commentStack.Pop(); + foreach (ILegacyComment reply in TutorialService.GetLegacyReplies(comment).OrderByDescending(c => c.CreatedAt)) + { + if (nestLevelMap.TryGetValue(comment, out int currentLevel)) + { + nestLevelMap[reply] = currentLevel + 1; + } + else + { + nestLevelMap[reply] = 1; + } + + commentStack.Push(reply); + } + + int padding = 0; + if (nestLevelMap.TryGetValue(comment, out int nestLevel)) + { + padding = 50 * nestLevel; + } + +
+ @comment.Author + @comment.Author • + + + @comment.CreatedAt.Humanize() + + +
@Html.Raw(Markdown.ToHtml(comment.Body, MarkdownPipeline))
+
+ } + } } else { diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs index 049cd6e..38985d3 100644 --- a/OliverBooth/Services/BlogPostService.cs +++ b/OliverBooth/Services/BlogPostService.cs @@ -67,6 +67,27 @@ internal sealed class BlogPostService : IBlogPostService .ToArray().Select(CacheAuthor).ToArray(); } + /// + public int GetLegacyCommentCount(IBlogPost post) + { + using BlogContext context = _dbContextFactory.CreateDbContext(); + return context.LegacyComments.Count(c => c.PostId == post.Id); + } + + /// + public IReadOnlyList GetLegacyComments(IBlogPost post) + { + using BlogContext context = _dbContextFactory.CreateDbContext(); + return context.LegacyComments.Where(c => c.PostId == post.Id && c.ParentComment == null).ToArray(); + } + + /// + public IReadOnlyList GetLegacyReplies(ILegacyComment comment) + { + using BlogContext context = _dbContextFactory.CreateDbContext(); + return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray(); + } + /// public IBlogPost? GetNextPost(IBlogPost blogPost) { diff --git a/OliverBooth/Services/IBlogPostService.cs b/OliverBooth/Services/IBlogPostService.cs index bcb48ee..5947f19 100644 --- a/OliverBooth/Services/IBlogPostService.cs +++ b/OliverBooth/Services/IBlogPostService.cs @@ -34,6 +34,27 @@ public interface IBlogPostService /// A collection of blog posts. IReadOnlyList GetBlogPosts(int page, int pageSize = 10); + /// + /// Returns the number of legacy comments for the specified post. + /// + /// The post whose legacy comments to count. + /// The total number of legacy comments. + int GetLegacyCommentCount(IBlogPost post); + + /// + /// Returns the collection of legacy comments for the specified post. + /// + /// The post whose legacy comments to retrieve. + /// A read-only view of the legacy comments. + IReadOnlyList GetLegacyComments(IBlogPost post); + + /// + /// Returns the collection of replies to the specified legacy comment. + /// + /// The comment whose replies to retrieve. + /// A read-only view of the replies. + IReadOnlyList GetLegacyReplies(ILegacyComment comment); + /// /// Returns the next blog post from the specified blog post. /// diff --git a/OliverBooth/Services/ITutorialService.cs b/OliverBooth/Services/ITutorialService.cs index a5bd547..1366172 100644 --- a/OliverBooth/Services/ITutorialService.cs +++ b/OliverBooth/Services/ITutorialService.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using OliverBooth.Data; +using OliverBooth.Data.Blog; using OliverBooth.Data.Web; namespace OliverBooth.Services; @@ -60,6 +61,27 @@ public interface ITutorialService /// is . string GetFullSlug(ITutorialArticle article); + /// + /// Returns the number of legacy comments for the specified article. + /// + /// The article whose legacy comments to count. + /// The total number of legacy comments. + int GetLegacyCommentCount(ITutorialArticle article); + + /// + /// Returns the collection of legacy comments for the specified article. + /// + /// The article whose legacy comments to retrieve. + /// A read-only view of the legacy comments. + IReadOnlyList GetLegacyComments(ITutorialArticle article); + + /// + /// Returns the collection of replies to the specified legacy comment. + /// + /// The comment whose replies to retrieve. + /// A read-only view of the replies. + IReadOnlyList GetLegacyReplies(ILegacyComment comment); + /// /// Renders the body of the specified article. /// diff --git a/OliverBooth/Services/TutorialService.cs b/OliverBooth/Services/TutorialService.cs index c6e1f7d..3fd21ed 100644 --- a/OliverBooth/Services/TutorialService.cs +++ b/OliverBooth/Services/TutorialService.cs @@ -4,24 +4,30 @@ using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; using OliverBooth.Data; +using OliverBooth.Data.Blog; using OliverBooth.Data.Web; namespace OliverBooth.Services; internal sealed class TutorialService : ITutorialService { + private readonly IDbContextFactory _blogContextFactory; private readonly IDbContextFactory _dbContextFactory; private readonly MarkdownPipeline _markdownPipeline; /// /// Initializes a new instance of the class. /// - /// The . + /// The factory. + /// The factory. /// The . - public TutorialService(IDbContextFactory dbContextFactory, MarkdownPipeline markdownPipeline) + public TutorialService(IDbContextFactory dbContextFactory, + IDbContextFactory blogContextFactory, + MarkdownPipeline markdownPipeline) { _dbContextFactory = dbContextFactory; _markdownPipeline = markdownPipeline; + _blogContextFactory = blogContextFactory; } /// @@ -103,6 +109,37 @@ internal sealed class TutorialService : ITutorialService return $"{GetFullSlug(folder)}/{article.Slug}"; } + /// + public int GetLegacyCommentCount(ITutorialArticle article) + { + if (article.RedirectFrom is not { } postId) + { + return 0; + } + + using BlogContext context = _blogContextFactory.CreateDbContext(); + return context.LegacyComments.Count(c => c.PostId == postId); + } + + /// + public IReadOnlyList GetLegacyComments(ITutorialArticle article) + { + if (article.RedirectFrom is not { } postId) + { + return ArraySegment.Empty; + } + + using BlogContext context = _blogContextFactory.CreateDbContext(); + return context.LegacyComments.Where(c => c.PostId == postId && c.ParentComment == null).ToArray(); + } + + /// + public IReadOnlyList GetLegacyReplies(ILegacyComment comment) + { + using BlogContext context = _blogContextFactory.CreateDbContext(); + return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray(); + } + /// public string RenderArticle(ITutorialArticle article) { diff --git a/src/scss/app.scss b/src/scss/app.scss index ce9f79c..3833241 100644 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -429,6 +429,38 @@ td.trim-p p:last-child { } } +.legacy-comment { + font-size: 14px !important; + + .blog-author-icon { + height: 28px; + } + + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + .comment { + font-size: 14px !important; + margin-left: 30px; + background: #1d1d1d; + padding: 10px; + border-radius: 5px; + + p:last-child { + margin-bottom: 0; + } + + blockquote.blockquote { + font-size: 14px !important; + border-left: 3px solid #687a86; + padding-left: 15px; + } + } +} + .mastodon-update-card.card { background-color: desaturate(darken(#6364FF, 50%), 50%); margin-bottom: 50px;