From d9d48c3cbf2959297d6f92707a96a1eeb6aa474c Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 14:52:31 +0100 Subject: [PATCH] feat: flag management --- .../Controllers/FlagsController.cs | 15 +- .../20241209134148_NullableFlagHash.cs | 41 ++ .../DatabaseContextModelSnapshot.cs | 5 +- Foxnouns.Backend/Database/Models/PrideFlag.cs | 4 +- Foxnouns.Backend/Dto/Flag.cs | 2 +- .../Jobs/CreateDataExportInvocable.cs | 6 + Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 25 +- .../Services/MemberRendererService.cs | 3 +- .../Services/UserRendererService.cs | 3 +- Foxnouns.Frontend/src/lib/api/models/meta.ts | 1 + Foxnouns.Frontend/src/lib/api/models/user.ts | 2 +- .../src/lib/components/ClientPaginator.svelte | 37 ++ .../components/editor/EditorFlagImage.svelte | 17 + .../lib/components/editor/FlagEditor.svelte | 52 +++ .../lib/components/profile/ProfileFlag.svelte | 3 +- .../src/lib/i18n/locales/en.json | 378 +++++++++--------- Foxnouns.Frontend/src/lib/index.ts | 1 + Foxnouns.Frontend/src/lib/paginate.ts | 35 ++ .../src/routes/@[username]/+page.server.ts | 26 +- .../src/routes/settings/flags/+page.server.ts | 42 ++ .../src/routes/settings/flags/+page.svelte | 121 ++++++ .../routes/settings/members/+page.server.ts | 17 +- Foxnouns.Frontend/static/unknown_flag.svg | 9 + README.md | 5 + 24 files changed, 615 insertions(+), 235 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs create mode 100644 Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/paginate.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/flags/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/flags/+page.svelte create mode 100644 Foxnouns.Frontend/static/unknown_flag.svg diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 14705fb..4bc947b 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -46,13 +46,22 @@ public class FlagsController( ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); - Snowflake id = snowflakeGenerator.GenerateSnowflake(); + var flag = new PrideFlag + { + Id = snowflakeGenerator.GenerateSnowflake(), + UserId = CurrentUser!.Id, + Name = req.Name, + Description = req.Description, + }; + + db.Add(flag); + await db.SaveChangesAsync(); queue.QueueInvocableWithPayload( - new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description) + new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Name, req.Image, req.Description) ); - return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); + return Accepted(userRenderer.RenderPrideFlag(flag)); } [HttpPatch("{id}")] diff --git a/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs b/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs new file mode 100644 index 0000000..12d84ff --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + [DbContext(typeof(DatabaseContext))] + [Migration("20241209134148_NullableFlagHash")] + public partial class NullableFlagHash : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "hash", + table: "pride_flags", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text" + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "hash", + table: "pride_flags", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true + ); + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 79a9855..4bd1ede 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -282,7 +282,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnName("description"); b.Property("Hash") - .IsRequired() .HasColumnType("text") .HasColumnName("hash"); @@ -546,7 +545,7 @@ namespace Foxnouns.Backend.Database.Migrations modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => { b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany() + .WithMany("DataExports") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() @@ -645,6 +644,8 @@ namespace Foxnouns.Backend.Database.Migrations { b.Navigation("AuthMethods"); + b.Navigation("DataExports"); + b.Navigation("Flags"); b.Navigation("Members"); diff --git a/Foxnouns.Backend/Database/Models/PrideFlag.cs b/Foxnouns.Backend/Database/Models/PrideFlag.cs index 45a3a7c..d5437f8 100644 --- a/Foxnouns.Backend/Database/Models/PrideFlag.cs +++ b/Foxnouns.Backend/Database/Models/PrideFlag.cs @@ -3,7 +3,9 @@ namespace Foxnouns.Backend.Database.Models; public class PrideFlag : BaseModel { public required Snowflake UserId { get; init; } - public required string Hash { get; init; } + + // A null hash means the flag hasn't been processed yet. + public string? Hash { get; set; } public required string Name { get; set; } public string? Description { get; set; } } diff --git a/Foxnouns.Backend/Dto/Flag.cs b/Foxnouns.Backend/Dto/Flag.cs index 649154a..882613a 100644 --- a/Foxnouns.Backend/Dto/Flag.cs +++ b/Foxnouns.Backend/Dto/Flag.cs @@ -4,7 +4,7 @@ using Foxnouns.Backend.Utils; namespace Foxnouns.Backend.Dto; -public record PrideFlagResponse(Snowflake Id, string ImageUrl, string Name, string? Description); +public record PrideFlagResponse(Snowflake Id, string? ImageUrl, string Name, string? Description); public record CreateFlagRequest(string Name, string Image, string? Description); diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index 8e34d01..c07ea7f 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -108,6 +108,12 @@ public class CreateDataExportInvocable( private async Task WritePrideFlag(ZipArchive zip, PrideFlag flag) { + if (flag.Hash == null) + { + _logger.Debug("Flag {FlagId} has a null hash, ignoring it", flag.Id); + return; + } + _logger.Debug("Writing flag {FlagId}", flag.Id); var flagData = $""" diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 5c5df2d..93a4e0c 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; +using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Jobs; @@ -26,6 +27,18 @@ public class CreateFlagInvocable( try { + PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => + f.Id == Payload.Id && f.UserId == Payload.UserId + ); + if (flag == null) + { + _logger.Warning( + "Got a flag create job for {FlagId} but it doesn't exist, aborting", + Payload.Id + ); + return; + } + (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( Payload.ImageData, 256, @@ -33,16 +46,8 @@ public class CreateFlagInvocable( ); await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); - var flag = new PrideFlag - { - Id = Payload.Id, - UserId = Payload.UserId, - Hash = hash, - Name = Payload.Name, - Description = Payload.Description, - }; - db.Add(flag); - + flag.Hash = hash; + db.Update(flag); await db.SaveChangesAsync(); _logger.Information("Uploaded flag {FlagId} with hash {Hash}", flag.Id, flag.Hash); diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 8661088..06de060 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -83,7 +83,8 @@ public class MemberRendererService(DatabaseContext db, Config config) ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + private string? ImageUrlFor(PrideFlag flag) => + flag.Hash != null ? $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp" : null; private PrideFlagResponse RenderPrideFlag(PrideFlag flag) => new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 3d4ca51..6f33583 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -131,7 +131,8 @@ public class UserRendererService( ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + public string? ImageUrlFor(PrideFlag flag) => + flag.Hash != null ? $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp" : null; public PrideFlagResponse RenderPrideFlag(PrideFlag flag) => new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); diff --git a/Foxnouns.Frontend/src/lib/api/models/meta.ts b/Foxnouns.Frontend/src/lib/api/models/meta.ts index eb77a31..56f31c9 100644 --- a/Foxnouns.Frontend/src/lib/api/models/meta.ts +++ b/Foxnouns.Frontend/src/lib/api/models/meta.ts @@ -17,4 +17,5 @@ export type Limits = { bio_length: number; custom_preferences: number; max_auth_methods: number; + max_flags: number; }; diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts index d2deb8f..f830983 100644 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -66,7 +66,7 @@ export type Field = { export type PrideFlag = { id: string; - image_url: string; + image_url: string | null; name: string; description: string | null; }; diff --git a/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte b/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte new file mode 100644 index 0000000..7f72ca2 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte @@ -0,0 +1,37 @@ + + +{#if pageCount > 1} +
+ + + (currentPage = 0)} /> + + + (currentPage = prevPage)} /> + + {#each new Array(pageCount) as _, page} + + (currentPage = page)}>{page + 1} + + {/each} + + (currentPage = nextPage)} /> + + + (currentPage = pageCount - 1)} /> + + +
+{/if} diff --git a/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte b/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte new file mode 100644 index 0000000..45875b8 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte @@ -0,0 +1,17 @@ + + +{flag.description + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte new file mode 100644 index 0000000..8b542da --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte @@ -0,0 +1,52 @@ + + +
+ + {flag.description + +
+ + +
+ + +
+
+
+ + diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte index bf171cd..b3d1611 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte @@ -1,5 +1,6 @@ + +

{$t("settings.flag-title")}

+ + + +
+ +

{$t("settings.flag-upload-title")}

+ + + + + + +

+ {$t("settings.flag-current-flags-title", { + count: data.flags.length, + max: data.meta.limits.max_flags, + })} +

+ + + + + {#each arr as flag (flag.id)} + + + + {flag.name} + + + {#if lastEditedFlag === flag.id}{/if} + + + {/each} + + + diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts index 220865b..715610b 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts @@ -1,18 +1,15 @@ +import paginate from "$lib/paginate"; + const MEMBERS_PER_PAGE = 15; export const load = async ({ url, parent }) => { const { user } = await parent(); - let currentPage = Number(url.searchParams.get("page") || "0"); - let pageCount = Math.ceil(user.members.length / MEMBERS_PER_PAGE); - let members = user.members.slice( - currentPage * MEMBERS_PER_PAGE, - (currentPage + 1) * MEMBERS_PER_PAGE, + const { data, currentPage, pageCount } = paginate( + user.members, + url.searchParams.get("page"), + MEMBERS_PER_PAGE, ); - if (members.length === 0) { - members = user.members.slice(0, MEMBERS_PER_PAGE); - currentPage = 0; - } - return { members, currentPage, pageCount }; + return { members: data, currentPage, pageCount }; }; diff --git a/Foxnouns.Frontend/static/unknown_flag.svg b/Foxnouns.Frontend/static/unknown_flag.svg new file mode 100644 index 0000000..bbf82ce --- /dev/null +++ b/Foxnouns.Frontend/static/unknown_flag.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index ea28fc8..146591f 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,8 @@ Code taken entirely or almost entirely from external sources: taken from [PluralKit](https://github.com/PluralKit/PluralKit/blob/32a6e97342acc3b35e6f9e7b4dd169e21d888770/PluralKit.Core/Database/Functions/functions.sql) - `Foxnouns.Backend/Database/prune-designer-cs-files.sh`, taken from [Iceshrimp.NET](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/commit/7c93dcf79dda54fc1a4ea9772e3f80874e6bcefb/Iceshrimp.Backend/Core/Database/prune-designer-cs-files.sh) + +Files under a different license: + +- `Foxnouns.Frontend/static/unknown_flag.svg` is https://commons.wikimedia.org/wiki/File:Unknown_flag.svg, + by 8938e on Wikimedia Commons, licensed as CC BY-SA 4.0. \ No newline at end of file