Compare commits
2 Commits
217aaf2f79
...
16618cc135
Author | SHA1 | Date | |
---|---|---|---|
16618cc135 | |||
a7426b008b |
@ -25,6 +25,12 @@ internal sealed class BlogContext : DbContext
|
|||||||
/// <value>The collection of blog posts.</value>
|
/// <value>The collection of blog posts.</value>
|
||||||
public DbSet<BlogPost> BlogPosts { get; private set; } = null!;
|
public DbSet<BlogPost> BlogPosts { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of legacy comments in the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The collection of legacy comments.</value>
|
||||||
|
public DbSet<LegacyComment> LegacyComments { get; private set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of users in the database.
|
/// Gets the collection of users in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -43,6 +49,7 @@ internal sealed class BlogContext : DbContext
|
|||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
modelBuilder.ApplyConfiguration(new BlogPostConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new LegacyCommentConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Blog.Configuration;
|
||||||
|
|
||||||
|
internal sealed class LegacyCommentConfiguration : IEntityTypeConfiguration<LegacyComment>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(EntityTypeBuilder<LegacyComment> 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);
|
||||||
|
}
|
||||||
|
}
|
54
OliverBooth/Data/Blog/ILegacyComment.cs
Normal file
54
OliverBooth/Data/Blog/ILegacyComment.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a comment that was posted on a legacy comment framework.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILegacyComment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the PNG-encoded avatar of the author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The author's avatar.</value>
|
||||||
|
string? Avatar { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the comment's author.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The author's name.</value>
|
||||||
|
string Author { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the body of the comment.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The comment body.</value>
|
||||||
|
string Body { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the date and time at which this comment was posted.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The creation timestamp.</value>
|
||||||
|
DateTimeOffset CreatedAt { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of this comment.
|
||||||
|
/// </summary>
|
||||||
|
Guid Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the comment this comment is replying to.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The parent comment ID, or <see langword="null" /> if this comment is not a reply.</value>
|
||||||
|
Guid? ParentComment { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the post to which this comment was posted.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The post ID.</value>
|
||||||
|
Guid PostId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the avatar URL of the comment's author.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The avatar URL.</returns>
|
||||||
|
string GetAvatarUrl();
|
||||||
|
}
|
33
OliverBooth/Data/Blog/LegacyComment.cs
Normal file
33
OliverBooth/Data/Blog/LegacyComment.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Blog;
|
||||||
|
|
||||||
|
internal sealed class LegacyComment : ILegacyComment
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? Avatar { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Author { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Body { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTimeOffset CreatedAt { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid? ParentComment { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid PostId { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetAvatarUrl()
|
||||||
|
{
|
||||||
|
return Avatar ?? $"https://ui-avatars.com/api/?name={HttpUtility.UrlEncode(Author)}";
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
|
|||||||
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
|
builder.Property(e => e.PreviewImageUrl).HasConversion<UriToStringConverter>();
|
||||||
builder.Property(e => e.NextPart);
|
builder.Property(e => e.NextPart);
|
||||||
builder.Property(e => e.PreviousPart);
|
builder.Property(e => e.PreviousPart);
|
||||||
|
builder.Property(e => e.RedirectFrom).IsRequired(false);
|
||||||
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
||||||
builder.Property(e => e.EnableComments).IsRequired();
|
builder.Property(e => e.EnableComments).IsRequired();
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,12 @@ public interface ITutorialArticle
|
|||||||
/// <value>The publish timestamp.</value>
|
/// <value>The publish timestamp.</value>
|
||||||
DateTimeOffset Published { get; }
|
DateTimeOffset Published { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the post that was redirected to this article.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The source redirect post ID.</value>
|
||||||
|
Guid? RedirectFrom { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the slug of this article.
|
/// Gets the slug of this article.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -38,6 +38,9 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTimeOffset Published { get; private set; }
|
public DateTimeOffset Published { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid? RedirectFrom { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Slug { get; private set; } = string.Empty;
|
public string Slug { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
|
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
|
||||||
@using Humanizer
|
@using Humanizer
|
||||||
|
@using Markdig
|
||||||
@using OliverBooth.Data
|
@using OliverBooth.Data
|
||||||
@using OliverBooth.Data.Blog
|
@using OliverBooth.Data.Blog
|
||||||
@using OliverBooth.Services
|
@using OliverBooth.Services
|
||||||
@inject IBlogPostService BlogPostService
|
@inject IBlogPostService BlogPostService
|
||||||
|
@inject MarkdownPipeline MarkdownPipeline
|
||||||
@model Article
|
@model Article
|
||||||
|
|
||||||
@if (Model.ShowPasswordPrompt)
|
@if (Model.ShowPasswordPrompt)
|
||||||
@ -145,6 +147,57 @@
|
|||||||
async>
|
async>
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int commentCount = BlogPostService.GetLegacyCommentCount(post);
|
||||||
|
if (commentCount > 0)
|
||||||
|
{
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
var nestLevelMap = new Dictionary<ILegacyComment, int>();
|
||||||
|
IReadOnlyList<ILegacyComment> legacyComments = BlogPostService.GetLegacyComments(post);
|
||||||
|
var commentStack = new Stack<ILegacyComment>(legacyComments.OrderByDescending(c => c.CreatedAt));
|
||||||
|
<p class="text-center">
|
||||||
|
<strong>@("legacy comment".ToQuantity(commentCount))</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<sub>Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity.</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="legacy-comment" style="margin-left: @(padding)px;">
|
||||||
|
<img class="blog-author-icon" src="@comment.GetAvatarUrl()" alt="@comment.Author">
|
||||||
|
@comment.Author •
|
||||||
|
|
||||||
|
<abbr class="text-muted" data-bs-toggle="tooltip" data-bs-title="@comment.CreatedAt.ToString("dddd, d MMMM yyyy HH:mm")">
|
||||||
|
@comment.CreatedAt.Humanize()
|
||||||
|
</abbr>
|
||||||
|
|
||||||
|
<div class="comment">@Html.Raw(Markdown.ToHtml(comment.Body, MarkdownPipeline))</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
@page "/tutorial/{**slug}"
|
@page "/tutorial/{**slug}"
|
||||||
@using Humanizer
|
@using Humanizer
|
||||||
|
@using Markdig
|
||||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@using OliverBooth.Data
|
@using OliverBooth.Data
|
||||||
|
@using OliverBooth.Data.Blog
|
||||||
@using OliverBooth.Data.Web
|
@using OliverBooth.Data.Web
|
||||||
@using OliverBooth.Services
|
@using OliverBooth.Services
|
||||||
@inject ITutorialService TutorialService
|
@inject ITutorialService TutorialService
|
||||||
|
@inject MarkdownPipeline MarkdownPipeline
|
||||||
@model Article
|
@model Article
|
||||||
|
|
||||||
@if (Model.CurrentArticle is not { } article)
|
@if (Model.CurrentArticle is not { } article)
|
||||||
@ -117,6 +120,57 @@
|
|||||||
async>
|
async>
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int commentCount = TutorialService.GetLegacyCommentCount(article);
|
||||||
|
if (commentCount > 0)
|
||||||
|
{
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
var nestLevelMap = new Dictionary<ILegacyComment, int>();
|
||||||
|
IReadOnlyList<ILegacyComment> legacyComments = TutorialService.GetLegacyComments(article);
|
||||||
|
var commentStack = new Stack<ILegacyComment>(legacyComments.OrderByDescending(c => c.CreatedAt));
|
||||||
|
<p class="text-center">
|
||||||
|
<strong>@("legacy comment".ToQuantity(commentCount))</strong>
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<sub>Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity.</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="legacy-comment" style="margin-left: @(padding)px;">
|
||||||
|
<img class="blog-author-icon" src="@comment.GetAvatarUrl()" alt="@comment.Author">
|
||||||
|
@comment.Author •
|
||||||
|
|
||||||
|
<abbr class="text-muted" data-bs-toggle="tooltip" data-bs-title="@comment.CreatedAt.ToString("dddd, d MMMM yyyy HH:mm")">
|
||||||
|
@comment.CreatedAt.Humanize()
|
||||||
|
</abbr>
|
||||||
|
|
||||||
|
<div class="comment">@Html.Raw(Markdown.ToHtml(comment.Body, MarkdownPipeline))</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -67,6 +67,27 @@ internal sealed class BlogPostService : IBlogPostService
|
|||||||
.ToArray().Select(CacheAuthor).ToArray();
|
.ToArray().Select(CacheAuthor).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int GetLegacyCommentCount(IBlogPost post)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
return context.LegacyComments.Count(c => c.PostId == post.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ILegacyComment> GetLegacyComments(IBlogPost post)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
return context.LegacyComments.Where(c => c.PostId == post.Id && c.ParentComment == null).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment)
|
||||||
|
{
|
||||||
|
using BlogContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IBlogPost? GetNextPost(IBlogPost blogPost)
|
public IBlogPost? GetNextPost(IBlogPost blogPost)
|
||||||
{
|
{
|
||||||
|
@ -34,6 +34,27 @@ public interface IBlogPostService
|
|||||||
/// <returns>A collection of blog posts.</returns>
|
/// <returns>A collection of blog posts.</returns>
|
||||||
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10);
|
IReadOnlyList<IBlogPost> GetBlogPosts(int page, int pageSize = 10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of legacy comments for the specified post.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="post">The post whose legacy comments to count.</param>
|
||||||
|
/// <returns>The total number of legacy comments.</returns>
|
||||||
|
int GetLegacyCommentCount(IBlogPost post);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the collection of legacy comments for the specified post.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="post">The post whose legacy comments to retrieve.</param>
|
||||||
|
/// <returns>A read-only view of the legacy comments.</returns>
|
||||||
|
IReadOnlyList<ILegacyComment> GetLegacyComments(IBlogPost post);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the collection of replies to the specified legacy comment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comment">The comment whose replies to retrieve.</param>
|
||||||
|
/// <returns>A read-only view of the replies.</returns>
|
||||||
|
IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the next blog post from the specified blog post.
|
/// Returns the next blog post from the specified blog post.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Data.Web;
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
namespace OliverBooth.Services;
|
namespace OliverBooth.Services;
|
||||||
@ -60,6 +61,27 @@ public interface ITutorialService
|
|||||||
/// <exception cref="ArgumentNullException"><paramref name="article" /> is <see langword="null" />.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="article" /> is <see langword="null" />.</exception>
|
||||||
string GetFullSlug(ITutorialArticle article);
|
string GetFullSlug(ITutorialArticle article);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of legacy comments for the specified article.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="article">The article whose legacy comments to count.</param>
|
||||||
|
/// <returns>The total number of legacy comments.</returns>
|
||||||
|
int GetLegacyCommentCount(ITutorialArticle article);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the collection of legacy comments for the specified article.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="article">The article whose legacy comments to retrieve.</param>
|
||||||
|
/// <returns>A read-only view of the legacy comments.</returns>
|
||||||
|
IReadOnlyList<ILegacyComment> GetLegacyComments(ITutorialArticle article);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the collection of replies to the specified legacy comment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comment">The comment whose replies to retrieve.</param>
|
||||||
|
/// <returns>A read-only view of the replies.</returns>
|
||||||
|
IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Renders the body of the specified article.
|
/// Renders the body of the specified article.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -4,24 +4,30 @@ using Humanizer;
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Blog;
|
||||||
using OliverBooth.Data.Web;
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
namespace OliverBooth.Services;
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
internal sealed class TutorialService : ITutorialService
|
internal sealed class TutorialService : ITutorialService
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<BlogContext> _blogContextFactory;
|
||||||
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
||||||
private readonly MarkdownPipeline _markdownPipeline;
|
private readonly MarkdownPipeline _markdownPipeline;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TutorialService" /> class.
|
/// Initializes a new instance of the <see cref="TutorialService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dbContextFactory">The <see cref="IDbContextFactory{TContext}" />.</param>
|
/// <param name="dbContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
|
/// <param name="blogContextFactory">The <see cref="BlogContext" /> factory.</param>
|
||||||
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
||||||
public TutorialService(IDbContextFactory<WebContext> dbContextFactory, MarkdownPipeline markdownPipeline)
|
public TutorialService(IDbContextFactory<WebContext> dbContextFactory,
|
||||||
|
IDbContextFactory<BlogContext> blogContextFactory,
|
||||||
|
MarkdownPipeline markdownPipeline)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_markdownPipeline = markdownPipeline;
|
_markdownPipeline = markdownPipeline;
|
||||||
|
_blogContextFactory = blogContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -103,6 +109,37 @@ internal sealed class TutorialService : ITutorialService
|
|||||||
return $"{GetFullSlug(folder)}/{article.Slug}";
|
return $"{GetFullSlug(folder)}/{article.Slug}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ILegacyComment> GetLegacyComments(ITutorialArticle article)
|
||||||
|
{
|
||||||
|
if (article.RedirectFrom is not { } postId)
|
||||||
|
{
|
||||||
|
return ArraySegment<ILegacyComment>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
using BlogContext context = _blogContextFactory.CreateDbContext();
|
||||||
|
return context.LegacyComments.Where(c => c.PostId == postId && c.ParentComment == null).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment)
|
||||||
|
{
|
||||||
|
using BlogContext context = _blogContextFactory.CreateDbContext();
|
||||||
|
return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string RenderArticle(ITutorialArticle article)
|
public string RenderArticle(ITutorialArticle article)
|
||||||
{
|
{
|
||||||
|
@ -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 {
|
.mastodon-update-card.card {
|
||||||
background-color: desaturate(darken(#6364FF, 50%), 50%);
|
background-color: desaturate(darken(#6364FF, 50%), 50%);
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
|
Loading…
Reference in New Issue
Block a user