R1 cleanup: collAnon and dead code gone, no IndexedDB, honest PWA files
Build / Build (push) Successful in 51s

Starts the completion program (SocialPub docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17).

- The localizer's namespace is decePubClient.Services; collAnon is gone from the repo.
- Dead code: PagesBase, VConstants, SettingsOptions, ViewLanguage, unused APIs routes, InstanceService.Self,
  RequestStats.LastOf/AverageMs, PostAnon/DeleteTotallyAnon, unused AppStatusService methods and their main.js helpers
  (the app badge stays for Web Push), ExtensionMethods/SUtility leftovers (Markdig and BlazorZXingJs packages go),
  unused PublicCacheData/PageSettings/AppConfiguration fields, commented-out code.
- The client log in IndexedDB, which nothing ever read, goes with the whole IndexedDB layer (DnetIndexedDb, IStorage):
  logs go to the browser console; main.js deletes the old "data" database once.
- The solution names the real ClientModels project. Unused vendor assets (fontawesome, solid, open-iconic, rxjs) go.
- robots.txt disallows everything; manifest.json and index.html say decePub; the service worker's cache name follows
  each publish's assets version, so old caches are purged.
- MessageType is Visibility (member names kept: they are resx keys); ExpandMessage.razor is Thread.razor (same route).
- i18n: dead keys removed, Italian "camera" is "punto di vista", DInputBase/DCalendar validation messages come from
  ErrorMessages, DataTransfer's spinner has its label.
- CLAUDE.md follows: client storage, JS interop, PWA, the D fields' messages.

Verified: dotnet build decePubClient.csproj and decePubClient.sln -c Release (0 errors, no new warnings); a Pasture
publish served by the pasture's decepub peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 02:56:57 +02:00
1 parent e4414f1ac9
commit fd2f2e7c65
79 files changed
+155 -6673

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
@page "/expand/{statusId}"
@attribute [Authorize]
@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>
@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)
{
<Content @key="post.Key" Post="post" CssContainer="ml-6 max-w-[calc(100%-1.5rem)]" FilterIn="FilterContext.Thread" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
}
<Content @key="current.Key" Post="current" Highlighted="true" IncludeExpand="false" OnDeleted="Remove" OnReplied="Replied" OnTag="OpenTag" />
@foreach (var post in descendants)
{
<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>
@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<FeedPost> ancestors = [];
List<FeedPost> 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<FeedFilter>(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();
}
}