Deleting a post, taking it off the persona's wall, muting or blocking an account (from a card or a profile), leaving a group, dropping a scheduled post and signing out acted at the first tap, a few pixels from their neighbours. They now ask first: new ConfirmService (Ask returns whether the reader confirmed, as PublicNoticeService does) and ConfirmHost in MainLayout, a sheet from the bottom on a phone and a dialog from md, with Cancel and the action named, in red when it deletes or cuts off. Logout is a button that asks, then goes to /logout as before. The reversible ones (unmute, unblock, removing a conversation from the list, a picture from the composer) don't ask; the inline confirms of lists, filters and the login's deletion stay. The desktop tests that delete a reply, take a post off a wall or drop a scheduled post confirm; a phone flow deletes a post after cancelling once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
148 lines
4.9 KiB
Plaintext
148 lines
4.9 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; }
|
|
[Inject] ConfirmService Confirm { 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)
|
|
{
|
|
if (!await Confirm.Ask(Localizer["Never publish this post?"], null, Localizer["Never publish it"], true))
|
|
return;
|
|
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."]);
|
|
}
|
|
}
|