Files
decePubClient/Pages/ExpandMessage.razor
T

185 lines
5.7 KiB
Plaintext
Raw Normal View History

@page "/expand/{statusId}"
@attribute [Authorize]
2022-12-15 20:03:40 +01:00
@inherits LocalizableComponentBase
<Title>@Localizer["Thread"]</Title>
@* 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. *@
<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
@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-06 23:36:03 +02:00
<Content @key="post.Key" Post="post" CssContainer="ml-6 max-w-[calc(100%-1.5rem)]" FilterIn="FilterContext.Thread" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
2022-02-14 01:51:52 +01: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
@foreach (var post in descendants)
{
2026-10-06 23:36:03 +02:00
<Content @key="post.Key" Post="post" CssContainer="ml-6 max-w-[calc(100%-1.5rem)]" FilterIn="FilterContext.Thread" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
}
}
</div>
</section>
2022-02-14 01:51:37 +01:00
@code {
[Inject] StatusService StatusService { get; set; }
[Inject] InstanceService InstanceService { get; set; }
2022-02-14 01:51:52 +01:00
[Inject] ILocalStorageService LocalStorage { get; set; }
[Inject] IJSRuntime JS { get; set; }
2022-02-14 01:51:37 +01:00
[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<FeedPost> ancestors = [];
List<FeedPost> descendants = [];
string loadedId;
CancellationTokenSource rereading;
2022-02-14 01:51:37 +01:00
protected override async Task OnParametersSetAsync()
2022-02-14 01:51:37 +01:00
{
if (StatusId == loadedId)
2022-02-14 01:51:37 +01:00
return;
loadedId = StatusId;
2022-02-14 01:51:37 +01:00
if (CascadingState.IsOffline)
2022-02-14 01:51:52 +01:00
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
2022-02-14 01:51:52 +01:00
await LoadData();
2022-02-14 01:51:52 +01:00
}
async Task LoadData()
2022-02-14 01:51:52 +01:00
{
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;
2022-02-14 01:51:52 +01:00
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);
}
2022-02-14 01:51:52 +01:00
}
void Remove(FeedPost post)
2022-02-14 01:51:52 +01:00
{
if (post.Key == current?.Key)
{
current = null;
return;
}
ancestors.RemoveAll(p => p.Key == post.Key);
descendants.RemoveAll(p => p.Key == post.Key);
2022-02-14 01:51:52 +01:00
}
async Task Replied(FeedPost reply)
2022-02-14 01:51:52 +01:00
{
if (reply is null)
return;
descendants.Add(reply);
await InstanceService.Resolve([reply.Host]);
InstanceService.Locate([reply]);
2022-02-14 01:51:52 +01:00
}
async Task OpenTag(string tag)
2022-02-14 01:51:52 +01: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
}
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();
}
}