Lists: made on /lists, read as a tab of the feed

A persona's lists (PrivaPub's /api/v1/lists): made, renamed, given a
replies policy and made exclusive on /lists, filled from the accounts it
follows (accounts/search?following=true), and read as the feed's Lists tab.
A followed account's profile has a Lists menu to put it in or take it out.
ListService does the calls; the e2e tests check each step against
PrivaPub's own answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 02:49:51 +02:00
1 parent 011bafb6af
commit e2491de16e
18 files changed
+914 -9

No files matched your search

+62
View File
@@ -56,6 +56,30 @@
@FollowLabel
</DButton>
}
@if (relationship?.Following == true)
{
<DDropdown @bind-IsOpen="isListMenuOpen" Align="DDropdownAlign.End" TriggerClass="btn btn-sm"
ContentClass="mt-2 flex flex-col gap-2 p-3 rounded-box w-64 bg-base-100 neo-raised neo-xs z-20" title="@Localizer["Lists"]">
<Trigger>
<i class="ion-md-list"></i><span>@Localizer["Lists"]</span>
</Trigger>
<ChildContent>
@if (allLists.Count == 0)
{
<p class="text-xs opacity-70">@Localizer["This persona has no list yet."]</p>
}
@foreach (var list in allLists)
{
<label class="label text-sm" data-list-id="@list.Id">
<input type="checkbox" class="checkbox checkbox-sm" checked="@inLists.Contains(list.Id)" disabled="@(IsDefaultDisabled || isActing)"
@onchange="@((ChangeEventArgs e) => SetListMember(list, e.Value is true))" />
<span class="truncate">@list.Title</span>
</label>
}
<a class="btn btn-xs" href="lists"><i class="ion-md-create"></i><span>@Localizer["Manage lists"]</span></a>
</ChildContent>
</DDropdown>
}
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing || relationship is null)"
OnClick="@(() => Do(relationship?.Muting == true ? AccountAction.Unmute : AccountAction.Mute))">
<i class="@(relationship?.Muting == true ? "ion-md-eye" : "ion-md-eye-off")"></i>
@@ -140,6 +164,7 @@
@code {
[Inject] AccountService AccountService { get; set; }
[Inject] ListService ListService { get; set; }
[Inject] InstanceService InstanceService { get; set; }
[Inject] ILocalStorageService LocalStorage { get; set; }
[Inject] IJSRuntime JS { get; set; }
@@ -155,6 +180,9 @@
string loadedPersonaId;
bool isActing;
bool isLoadingMore;
bool isListMenuOpen;
List<MastodonList> allLists = [];
HashSet<string> inLists = [];
bool IsOwn => account is not null && account.Id == CascadingState.PersonaId;
@@ -197,12 +225,44 @@
return;
}
relationship = account.Id == CascadingState.PersonaId ? null : await AccountService.Relationship(account.Id);
await LoadLists();
posts = [];
maxId = null;
IsLoading = false;
await LoadPage();
}
/// <summary>The persona's lists and which of them hold the account, for an account the persona follows.</summary>
async Task LoadLists()
{
if (relationship?.Following != true)
{
allLists = [];
inLists = [];
return;
}
allLists = await ListService.All();
inLists = [.. (await ListService.Of(account.Id)).Select(l => l.Id)];
}
async Task SetListMember(MastodonList list, bool member)
{
isActing = true;
var result = await ListService.SetMember(list.Id, account.Id, member);
isActing = false;
if (result.IsInvalid)
{
Toast.AddResponseError(result);
return;
}
if (member)
inLists.Add(list.Id);
else
inLists.Remove(list.Id);
Toast.AddResponseSuccess(member ? Localizer["{0} added to {1}.", account.Name, list.Title] : Localizer["{0} removed from {1}.", account.Name, list.Title]);
}
async Task LoadPage()
{
isLoadingMore = true;
@@ -247,6 +307,8 @@
}
relationship = (Relationship)result.Data ?? relationship;
if (action is AccountAction.Follow or AccountAction.Unfollow)
await LoadLists();
Toast.AddResponseSuccess(action switch
{
AccountAction.Follow when relationship?.Requested == true => Localizer["Asked {0} to accept you.", account.Name],