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

+34 -2
View File
@@ -1,7 +1,8 @@
@inherits LocalizableComponentBase
@* The feed page's tabs, one per kind of feed, scrolling sideways on narrow screens. Groups adds a choice of the
persona's communities and circles, Tag the hashtag being followed. The stats icon shows how the current feed was
persona's communities and circles, Lists a choice of its lists (made and filled on /lists), Tag the hashtag being
followed. The stats icon shows how the current feed was
read; the buttons on the right open the composer and, on narrow screens, the globe. *@
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 min-w-0">
@@ -49,6 +50,28 @@
</DSelect>
}
}
else if (Filter.TimelineType is TimelineType.Lists)
{
<div class="flex flex-wrap items-center gap-2">
@if (Lists.Count == 0)
{
<p class="text-xs opacity-70">@Localizer["This persona has no list yet."]</p>
}
else
{
<DSelect TValue="string" Size="DSize.Sm" class="w-full sm:w-auto" Value="Filter.ListId" ValueChanged="SelectList" ValueExpression="() => Filter.ListId">
<Prefix><i class="ion-md-list"></i></Prefix>
<ChildContent>
@foreach (var list in Lists)
{
<option value="@list.Id">@list.Title</option>
}
</ChildContent>
</DSelect>
}
<a class="btn btn-sm" href="lists"><i class="ion-md-create"></i><span>@Localizer["Manage lists"]</span></a>
</div>
}
else if (Filter.TimelineType is TimelineType.Tag)
{
<p class="text-sm inline-flex items-center gap-2"><i class="ion-md-pricetag"></i><b>#@Filter.Tag</b></p>
@@ -58,6 +81,7 @@
@code {
[Parameter, EditorRequired] public FeedFilter Filter { get; set; }
[Parameter] public List<ViewGroup> Groups { get; set; } = [];
[Parameter] public List<MastodonList> Lists { get; set; } = [];
[Parameter] public IEnumerable<NerdRow> Stats { get; set; } = [];
[Parameter] public bool IsComposing { get; set; }
[Parameter] public EventCallback OnCompose { get; set; }
@@ -67,7 +91,7 @@
static readonly TimelineType[] Always =
[
TimelineType.Home, TimelineType.Local, TimelineType.Federation, TimelineType.Nearby, TimelineType.Groups, TimelineType.Direct,
TimelineType.Notifications, TimelineType.Favourites, TimelineType.Bookmarks
TimelineType.Notifications, TimelineType.Favourites, TimelineType.Bookmarks, TimelineType.Lists
];
List<TimelineType> Types => Filter.Tag is { Length: > 0 } ? [.. Always, TimelineType.Tag] : [.. Always];
@@ -90,6 +114,14 @@
Filter.TimelineType = Types[index];
if (Filter.TimelineType is TimelineType.Groups && SelectedGroup is null)
Filter.GroupId = Groups.FirstOrDefault()?.Id;
if (Filter.TimelineType is TimelineType.Lists && Lists.All(l => l.Id != Filter.ListId))
Filter.ListId = Lists.FirstOrDefault()?.Id;
await OnChanged.InvokeAsync(Filter);
}
async Task SelectList(string listId)
{
Filter.ListId = listId;
await OnChanged.InvokeAsync(Filter);
}