From e88f4afc456ba52a8890ff1d3a7b4c214d61b2ff Mon Sep 17 00:00:00 2001 From: thepra Date: Wed, 7 Oct 2026 02:14:53 +0200 Subject: [PATCH] A located post's author deletes it A located post's card had no action at all, so whoever wrote one (with this device's position) could not take it back from the app. The persona's own located card now has Delete, which asks first, without the line about other servers (a located post never left PrivaPub), and goes through /clientapi/post/delete, since the Mastodon API looks past located posts. The globe's end-to-end test deletes the post it writes this way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- CLAUDE.md | 2 +- ClientJsonContext.cs | 1 + Components/Content.razor | 25 ++++++++++++++++++------- Models/APIs.cs | 1 + Services/StatusService.cs | 11 +++++++++++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d245d48..c4b6d93 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,7 +285,7 @@ supplies: `/clientapi/post/nearby` and have no Mastodon actions), `StatusService` (publish, favourite, boost, bookmark, delete, threads, mute, block, poll votes, emoji reactions through Pleroma's `/api/v1/pleroma/statuses/:id/reactions`, joining and leaving remote events through `/api/privapub/v1/statuses/:id/join|leave`; located and group posts go through - `/clientapi/post/insert`), `AccountService` (profiles, relationships, follow, + `/clientapi/post/insert`, and a located post's author deletes it through `/clientapi/post/delete`, its card's one action), `AccountService` (profiles, relationships, follow, block, mute, follow requests, `/api/v2/search`, whose handles and URLs PrivaPub resolves), `ListService` (the persona's lists and their members; members are only accounts it follows, found with `accounts/search?following=true`), `FilterService` (the persona's v2 filters, their keywords and filtered posts), `DiscoveryService` (hashtags and diff --git a/ClientJsonContext.cs b/ClientJsonContext.cs index d4b14d2..9f978ed 100644 --- a/ClientJsonContext.cs +++ b/ClientJsonContext.cs @@ -49,6 +49,7 @@ namespace decePubClient; [JsonSerializable(typeof(ViewPost))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(InsertPostForm))] +[JsonSerializable(typeof(DeletePostForm))] [JsonSerializable(typeof(ViewReport))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(DomainBlockForm))] diff --git a/Components/Content.razor b/Components/Content.razor index 9fc8522..87d5b37 100644 --- a/Components/Content.razor +++ b/Components/Content.razor @@ -7,7 +7,7 @@ quoted post (Compact) has none, since it is not an item of the feed. A CDN-fronted server's post has no point, only its CDN's group (data-cdn); data-post-at is when it was written, for the globe's Sun; data-uri is its ActivityPub id, which the e2e tests (tests/e2e) find it by. The persona's filters apply where the page says the card is (FilterIn): a - hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media. Located posts have no actions: PrivaPub keeps them off the Mastodon API. Nerd stats are on + hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media. Located posts have no Mastodon actions (PrivaPub keeps them off the Mastodon API); their author deletes them through /clientapi. Nerd stats are on the time, the author and its server's history, the place or the CDN, the media and the counts. *@ @if (!IsFilteredOut) { @@ -211,10 +211,20 @@ @if (Post.Located) { -

- - @Localizer["{0} km from you, shown within {1} km", Number(Post.DistanceKm), Number(Post.RangeKm)] -

+
+

+ + @Localizer["{0} km from you, shown within {1} km", Number(Post.DistanceKm), Number(Post.RangeKm)] +

+ @* its author takes it back here: it has no other action *@ + @if (IsOwn && !Compact) + { + + + @Localizer["Delete"] + + } +
} @if (status.Privapub?.Approval is "pending" or "rejected") @@ -949,11 +959,12 @@ async Task Delete() { - if (!await Confirm.Ask(Localizer["Delete this post?"], Localizer["It goes from here, and the servers that have it are asked to delete it too."], + // a located post never left PrivaPub: no server is asked + if (!await Confirm.Ask(Localizer["Delete this post?"], Post.Located ? null : Localizer["It goes from here, and the servers that have it are asked to delete it too."], Localizer["Delete"], true)) return; isActing = true; - var result = await StatusService.Delete(Status.Id); + var result = Post.Located ? await StatusService.DeleteLocated(Status.Id, CascadingState.PersonaId) : await StatusService.Delete(Status.Id); isActing = false; if (result.IsInvalid) { diff --git a/Models/APIs.cs b/Models/APIs.cs index 6882a2c..29d3f41 100644 --- a/Models/APIs.cs +++ b/Models/APIs.cs @@ -33,6 +33,7 @@ public static class APIs public const string GET_Posts = "clientapi/post/list"; public const string GET_Nearby = "clientapi/post/nearby"; public const string POST_InsertPost = "clientapi/post/insert"; + public const string POST_DeletePost = "clientapi/post/delete"; public const string GET_ModeratorReports = "clientapi/moderator/reports"; public static string POST_M_ResolveReport(string reportId) => $"clientapi/moderator/reports/{Uri.EscapeDataString(reportId)}/resolve"; public const string GET_AdminDomainBlocks = "clientapi/admin/domainblocks/list"; diff --git a/Services/StatusService.cs b/Services/StatusService.cs index 8b53241..4485d82 100644 --- a/Services/StatusService.cs +++ b/Services/StatusService.cs @@ -228,6 +228,17 @@ namespace decePubClient.Services return await StatusResult(response, cancellationToken); } + /// Deletes the persona's own located post: through /clientapi, since the Mastodon API looks past located posts. + public async Task DeleteLocated(string postId, string personaId, CancellationToken cancellationToken = default) + { + using var response = await httpService.Post(APIs.ClientApi.POST_DeletePost, new DeletePostForm { AvatarId = personaId, PostId = postId }, + cancellationToken: cancellationToken); + if (!response.IsSuccessStatusCode) + return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken); + + return new WebResult(); + } + public async Task Get(string statusId, CancellationToken cancellationToken = default) { using var response = await httpService.Get(APIs.Mastodon.GET_M_Status(statusId), cancellationToken: cancellationToken);