Posts for later, hashtags followed, and Explore

The composer's clock button gives a post a time (scheduled_at, at least five
minutes ahead; located and group posts have none), and /scheduled moves or
drops what waits. A hashtag's feed tab shows its week and follows it, so its
public posts come home. Explore (/explore) shows what PrivaPub says is
trending, hashtags, posts and links from public posts it holds, the
directory by activity or by joining, and the hashtags the persona follows.
DiscoveryService does the calls; the e2e tests check each against PrivaPub's
own answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 03:53:51 +02:00
1 parent 9a752d9288
commit 6d5cf17723
17 files changed
+1001 -7

No files matched your search

+50 -3
View File
@@ -2,7 +2,8 @@
@* A new post or a reply, as the current persona. Through the Mastodon API it is plain text with a content warning, a
visibility and media (each uploaded first, with its alt text). A located post is text only, kept within its range of
this device's position and never federated; a group post goes to the group. Both of those go through /clientapi. *@
this device's position and never federated; a group post goes to the group. Both of those go through /clientapi.
A Mastodon API post can wait for a time instead (scheduled_at, at least five minutes ahead); /scheduled lists those. *@
<EditForm Model="form" OnValidSubmit="Publish" class="flex flex-col gap-3">
<DataAnnotationsValidator />
@@ -121,6 +122,13 @@
<i class="ion-md-locate text-base"></i>
</DButton>
}
@if (!form.IsLocated && GroupId is null)
{
<DButton Size="DSize.Sm" Active="form.IsScheduled" OnClick="ToggleScheduled" title="@Localizer["Publish later"]" data-schedule>
<i class="ion-md-time text-base"></i>
</DButton>
}
</div>
<DButton Size="DSize.Sm" Type="submit" Color="DColor.Primary" Disabled="IsDefaultDisabled || isPublishing || (form.IsLocated && form.Latitude is null)">
@@ -128,10 +136,22 @@
{
<DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" />
}
<span>@Localizer[InReplyTo is null ? "Post" : "Reply"]</span>
<i class="ion-md-send"></i>
<span>@(form.IsScheduled ? Localizer["Schedule"] : Localizer[InReplyTo is null ? "Post" : "Reply"])</span>
<i class="@(form.IsScheduled ? "ion-md-time" : "ion-md-send")"></i>
</DButton>
</div>
@if (form.IsScheduled && !form.IsLocated && GroupId is null)
{
<div class="flex flex-wrap items-center gap-2 text-sm">
<label class="flex flex-wrap items-center gap-2">
<span>@Localizer["Publish it on"]</span>
<DInput TValue="string" type="datetime-local" Size="DSize.Sm" class="w-auto" @bind-Value="form.ScheduledAt" autocomplete="off"
min="@DateTime.Now.AddMinutes(6).ToString("yyyy-MM-ddTHH:mm")" data-schedule-at />
</label>
<a class="link text-xs" href="scheduled">@Localizer["Scheduled posts"]</a>
</div>
}
</EditForm>
@code {
@@ -198,6 +218,14 @@
form.IsScopeOptionsOpen = false;
}
// a scheduled post starts an hour ahead, which the reader moves as it likes
void ToggleScheduled()
{
form.IsScheduled = !form.IsScheduled;
if (form.IsScheduled && string.IsNullOrEmpty(form.ScheduledAt))
form.ScheduledAt = DateTime.Now.AddHours(1).ToString("yyyy-MM-ddTHH:mm");
}
async Task ToggleLocated()
{
form.IsLocated = !form.IsLocated;
@@ -275,6 +303,17 @@
return;
}
if (form.IsScheduled && form.ScheduledAtUtc is not { } at)
{
error = Localizer["Choose when to publish it."];
return;
}
if (form.ScheduledAtUtc < DateTime.UtcNow.AddMinutes(5))
{
error = Localizer["A post is scheduled at least five minutes ahead."];
return;
}
isPublishing = true;
var result = await StatusService.Publish(form, CascadingState.PersonaId);
isPublishing = false;
@@ -284,6 +323,14 @@
return;
}
if (result.Data is ScheduledStatus scheduled)
{
Toast.AddResponseSuccess(Localizer["Scheduled for {0}.", scheduled.ScheduledAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)]);
form = new() { GroupId = GroupId };
await OnPublished.InvokeAsync(null);
return;
}
Toast.AddResponseSuccess(form.IsLocated ? Localizer["Located post published."] : Localizer["Published."]);
var published = result.Data as FeedPost;
form = new() { GroupId = GroupId };