@page "/explore" @attribute [Authorize] @inherits LocalizableComponentBase @Localizer["Explore"] @* 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, 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. *@

@Localizer["Explore"]

@Localizer["Hashtags"]
@Localizer["Popular posts"]
@Localizer["Shared links"]
@Localizer["People"]
@Localizer["Who to follow"]
@Localizer["Followed hashtags"]
@if (IsLoading) {

@Localizer["loading"]

} else if (tab is ExploreTab.Hashtags or ExploreTab.Followed) { @if (tags.Count == 0) {

@(tab is ExploreTab.Followed ? Localizer["This persona follows no hashtag yet."] : Localizer["Nothing is trending yet."])

}
@foreach (var item in tags) {
@(item.Following == true ? Localizer["Unfollow"] : Localizer["Follow"])
}
} else if (tab is ExploreTab.Posts) { @if (posts.Count == 0) {

@Localizer["Nothing is trending yet."]

}
@foreach (var post in posts) { }
} else if (tab is ExploreTab.Suggestions) { @if (suggestions.Count == 0) {

@Localizer["No one to suggest yet."]

}
@foreach (var suggestion in suggestions) { }
} else if (tab is ExploreTab.Links) { @if (links.Count == 0) {

@Localizer["Nothing is trending yet."]

} } else {
@if (people.Count == 0) {

@Localizer["Empty"]

}
@foreach (var account in people) { @* alt="": the name is next to it. *@ @account.Name @("@" + account.Acct) } @if (morePeople) { @Localizer["Load more"] }
}
@code { ElementReference strip; int revealed = -1; protected override void OnAfterRender(bool firstRender) { if ((int)tab == revealed) return; revealed = (int)tab; CascadingState.Status.RevealActive(strip); } enum ExploreTab { Hashtags, 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; } ExploreTab tab = ExploreTab.Hashtags; List tags = []; List posts = []; List links = []; List people = []; List suggestions = []; DirectoryOrder order = DirectoryOrder.Active; bool localOnly; bool morePeople; bool isActing; string loadedPersonaId; static int Bar(int uses, Tag tag) => tag.History.Max(h => h.UsesCount) is > 0 and var most ? Math.Max(8, uses * 100 / most) : 8; // The persona changed (NavMenu's switcher): what it follows is another persona's. protected override async Task OnParametersSetAsync() { if (CascadingState.PersonaId is null) { IsLoading = false; return; } if (CascadingState.PersonaId == loadedPersonaId) return; loadedPersonaId = CascadingState.PersonaId; if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); } async Task LoadData() { IsLoading = true; switch (tab) { case ExploreTab.Hashtags: tags = await DiscoveryService.TrendingTags(); break; case ExploreTab.Followed: tags = await DiscoveryService.FollowedTags(); break; case ExploreTab.Posts: posts = [.. (await DiscoveryService.TrendingStatuses()).Select(FeedPost.Of)]; await InstanceService.Resolve(posts.Select(p => p.Host).Append(InstanceService.SelfHost)); InstanceService.Locate(posts); break; 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; break; } IsLoading = false; } async Task SelectTab(int index) { tab = (ExploreTab)index; await LoadData(); } async Task ChangeOrder(DirectoryOrder value) { order = value; await LoadData(); } async Task ToggleLocal(ChangeEventArgs e) { localOnly = e.Value is true; await LoadData(); } async Task LoadMorePeople() { isActing = true; var known = people.Select(a => a.Id).ToHashSet(); var page = await DiscoveryService.Directory(order, localOnly, people.Count); isActing = false; people.AddRange(page.Where(a => known.Add(a.Id))); morePeople = page.Count == DiscoveryService.DirectoryPage; } async Task ToggleFollow(Tag item) { isActing = true; var result = await DiscoveryService.FollowTag(item.Name, item.Following != true); isActing = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } item.Following = ((Tag)result.Data).Following; if (tab is ExploreTab.Followed && item.Following != true) tags.Remove(item); 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(nameof(FeedFilter)) ?? new(); filter.Tag = tagName; filter.TimelineType = TimelineType.Tag; await LocalStorage.SetItemAsync(nameof(FeedFilter), filter); Navigation.NavigateTo(""); } }