Iceshrimp.NET/Iceshrimp.Frontend/Components/SearchComponent.razor

239 lines
8.7 KiB
Text

@using System.Diagnostics.CodeAnalysis
@using Iceshrimp.Assets.PhosphorIcons
@* ReSharper disable once RedundantUsingDirective *@
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@using Iceshrimp.Frontend.Core.Services.StateServicePatterns
@inject IStringLocalizer<Localization> Loc;
@inject ApiService Api;
@inject NavigationManager Navigation;
@inject StateService StateService;
@inject IJSRuntime Js;
<form class="search" @onsubmit="Search" role="search">
<input @bind="SearchString" class="input" aria-label=""/>
<InputSelect @bind-Value="@SearchType">
@foreach (var type in Enum.GetValues(typeof(SearchTypeEnum)))
{
<option value="@type">@type</option>
}
</InputSelect>
<button class="button"><Icon Name="Icons.MagnifyingGlass"/>@Loc["Search"]</button>
</form>
@if (_resultType == ResultType.Notes)
{
<div class="note-results" @ref="Scroller">
@if (NoteSearchInstance?.NoteResponses != null)
{
foreach (var note in NoteSearchInstance?.NoteResponses!)
{
<div class="wrapper">
<div @onclick="() => OpenNote(note.Id)" class="note-container">
<Note NoteResponse="note" OpenNote="false"/>
</div>
</div>
}
<div class="end">
<ScrollEnd ManualLoad="NoteSearchInstance.FetchOlder" IntersectionChange="async () => { if (!NoteSearchInstance.SearchComplete) { await NoteSearchInstance.FetchOlder(); } }"/>
</div>
}
</div>
}
@if (_resultType == ResultType.Users)
{
<div class="user-results">
@if (UserSearchInstance?.UserResponses != null)
{
foreach (var user in UserSearchInstance?.UserResponses!)
{
<div @onclick="() => OpenProfile(user)" class="wrapper">
<UserProfileCard User="user"/>
</div>
}
<div class="end">
<ScrollEnd ManualLoad="UserSearchInstance.FetchOlder" IntersectionChange="async () => { if (!UserSearchInstance.SearchComplete) { await UserSearchInstance.FetchOlder(); } }"/>
</div>
}
</div>
}
@code {
private string SearchString { get; set; } = "";
private SearchTypeEnum SearchType { get; set; } = SearchTypeEnum.Note;
private NoteSearch? NoteSearchInstance { get; set; }
private UserSearch? UserSearchInstance { get; set; }
private IJSInProcessObjectReference Module { get; set; } = null!;
private ElementReference Scroller { get; set; }
private ResultType _resultType;
private bool _setScroll;
private float _scrollTop;
protected override async Task OnInitializedAsync()
{
Module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "/Components/SearchComponent.razor.js");
var state = StateService.Search.GetState();
if (state != null)
{
NoteSearchInstance = new NoteSearch(state.SearchString, Api, state.SearchResults);
_resultType = ResultType.Notes;
_scrollTop = state.ScrollTop;
_setScroll = true;
}
}
protected override void OnAfterRender(bool firstRender)
{
if (_setScroll)
{
SetScrollY(_scrollTop);
_setScroll = false;
}
}
private async Task Search()
{
NoteSearchInstance = null;
_resultType = ResultType.None;
StateHasChanged();
try
{
var lookup = await Api.Search.LookupAsync(SearchString);
if (lookup is not null && lookup.TargetUrl.Contains("user"))
{
var user = await Api.Users.GetUserAsync(lookup.TargetUrl.Split("users/")[1]);
if (user is not null)
{
var username = $"@{user.Username}";
if (user.Host != null) username += $"@{user.Host}";
Navigation.NavigateTo($"/{username}");
}
}
else if (lookup is not null && lookup.TargetUrl.Contains("notes"))
{
Navigation.NavigateTo(lookup.TargetUrl);
}
}
catch (ApiException)
{
if (SearchType == SearchTypeEnum.Note)
{
var searchRes = await Api.Search.SearchNotesAsync(SearchString, new PaginationQuery { Limit = 20 });
if (searchRes.Count > 0)
{
NoteSearchInstance = new NoteSearch(searchString: SearchString, api: Api, noteResponses: searchRes);
_resultType = ResultType.Notes;
StateHasChanged();
}
}
if (SearchType == SearchTypeEnum.User)
{
var searchRes = await Api.Search.SearchUsersAsync(SearchString, new PaginationQuery() { Limit = 20 });
if (searchRes.Count > 0)
{
UserSearchInstance = new UserSearch(searchString: SearchString, api: Api, userResponses: searchRes);
_resultType = ResultType.Users;
StateHasChanged();
}
}
}
}
private void OpenNote(string id)
{
StateService.Search.SetState(new SearchState { SearchResults = NoteSearchInstance!.NoteResponses, ScrollTop = GetScrollY(), SearchString = NoteSearchInstance!.SearchString });
Navigation.NavigateTo($"/notes/{id}");
}
private void OpenProfile(UserResponse user)
{
var username = $"@{user.Username}";
if (user.Host != null) username += $"@{user.Host}";
Navigation.NavigateTo($"/{username}");
}
private float GetScrollY()
{
return Module.Invoke<float>("GetScrollY");
}
private void SetScrollY(float scrollTop)
{
Module.InvokeVoid("SetScrollY", scrollTop);
}
private enum ResultType
{
Default,
Notes,
Users,
None
}
private enum SearchTypeEnum
{
Note,
User
}
[method: SetsRequiredMembers]
private class UserSearch(string searchString, ApiService api, List<UserResponse> userResponses)
{
internal required string MinId = userResponses.Last().Id;
internal required string MaxId = userResponses.First().Id;
internal required string SearchString = searchString;
internal required ApiService Api = api;
public required List<UserResponse> UserResponses { get; init; } = userResponses;
public bool SearchComplete = false;
public async Task FetchOlder()
{
var pq = new PaginationQuery { Limit = 15, MaxId = MinId };
var res = await Api.Search.SearchUsersAsync(SearchString, pq);
switch (res.Count)
{
case > 0:
MinId = res.Last().Id;
UserResponses.AddRange(res);
break;
case 0:
SearchComplete = true;
break;
}
}
}
[method: SetsRequiredMembers]
private class NoteSearch(string searchString, ApiService api, List<NoteResponse> noteResponses)
{
internal required string MinId = noteResponses.Last().Id;
internal required string MaxId = noteResponses.First().Id;
public required string SearchString = searchString;
internal required ApiService Api = api;
public required List<NoteResponse> NoteResponses { get; init; } = noteResponses;
public bool SearchComplete = false;
public async Task FetchOlder()
{
var pq = new PaginationQuery { Limit = 15, MaxId = MinId };
var res = await Api.Search.SearchNotesAsync(SearchString, pq);
switch (res.Count)
{
case > 0:
MinId = res.Last().Id;
NoteResponses.AddRange(res);
break;
case 0:
SearchComplete = true;
break;
}
}
}
}