2026-10-05 01:23:18 +02:00
|
|
|
@page "/search"
|
|
|
|
|
@attribute [Authorize]
|
|
|
|
|
@inherits LocalizableComponentBase
|
|
|
|
|
<Title>@Localizer["Search"]</Title>
|
|
|
|
|
|
|
|
|
|
@* Accounts, posts and hashtags (/api/v2/search). A handle (name@host) or a post's address is resolved by PrivaPub,
|
|
|
|
|
which fetches it from its server if it has not met it yet; the browser never contacts another server. *@
|
|
|
|
|
<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">
|
|
|
|
|
|
|
|
|
|
<EditForm Model="form" OnValidSubmit="Run" class="flex flex-wrap gap-2 items-center">
|
|
|
|
|
<DInput TValue="string" class="flex-1 min-w-48" @bind-Value="form.Query" autocomplete="off" type="search"
|
|
|
|
|
placeholder="@Localizer["Search accounts, posts and tags, or paste an address"]">
|
|
|
|
|
<Prefix><i class="ion-md-search"></i></Prefix>
|
|
|
|
|
</DInput>
|
|
|
|
|
<DButton Type="submit" Size="DSize.Sm" Color="DColor.Primary" Disabled="@(IsDefaultDisabled || isSearching)">
|
|
|
|
|
@if (isSearching)
|
|
|
|
|
{
|
|
|
|
|
<DLoading Size="DSize.Xs" LabelText="@Localizer["loading"]" />
|
|
|
|
|
}
|
|
|
|
|
<span>@Localizer["Search"]</span>
|
|
|
|
|
</DButton>
|
|
|
|
|
</EditForm>
|
|
|
|
|
|
|
|
|
|
@if (searched)
|
|
|
|
|
{
|
|
|
|
|
@if (results.Accounts.Count == 0 && results.Statuses.Count == 0 && results.Hashtags.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
<p class="w-full text-center"><i class="ion-ios-remove-circle-outline"></i> @Localizer["Nothing found."]</p>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (results.Accounts.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<h2 class="font-bold">@Localizer["Accounts"]</h2>
|
|
|
|
|
<div class="flex flex-col gap-2">
|
|
|
|
|
@foreach (var account in results.Accounts)
|
|
|
|
|
{
|
|
|
|
|
<a class="flex items-center gap-3 p-3 rounded-xl neo-raised neo-2xs min-w-0" href="@($"account/{Uri.EscapeDataString(account.Id)}")">
|
|
|
|
|
@* alt="": the name is next to it. *@
|
|
|
|
|
<img alt="" class="h-10 w-10 flex-none object-cover rounded-full" src="@(account.Avatar ?? "imgs/icon-192.png")" />
|
|
|
|
|
<span class="flex flex-col min-w-0">
|
|
|
|
|
<b class="truncate">@account.Name</b>
|
|
|
|
|
<span class="text-xs opacity-70 truncate">@("@" + account.Acct)</span>
|
|
|
|
|
</span>
|
|
|
|
|
</a>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (results.Hashtags.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<h2 class="font-bold">@Localizer["Hashtags"]</h2>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
@foreach (var tag in results.Hashtags)
|
|
|
|
|
{
|
|
|
|
|
<DButton Size="DSize.Sm" OnClick="@(() => OpenTag(tag.Name))">#@tag.Name</DButton>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (results.Statuses.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<h2 class="font-bold">@Localizer["Posts and replies"]</h2>
|
|
|
|
|
<div class="flex flex-col gap-4">
|
|
|
|
|
@foreach (var post in statuses)
|
|
|
|
|
{
|
|
|
|
|
<Content @key="post.Key" Post="post" OnTag="OpenTag" />
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
[Inject] AccountService AccountService { get; set; }
|
|
|
|
|
[Inject] InstanceService InstanceService { get; set; }
|
|
|
|
|
[Inject] ILocalStorageService LocalStorage { get; set; }
|
|
|
|
|
|
|
|
|
|
[SupplyParameterFromQuery(Name = "q")] public string Q { get; set; }
|
|
|
|
|
|
|
|
|
|
sealed class SearchForm
|
|
|
|
|
{
|
|
|
|
|
public string Query { get; set; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SearchForm form = new();
|
|
|
|
|
SearchResults results = new();
|
|
|
|
|
List<FeedPost> statuses = [];
|
|
|
|
|
bool searched;
|
|
|
|
|
bool isSearching;
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
IsLoading = false;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(Q))
|
|
|
|
|
return;
|
|
|
|
|
form.Query = Q;
|
|
|
|
|
await Run();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task Run()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(form.Query))
|
|
|
|
|
return;
|
|
|
|
|
isSearching = true;
|
|
|
|
|
// a handle or an address is something to resolve; plain words only search what PrivaPub already holds
|
|
|
|
|
var resolve = form.Query.Contains('@') || form.Query.StartsWith("http", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
results = await AccountService.Search(form.Query, resolve);
|
|
|
|
|
statuses = [.. results.Statuses.Select(FeedPost.Of)];
|
|
|
|
|
searched = true;
|
|
|
|
|
isSearching = false;
|
|
|
|
|
await InstanceService.Resolve(statuses.Select(p => p.Host).Append(InstanceService.SelfHost));
|
|
|
|
|
InstanceService.Locate(statuses);
|
2026-10-05 06:20:18 +02:00
|
|
|
// a handle opens its account's profile, whatever else matched
|
|
|
|
|
var handle = form.Query.Trim().TrimStart('@');
|
|
|
|
|
if (form.Query.Contains('@') && results.Accounts.FirstOrDefault(a => string.Equals(a.Acct, handle, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|| string.Equals($"{a.Acct}@{InstanceService.SelfHost}", handle, StringComparison.OrdinalIgnoreCase)) is { } exact)
|
|
|
|
|
Navigation.NavigateTo($"account/{Uri.EscapeDataString(exact.Id)}");
|
|
|
|
|
else if (results.Accounts.Count == 1 && results.Statuses.Count == 0 && form.Query.Contains('@'))
|
2026-10-05 01:23:18 +02:00
|
|
|
Navigation.NavigateTo($"account/{Uri.EscapeDataString(results.Accounts[0].Id)}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task OpenTag(string tagName)
|
|
|
|
|
{
|
|
|
|
|
var filter = await LocalStorage.GetItemAsync<FeedFilter>(nameof(FeedFilter)) ?? new();
|
|
|
|
|
filter.Tag = tagName;
|
|
|
|
|
filter.TimelineType = TimelineType.Tag;
|
|
|
|
|
await LocalStorage.SetItemAsync(nameof(FeedFilter), filter);
|
|
|
|
|
Navigation.NavigateTo("");
|
|
|
|
|
}
|
|
|
|
|
}
|