Uses markdown in post as rich text

This commit is contained in:
LittleSheep 2025-05-05 01:07:10 +08:00
parent 1c361b94f3
commit 7e7c8fe556
8 changed files with 31 additions and 109 deletions

View File

@ -135,6 +135,7 @@ public class AppDatabase(
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Post.Post>() modelBuilder.Entity<Post.Post>()
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })
.HasIndex(p => p.SearchVector) .HasIndex(p => p.SearchVector)
.HasMethod("GIN"); .HasMethod("GIN");
modelBuilder.Entity<Post.Post>() modelBuilder.Entity<Post.Post>()
@ -199,7 +200,7 @@ public class AppDatabase(
.HasForeignKey(m => m.ForwardedMessageId) .HasForeignKey(m => m.ForwardedMessageId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Chat.Message>() modelBuilder.Entity<Chat.Message>()
.HasOne(m => m.RepliedMessage) .HasOne(m => m.RepliedMessage)
.WithMany() .WithMany()
.HasForeignKey(m => m.RepliedMessageId) .HasForeignKey(m => m.RepliedMessageId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);

View File

@ -16,7 +16,7 @@ using NpgsqlTypes;
namespace DysonNetwork.Sphere.Migrations namespace DysonNetwork.Sphere.Migrations
{ {
[DbContext(typeof(AppDatabase))] [DbContext(typeof(AppDatabase))]
[Migration("20250503124624_InitialMigration")] [Migration("20250504170705_InitialMigration")]
partial class InitialMigration partial class InitialMigration
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1080,8 +1080,8 @@ namespace DysonNetwork.Sphere.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Content") b.Property<string>("Content")
.HasColumnType("jsonb") .HasColumnType("text")
.HasColumnName("content"); .HasColumnName("content");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
@ -1131,8 +1131,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("replied_post_id"); .HasColumnName("replied_post_id");
b.Property<NpgsqlTsVector>("SearchVector") b.Property<NpgsqlTsVector>("SearchVector")
.IsRequired()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("tsvector") .HasColumnType("tsvector")
.HasColumnName("search_vector"); .HasColumnName("search_vector")
.HasAnnotation("Npgsql:TsVectorConfig", "simple")
.HasAnnotation("Npgsql:TsVectorProperties", new[] { "Title", "Description", "Content" });
b.Property<long?>("ThreadedPostId") b.Property<long?>("ThreadedPostId")
.HasColumnType("bigint") .HasColumnType("bigint")

View File

@ -709,7 +709,7 @@ namespace DysonNetwork.Sphere.Migrations
edited_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), edited_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
published_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), published_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
visibility = table.Column<int>(type: "integer", nullable: false), visibility = table.Column<int>(type: "integer", nullable: false),
content = table.Column<JsonDocument>(type: "jsonb", nullable: true), content = table.Column<string>(type: "text", nullable: true),
type = table.Column<int>(type: "integer", nullable: false), type = table.Column<int>(type: "integer", nullable: false),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
views_unique = table.Column<int>(type: "integer", nullable: false), views_unique = table.Column<int>(type: "integer", nullable: false),
@ -719,7 +719,9 @@ namespace DysonNetwork.Sphere.Migrations
threaded_post_id = table.Column<long>(type: "bigint", nullable: true), threaded_post_id = table.Column<long>(type: "bigint", nullable: true),
replied_post_id = table.Column<long>(type: "bigint", nullable: true), replied_post_id = table.Column<long>(type: "bigint", nullable: true),
forwarded_post_id = table.Column<long>(type: "bigint", nullable: true), forwarded_post_id = table.Column<long>(type: "bigint", nullable: true),
search_vector = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: true), search_vector = table.Column<NpgsqlTsVector>(type: "tsvector", nullable: false)
.Annotation("Npgsql:TsVectorConfig", "simple")
.Annotation("Npgsql:TsVectorProperties", new[] { "title", "description", "content" }),
publisher_id = table.Column<long>(type: "bigint", nullable: false), publisher_id = table.Column<long>(type: "bigint", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),

View File

@ -1077,8 +1077,8 @@ namespace DysonNetwork.Sphere.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Content") b.Property<string>("Content")
.HasColumnType("jsonb") .HasColumnType("text")
.HasColumnName("content"); .HasColumnName("content");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
@ -1128,8 +1128,12 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("replied_post_id"); .HasColumnName("replied_post_id");
b.Property<NpgsqlTsVector>("SearchVector") b.Property<NpgsqlTsVector>("SearchVector")
.IsRequired()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("tsvector") .HasColumnType("tsvector")
.HasColumnName("search_vector"); .HasColumnName("search_vector")
.HasAnnotation("Npgsql:TsVectorConfig", "simple")
.HasAnnotation("Npgsql:TsVectorProperties", new[] { "Title", "Description", "Content" });
b.Property<long?>("ThreadedPostId") b.Property<long?>("ThreadedPostId")
.HasColumnType("bigint") .HasColumnType("bigint")

View File

@ -32,8 +32,9 @@ public class Post : ModelBase
public Instant? EditedAt { get; set; } public Instant? EditedAt { get; set; }
public Instant? PublishedAt { get; set; } public Instant? PublishedAt { get; set; }
public PostVisibility Visibility { get; set; } = PostVisibility.Public; public PostVisibility Visibility { get; set; } = PostVisibility.Public;
[Column(TypeName = "jsonb")] public JsonDocument? Content { get; set; } // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string? Content { get; set; }
public PostType Type { get; set; } public PostType Type { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
@ -51,8 +52,8 @@ public class Post : ModelBase
public long? ForwardedPostId { get; set; } public long? ForwardedPostId { get; set; }
public Post? ForwardedPost { get; set; } public Post? ForwardedPost { get; set; }
public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>(); public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>();
[JsonIgnore] public NpgsqlTsVector? SearchVector { get; set; } [JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!;
public Publisher Publisher { get; set; } = null!; public Publisher Publisher { get; set; } = null!;
public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>(); public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>();

View File

@ -119,7 +119,7 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService
{ {
[MaxLength(1024)] public string? Title { get; set; } [MaxLength(1024)] public string? Title { get; set; }
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
public JsonDocument? Content { get; set; } public string? Content { get; set; }
public PostVisibility? Visibility { get; set; } public PostVisibility? Visibility { get; set; }
public PostType? Type { get; set; } public PostType? Type { get; set; }
[MaxLength(16)] public List<string>? Tags { get; set; } [MaxLength(16)] public List<string>? Tags { get; set; }

View File

@ -10,57 +10,12 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act)
{ {
public static List<Post> TruncatePostContent(List<Post> input) public static List<Post> TruncatePostContent(List<Post> input)
{ {
// This truncate post content is designed for quill delta
const int maxLength = 256; const int maxLength = 256;
foreach (var item in input) foreach (var item in input)
{ {
if (item.Content is not { RootElement: var rootElement }) continue; if (!(item.Content?.Length > maxLength)) continue;
item.Content = item.Content[..maxLength];
if (rootElement.ValueKind != JsonValueKind.Array) continue; item.IsTruncated = true;
var totalLength = 0;
var truncatedArrayElements = new List<JsonElement>();
foreach (var element in rootElement.EnumerateArray())
{
if (element is { ValueKind: JsonValueKind.Object } &&
element.TryGetProperty("insert", out var insertProperty))
{
if (insertProperty is { ValueKind: JsonValueKind.String })
{
var textContent = insertProperty.GetString()!;
if (totalLength + textContent.Length <= maxLength)
{
truncatedArrayElements.Add(element);
totalLength += textContent.Length;
}
else
{
var remainingLength = maxLength - totalLength;
if (remainingLength > 0)
{
using var truncatedElementDocument =
JsonDocument.Parse(
$@"{{ ""insert"": ""{textContent.Substring(0, remainingLength)}"" }}"
);
truncatedArrayElements.Add(truncatedElementDocument.RootElement.Clone());
totalLength = maxLength;
}
break;
}
}
else
truncatedArrayElements.Add(element);
}
else
truncatedArrayElements.Add(element);
if (totalLength >= maxLength)
break;
}
var newDocument = JsonDocument.Parse(JsonSerializer.Serialize(truncatedArrayElements));
item.Content = newDocument;
} }
return input; return input;
@ -121,29 +76,6 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act)
throw new InvalidOperationException("Categories contains one or more categories that wasn't exists."); throw new InvalidOperationException("Categories contains one or more categories that wasn't exists.");
} }
// Vectorize the quill delta content
if (post.Content?.RootElement is { ValueKind: JsonValueKind.Array })
{
var searchTextBuilder = new System.Text.StringBuilder();
if (!string.IsNullOrWhiteSpace(post.Title))
searchTextBuilder.AppendLine(post.Title);
if (!string.IsNullOrWhiteSpace(post.Description))
searchTextBuilder.AppendLine(post.Description);
foreach (var element in post.Content.RootElement.EnumerateArray())
{
if (element is { ValueKind: JsonValueKind.Object } &&
element.TryGetProperty("insert", out var insertProperty) &&
insertProperty.ValueKind == JsonValueKind.String)
{
searchTextBuilder.Append(insertProperty.GetString());
}
}
post.SearchVector = EF.Functions.ToTsVector(searchTextBuilder.ToString().Trim());
}
// TODO Notify the subscribers // TODO Notify the subscribers
db.Posts.Add(post); db.Posts.Add(post);
@ -222,29 +154,6 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act)
throw new InvalidOperationException("Categories contains one or more categories that wasn't exists."); throw new InvalidOperationException("Categories contains one or more categories that wasn't exists.");
} }
// Vectorize the quill delta content
if (post.Content?.RootElement is { ValueKind: JsonValueKind.Array })
{
var searchTextBuilder = new System.Text.StringBuilder();
if (!string.IsNullOrWhiteSpace(post.Title))
searchTextBuilder.AppendLine(post.Title);
if (!string.IsNullOrWhiteSpace(post.Description))
searchTextBuilder.AppendLine(post.Description);
foreach (var element in post.Content.RootElement.EnumerateArray())
{
if (element is { ValueKind: JsonValueKind.Object } &&
element.TryGetProperty("insert", out var insertProperty) &&
insertProperty.ValueKind == JsonValueKind.String)
{
searchTextBuilder.Append(insertProperty.GetString());
}
}
post.SearchVector = EF.Functions.ToTsVector(searchTextBuilder.ToString().Trim());
}
db.Update(post); db.Update(post);
await db.SaveChangesAsync(); await db.SaveChangesAsync();

View File

@ -40,6 +40,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFoundResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0b5acdd962e549369896cece0026e556214600_003F28_003F290250f5_003FNotFoundResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFound_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff2c049af93e430aac427e8ff3cc9edd8763d5c9f006d7121ed1c5921585cba_003FNotFound_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFound_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff2c049af93e430aac427e8ff3cc9edd8763d5c9f006d7121ed1c5921585cba_003FNotFound_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANpgsqlEntityTypeBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fccb1faacaea4420db96b09857fc56178a1600_003Fd9_003F9acf9507_003FNpgsqlEntityTypeBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOk_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003F3b_003F237bf104_003FOk_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOk_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003F3b_003F237bf104_003FOk_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOptionsConfigurationServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6622dea924b14dc7aa3ee69d7c84e5735000_003Fe0_003F024ba0b7_003FOptionsConfigurationServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOptionsConfigurationServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6622dea924b14dc7aa3ee69d7c84e5735000_003Fe0_003F024ba0b7_003FOptionsConfigurationServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fd3_003F7b05b2bd_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fd3_003F7b05b2bd_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>