Files
decePubClient/Pages/Scheduled.razor
T
thepraandClaude Opus 5.5 6d5cf17723 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
2026-10-05 03:53:51 +02:00

145 lines
4.7 KiB
Plaintext

@page "/scheduled"
@attribute [Authorize]
@inherits LocalizableComponentBase
<Title>@Localizer["Scheduled posts"]</Title>
@* The current persona's posts waiting for their time (/api/v1/scheduled_statuses): moved to another time, or dropped so
they are never published. The composer schedules them. *@
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
<div class="flex items-center gap-2">
<i class="ion-md-time text-lg"></i>
<h1 class="font-bold truncate">@Localizer["Scheduled posts"]</h1>
</div>
@if (IsLoading)
{
<p class="flex w-full items-center justify-center gap-2">
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
@Localizer["loading"]
</p>
}
else if (scheduled.Count == 0)
{
<p class="w-full text-center" data-feed-empty><i class="ion-ios-remove-circle-outline"></i> @Localizer["Nothing is waiting to be published."]</p>
}
<div class="flex flex-col gap-3">
@foreach (var item in scheduled)
{
<article class="flex flex-col gap-2 p-3 md:p-4 rounded-xl neo-raised neo-xs" data-scheduled-id="@item.Id">
<div class="flex flex-wrap items-center gap-2 text-sm">
<i class="ion-md-time"></i>
<b>@item.ScheduledAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)</b>
<span class="opacity-70">@VisibilityName(item.Params.Visibility)</span>
@if (item.MediaAttachments.Count > 0)
{
<span class="opacity-70"><i class="ion-md-images"></i> @item.MediaAttachments.Count</span>
}
</div>
@if (item.Params.SpoilerText is { Length: > 0 } warning)
{
<p class="text-sm"><i class="ion-md-warning text-warning"></i> @warning</p>
}
<p class="text-sm whitespace-pre-wrap break-words">@item.Params.Text</p>
<div class="flex flex-wrap items-center gap-2">
<input type="datetime-local" class="input input-sm w-auto" autocomplete="off" aria-label="@Localizer["Publish it on"]"
value="@(moves.GetValueOrDefault(item.Id) ?? item.ScheduledAt.ToLocalTime().ToString("yyyy-MM-ddTHH:mm"))"
@onchange="@((ChangeEventArgs e) => moves[item.Id] = e.Value?.ToString())" />
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isSaving || !moves.ContainsKey(item.Id))" OnClick="@(() => Move(item))">
<i class="ion-md-checkmark"></i><span>@Localizer["Move"]</span>
</DButton>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isSaving)" OnClick="@(() => Drop(item))">
<i class="ion-md-trash text-error"></i><span>@Localizer["Never publish it"]</span>
</DButton>
</div>
</article>
}
</div>
</div>
</section>
@code {
[Inject] StatusService StatusService { get; set; }
List<ScheduledStatus> scheduled = [];
readonly Dictionary<string, string> moves = [];
string loadedPersonaId;
bool isSaving;
string VisibilityName(string visibility) => visibility switch
{
"unlisted" => Localizer["Unlisted"],
"private" => Localizer["Followers only"],
"direct" => Localizer["Direct"],
_ => Localizer["Public"]
};
// The persona changed (NavMenu's switcher): its scheduled posts are another persona's.
protected override async Task OnParametersSetAsync()
{
if (CascadingState.PersonaId is null)
{
IsLoading = false;
return;
}
if (CascadingState.PersonaId == loadedPersonaId)
return;
loadedPersonaId = CascadingState.PersonaId;
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
}
async Task LoadData()
{
IsLoading = true;
moves.Clear();
scheduled = await StatusService.Scheduled();
IsLoading = false;
}
async Task Move(ScheduledStatus item)
{
if (!DateTime.TryParse(moves[item.Id], System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var local))
return;
isSaving = true;
var result = await StatusService.Reschedule(item.Id, local.ToUniversalTime());
isSaving = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
var moved = (ScheduledStatus)result.Data;
moves.Remove(item.Id);
item.ScheduledAt = moved.ScheduledAt;
scheduled = [.. scheduled.OrderBy(s => s.ScheduledAt)];
Toast.AddResponseSuccess(Localizer["Scheduled for {0}.", moved.ScheduledAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)]);
}
async Task Drop(ScheduledStatus item)
{
isSaving = true;
var result = await StatusService.Unschedule(item.Id);
isSaving = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
scheduled.Remove(item);
Toast.AddResponseSuccess(Localizer["It will not be published."]);
}
}