2026-10-04 11:03:14 +02:00
|
|
|
@page "/expand/{statusId}"
|
|
|
|
|
@attribute [Authorize]
|
2022-12-15 20:03:40 +01:00
|
|
|
@inherits LocalizableComponentBase
|
2026-10-04 11:03:14 +02:00
|
|
|
<Title>@Localizer["Thread"]</Title>
|
|
|
|
|
|
|
|
|
|
@* A post with its thread (/api/v1/statuses/{id}/context): the posts it answers above, the post, the answers below.
|
2026-10-05 03:10:27 +02:00
|
|
|
The cards do their own actions; a reply or a deletion updates the thread here. The persona's thread filters apply
|
2026-10-05 06:20:18 +02:00
|
|
|
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. *@
|
2026-10-04 11:03:14 +02:00
|
|
|
<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">
|
|
|
|
|
<DButton Size="DSize.Sm" OnClick="Back" title="@Localizer["Back"]">
|
|
|
|
|
<i class="ion-md-arrow-back"></i>
|
|
|
|
|
</DButton>
|
|
|
|
|
<h1 class="font-bold">@Localizer["Thread"]</h1>
|
|
|
|
|
</div>
|
2022-02-14 01:51:37 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
@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 (current is null)
|
|
|
|
|
{
|
|
|
|
|
<p class="w-full text-center text-lg">
|
|
|
|
|
<i class="ion-ios-remove-circle-outline"></i> @Localizer["This message doesn't exist."]
|
|
|
|
|
</p>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
@foreach (var post in ancestors)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-05 03:10:27 +02:00
|
|
|
<Content @key="post.Key" Post="post" CssContainer="ml-6 w-auto" FilterIn="FilterContext.Thread" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
<Content @key="current.Key" Post="current" Highlighted="true" IncludeExpand="false" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
@foreach (var post in descendants)
|
|
|
|
|
{
|
2026-10-05 03:10:27 +02:00
|
|
|
<Content @key="post.Key" Post="post" CssContainer="ml-6 w-auto" FilterIn="FilterContext.Thread" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
|
2026-10-04 11:03:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
2022-02-14 01:51:37 +01:00
|
|
|
|
|
|
|
|
@code {
|
2026-10-04 11:03:14 +02:00
|
|
|
[Inject] StatusService StatusService { get; set; }
|
|
|
|
|
[Inject] InstanceService InstanceService { get; set; }
|
2022-02-14 01:51:52 +01:00
|
|
|
[Inject] ILocalStorageService LocalStorage { get; set; }
|
2026-10-04 11:03:14 +02:00
|
|
|
[Inject] IJSRuntime JS { get; set; }
|
2022-02-14 01:51:37 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
[Parameter] public string StatusId { get; set; }
|
|
|
|
|
|
2026-10-05 06:20:18 +02:00
|
|
|
// 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)];
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
FeedPost current;
|
|
|
|
|
List<FeedPost> ancestors = [];
|
|
|
|
|
List<FeedPost> descendants = [];
|
|
|
|
|
string loadedId;
|
2026-10-05 06:20:18 +02:00
|
|
|
CancellationTokenSource rereading;
|
2022-02-14 01:51:37 +01:00
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
protected override async Task OnParametersSetAsync()
|
2022-02-14 01:51:37 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
if (StatusId == loadedId)
|
2022-02-14 01:51:37 +01:00
|
|
|
return;
|
2026-10-04 11:03:14 +02:00
|
|
|
loadedId = StatusId;
|
2022-02-14 01:51:37 +01:00
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
if (CascadingState.IsOffline)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-03 15:26:21 +02:00
|
|
|
WhenOnline(LoadData);
|
|
|
|
|
StartOnlinePolling();
|
|
|
|
|
IsLoading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
await LoadData();
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
async Task LoadData()
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-05 06:20:18 +02:00
|
|
|
rereading?.Cancel();
|
|
|
|
|
rereading?.Dispose();
|
|
|
|
|
rereading = null;
|
2026-10-04 11:03:14 +02:00
|
|
|
IsLoading = true;
|
|
|
|
|
var status = await StatusService.Get(StatusId);
|
|
|
|
|
if (status is null)
|
2026-10-03 15:26:21 +02:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
current = null;
|
|
|
|
|
IsLoading = false;
|
2026-10-03 15:26:21 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
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;
|
2022-02-14 01:51:52 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
var all = ancestors.Append(current).Concat(descendants).ToList();
|
|
|
|
|
await InstanceService.Resolve(all.Select(p => p.Host).Append(InstanceService.SelfHost));
|
|
|
|
|
InstanceService.Locate(all);
|
2026-10-05 06:20:18 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
void Remove(FeedPost post)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
if (post.Key == current?.Key)
|
2026-10-03 15:26:21 +02:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
current = null;
|
2026-10-03 15:26:21 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-10-04 11:03:14 +02:00
|
|
|
ancestors.RemoveAll(p => p.Key == post.Key);
|
|
|
|
|
descendants.RemoveAll(p => p.Key == post.Key);
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
async Task Replied(FeedPost reply)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
if (reply is null)
|
2026-10-03 15:26:21 +02:00
|
|
|
return;
|
2026-10-04 11:03:14 +02:00
|
|
|
descendants.Add(reply);
|
|
|
|
|
await InstanceService.Resolve([reply.Host]);
|
|
|
|
|
InstanceService.Locate([reply]);
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
async Task OpenTag(string tag)
|
2022-02-14 01:51:52 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
var filter = await LocalStorage.GetItemAsync<FeedFilter>(nameof(FeedFilter)) ?? new();
|
|
|
|
|
filter.Tag = tag;
|
|
|
|
|
filter.TimelineType = TimelineType.Tag;
|
|
|
|
|
await LocalStorage.SetItemAsync(nameof(FeedFilter), filter);
|
|
|
|
|
Navigation.NavigateTo("");
|
2022-02-14 01:51:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
async Task Back() => await JS.InvokeVoidAsync("history.back");
|
2026-10-05 06:20:18 +02:00
|
|
|
|
|
|
|
|
public override async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
if (rereading is not null)
|
|
|
|
|
{
|
|
|
|
|
await rereading.CancelAsync();
|
|
|
|
|
rereading.Dispose();
|
|
|
|
|
}
|
|
|
|
|
await base.DisposeAsync();
|
|
|
|
|
}
|
2026-10-03 15:26:21 +02:00
|
|
|
}
|