@Localizer["Thread"]
@* A post with its thread (/api/v1/statuses/{id}/context): the posts it answers above, the post, the answers below.
The cards do their own actions; a reply or a deletion updates the thread here. The persona's thread filters apply
to the posts around the one it opened, never to that one. Opening a remote thread makes PrivaPub read its replies
from their servers, so the thread is read twice more, a few seconds later, and what was found joins it. *@
@Localizer["Thread"]
@if (IsLoading)
{
@Localizer["loading"]
}
else if (current is null)
{
@Localizer["This message doesn't exist."]
}
else
{
@foreach (var post in ancestors)
{
}
@foreach (var post in descendants)
{
}
}
@code {
[Inject] StatusService StatusService { get; set; }
[Inject] InstanceService InstanceService { get; set; }
[Inject] ILocalStorageService LocalStorage { get; set; }
[Inject] IJSRuntime JS { get; set; }
[Parameter] public string StatusId { get; set; }
// when the thread is read again, after PrivaPub has had time to fetch the replies held on other servers
static readonly TimeSpan[] Rereads = [TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(15)];
FeedPost current;
List ancestors = [];
List descendants = [];
string loadedId;
CancellationTokenSource rereading;
protected override async Task OnParametersSetAsync()
{
if (StatusId == loadedId)
return;
loadedId = StatusId;
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
}
async Task LoadData()
{
rereading?.Cancel();
rereading?.Dispose();
rereading = null;
IsLoading = true;
var status = await StatusService.Get(StatusId);
if (status is null)
{
current = null;
IsLoading = false;
return;
}
var context = await StatusService.Context(status.Id);
current = FeedPost.Of(status);
ancestors = [.. (context?.Ancestors ?? []).Select(FeedPost.Of)];
descendants = [.. (context?.Descendants ?? []).Select(FeedPost.Of)];
IsLoading = false;
var all = ancestors.Append(current).Concat(descendants).ToList();
await InstanceService.Resolve(all.Select(p => p.Host).Append(InstanceService.SelfHost));
InstanceService.Locate(all);
if (new[] { current, ancestors.FirstOrDefault() }.Any(p => p is not null && p.Host != InstanceService.SelfHost))
{
rereading = new();
_ = Reread(status.Id, rereading.Token);
}
}
// the answers PrivaPub found since, in the thread's order; the cards already shown stay as they are, and one the
// thread no longer lists (a reply just made, not yet in it) stays at the end
async Task Reread(string statusId, CancellationToken cancellationToken)
{
foreach (var pause in Rereads)
{
try
{
await Task.Delay(pause, cancellationToken);
}
catch (OperationCanceledException)
{
return;
}
var context = await StatusService.Context(statusId, cancellationToken);
if (cancellationToken.IsCancellationRequested || context is null)
return;
var shown = descendants.ToDictionary(p => p.Key);
var found = context.Descendants.Where(s => !shown.ContainsKey(s.Id)).Select(FeedPost.Of).ToList();
if (found.Count == 0)
continue;
var listed = context.Descendants.Select(s => s.Id).ToHashSet();
descendants = [.. context.Descendants.Select(s => shown.GetValueOrDefault(s.Id) ?? found.First(p => p.Key == s.Id)),
.. descendants.Where(p => !listed.Contains(p.Key))];
await InstanceService.Resolve(found.Select(p => p.Host));
InstanceService.Locate(found);
await InvokeAsync(StateHasChanged);
}
}
void Remove(FeedPost post)
{
if (post.Key == current?.Key)
{
current = null;
return;
}
ancestors.RemoveAll(p => p.Key == post.Key);
descendants.RemoveAll(p => p.Key == post.Key);
}
async Task Replied(FeedPost reply)
{
if (reply is null)
return;
descendants.Add(reply);
await InstanceService.Resolve([reply.Host]);
InstanceService.Locate([reply]);
}
async Task OpenTag(string tag)
{
var filter = await LocalStorage.GetItemAsync(nameof(FeedFilter)) ?? new();
filter.Tag = tag;
filter.TimelineType = TimelineType.Tag;
await LocalStorage.SetItemAsync(nameof(FeedFilter), filter);
Navigation.NavigateTo("");
}
async Task Back() => await JS.InvokeVoidAsync("history.back");
public override async ValueTask DisposeAsync()
{
if (rereading is not null)
{
await rereading.CancelAsync();
rereading.Dispose();
}
await base.DisposeAsync();
}
}