R8 the Mastodon API made real in decePub: featured tags, notify, languages, recommendations, notes, suggestions, the direct stream
A profile shows the hashtags an account features as chips opening their feed; under another account's header is what the persona keeps of it for itself alone: for one it follows, a bell (follow with notify), the languages of its posts that reach the home feed (follow with languages[]) and Recommend (endorsements, private here), and for any account a private note saved on blur or with Save. Settings edits the persona's featured hashtags (added by name or from those used lately, 10 at most) and its reading preferences (/clientapi/avatar/private/preferences), which CascadingState keeps per persona and the card applies to sensitive media and content warnings. A hashtag's tab features it on the profile. Explore has Who to follow (/api/v2/suggestions, dismissed for good after asking); Administration has Staff picks. The Direct tab follows the direct stream's conversation events; status notifications show the post. The follows CSV carries notify and languages. Verified: Release build, 0 errors (the 3 warnings were there before). Not run against PrivaPub or in the pasture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
566b0e33ea
commit
d33697e3fc
28 files changed
+1784
-52
No files matched your search
+103
-2
@@ -4,8 +4,10 @@
|
||||
<Title>@Localizer["Explore"]</Title>
|
||||
|
||||
@* What there is to find on PrivaPub: the hashtags, posts and links being talked about (from public posts it already
|
||||
holds; PrivaPub fetches nothing for them), the directory of accounts that let themselves be discovered, and the
|
||||
hashtags the persona follows. A hashtag opens as a tab of the feed; a link goes out to its site. *@
|
||||
holds; PrivaPub fetches nothing for them), the directory of accounts that let themselves be discovered, who to follow
|
||||
(PrivaPub's suggestions, each with why: picked by the staff, past interactions, popular here; followed or dismissed
|
||||
from here, a dismissal for good, so asked first), and the hashtags the persona follows. A hashtag opens as a tab of
|
||||
the feed; a link goes out to its site. *@
|
||||
<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">
|
||||
|
||||
@@ -21,6 +23,7 @@
|
||||
<DTab><Header><i class="ion-md-flame"></i> @Localizer["Popular posts"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-link"></i> @Localizer["Shared links"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-people"></i> @Localizer["People"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-person-add"></i> @Localizer["Who to follow"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-bookmarks"></i> @Localizer["Followed hashtags"]</Header></DTab>
|
||||
</DTabs>
|
||||
</div>
|
||||
@@ -75,6 +78,41 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (tab is ExploreTab.Suggestions)
|
||||
{
|
||||
@if (suggestions.Count == 0)
|
||||
{
|
||||
<p class="w-full text-center" data-feed-empty><i class="ion-ios-remove-circle-outline"></i> @Localizer["No one to suggest yet."]</p>
|
||||
}
|
||||
<div class="flex flex-col gap-2" data-suggestions>
|
||||
@foreach (var suggestion in suggestions)
|
||||
{
|
||||
<div class="flex flex-wrap items-center gap-3 pointer-coarse:gap-2 p-3 rounded-xl neo-raised neo-2xs min-w-0"
|
||||
data-suggestion="@suggestion.Account.Acct" data-suggestion-source="@suggestion.Source">
|
||||
<a class="flex items-center gap-3 min-w-0 flex-1" href="@($"account/{Uri.EscapeDataString(suggestion.Account.Id)}")">
|
||||
@* alt="": the name is next to it. *@
|
||||
<img alt="" class="h-10 w-10 flex-none object-cover rounded-full" src="@(suggestion.Account.Avatar ?? "imgs/icon-192.png")" />
|
||||
<span class="flex flex-col min-w-0">
|
||||
<b class="truncate">@CustomEmojis.Text(suggestion.Account.Name, suggestion.Account.Emojis)</b>
|
||||
<span class="text-xs opacity-70 truncate">@("@" + suggestion.Account.Acct)</span>
|
||||
</span>
|
||||
</a>
|
||||
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" title="@WhyText(suggestion)">@SourceName(suggestion.Source)</DBadge>
|
||||
<div class="flex gap-2 pointer-coarse:gap-2">
|
||||
<DButton Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => FollowSuggested(suggestion)"
|
||||
data-suggestion-follow>
|
||||
<i class="ion-md-person-add" aria-hidden="true"></i><span>@Localizer["Follow"]</span>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isActing)" OnClick="() => Dismiss(suggestion)"
|
||||
title="@Localizer["Never suggest {0} again", suggestion.Account.Name]" aria-label="@Localizer["Never suggest {0} again", suggestion.Account.Name]"
|
||||
data-suggestion-dismiss>
|
||||
<i class="ion-md-close" aria-hidden="true"></i><span class="max-sm:hidden">@Localizer["Dismiss"]</span>
|
||||
</DButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (tab is ExploreTab.Links)
|
||||
{
|
||||
@if (links.Count == 0)
|
||||
@@ -157,10 +195,13 @@
|
||||
Posts,
|
||||
Links,
|
||||
People,
|
||||
Suggestions,
|
||||
Followed
|
||||
}
|
||||
|
||||
[Inject] DiscoveryService DiscoveryService { get; set; }
|
||||
[Inject] AccountService AccountService { get; set; }
|
||||
[Inject] ConfirmService Confirm { get; set; }
|
||||
[Inject] InstanceService InstanceService { get; set; }
|
||||
[Inject] ILocalStorageService LocalStorage { get; set; }
|
||||
|
||||
@@ -169,6 +210,7 @@
|
||||
List<FeedPost> posts = [];
|
||||
List<TrendLink> links = [];
|
||||
List<Account> people = [];
|
||||
List<Suggestion> suggestions = [];
|
||||
DirectoryOrder order = DirectoryOrder.Active;
|
||||
bool localOnly;
|
||||
bool morePeople;
|
||||
@@ -219,6 +261,9 @@
|
||||
case ExploreTab.Links:
|
||||
links = await DiscoveryService.TrendingLinks();
|
||||
break;
|
||||
case ExploreTab.Suggestions:
|
||||
suggestions = [.. (await DiscoveryService.Suggestions()).Where(s => s.Account is not null)];
|
||||
break;
|
||||
default:
|
||||
people = await DiscoveryService.Directory(order, localOnly, 0);
|
||||
morePeople = people.Count == DiscoveryService.DirectoryPage;
|
||||
@@ -272,6 +317,62 @@
|
||||
Toast.AddResponseSuccess(item.Following == true ? Localizer["Following #{0}: its public posts come home.", item.Name] : Localizer["No longer following #{0}.", item.Name]);
|
||||
}
|
||||
|
||||
// why PrivaPub suggests an account: its v2 source
|
||||
string SourceName(string source) => source switch
|
||||
{
|
||||
"staff" => Localizer["Picked by the staff"],
|
||||
"past_interactions" => Localizer["You interacted with them"],
|
||||
"global" => Localizer["Popular here"],
|
||||
_ => source
|
||||
};
|
||||
|
||||
// every reason PrivaPub has for it (its sources)
|
||||
string WhyText(Suggestion suggestion) => string.Join(", ", suggestion.Sources.Select(source => source switch
|
||||
{
|
||||
"featured" => Localizer["picked by this server's staff"],
|
||||
"most_followed" => Localizer["followed by many here"],
|
||||
"most_interactions" => Localizer["much favourited and boosted here lately"],
|
||||
"past_interactions" => Localizer["you favourited, boosted or answered them lately"],
|
||||
_ => source
|
||||
}));
|
||||
|
||||
async Task FollowSuggested(Suggestion suggestion)
|
||||
{
|
||||
isActing = true;
|
||||
var result = await AccountService.Act(suggestion.Account.Id, AccountAction.Follow);
|
||||
isActing = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.Remove(suggestion);
|
||||
Toast.AddResponseSuccess(((Relationship)result.Data)?.Requested == true
|
||||
? Localizer["Asked {0} to accept you.", suggestion.Account.Name]
|
||||
: Localizer["Following {0}.", suggestion.Account.Name]);
|
||||
}
|
||||
|
||||
// for good: PrivaPub never suggests it to this persona again, so it is asked first
|
||||
async Task Dismiss(Suggestion suggestion)
|
||||
{
|
||||
if (!await Confirm.Ask(Localizer["Never suggest {0} again?", suggestion.Account.Name],
|
||||
Localizer["There is no undoing it, but {0} can still be followed from its profile.", suggestion.Account.Name], Localizer["Dismiss"], false))
|
||||
return;
|
||||
|
||||
isActing = true;
|
||||
var result = await DiscoveryService.DismissSuggestion(suggestion.Account.Id);
|
||||
isActing = false;
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.Remove(suggestion);
|
||||
Toast.AddResponseSuccess(Localizer["{0} is no longer suggested.", suggestion.Account.Name]);
|
||||
}
|
||||
|
||||
async Task OpenTag(string tagName)
|
||||
{
|
||||
var filter = await LocalStorage.GetItemAsync<FeedFilter>(nameof(FeedFilter)) ?? new();
|
||||
|
||||
Reference in new issue
Block a user