[frontend/components] Add global user dialog

This commit is contained in:
pancakes 2025-02-22 11:53:44 +10:00
parent 03def5b290
commit 1777ec248b
No known key found for this signature in database
GPG key ID: ED53D426432B861B
6 changed files with 247 additions and 0 deletions

View file

@ -5,5 +5,6 @@
<PromptDialog /> <PromptDialog />
<SelectDialog /> <SelectDialog />
<StreamingStatus /> <StreamingStatus />
<UserDialog />
@code { @code {
} }

View file

@ -0,0 +1,149 @@
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@inject ApiService Api;
@inject GlobalComponentSvc GlobalComponentSvc;
@inject IJSRuntime Js;
@inject IStringLocalizer<Localization> Loc;
<dialog @onkeyup="HandleKeys" class="dialog" @ref="Dialog">
<div class="select @(Waiting ? "waiting" : "")">
@if (Waiting)
{
<LoadingSpinner Scale="1.5"/>
}
else
{
<span>@Text</span>
<div class="search">
<input type="text" class="input" id="username" @bind="Username" @bind:event="oninput" @bind:after="Search" placeholder="@Loc["Username"]" aria-label="username"/>
<input type="text" class="input" id="host" @bind="Host" @bind:event="oninput" @bind:after="Search" placeholder="@Loc["Host"]" aria-label="host"/>
</div>
@foreach (var user in Users)
{
<div @onclick="() => { Selected = user; }" class="user-entry @(user == Selected ? "selected" : "")">
<UserAvatar User="@user"/>
<div class="name-section">
<span class="display-name"><UserDisplayName User="@user"/></span>
<span class="identifier">
@@@user.Username@(user.Host != null ? $"@{user.Host}" : "")
</span>
</div>
</div>
}
<div class="buttons">
<button class="button submit-btn" @onclick="ConfirmAction" disabled="@(Selected == null)">@(ButtonText ?? Loc["Submit"])</button>
<button class="button" @onclick="CancelAction">@Loc["Cancel"]</button>
</div>
}
</div>
</dialog>
@code {
private ElementReference Dialog { get; set; }
private IJSObjectReference _module = null!;
private EventCallback<UserResponse?> Action { get; set; }
private string Text { get; set; } = "";
private List<UserResponse> Users { get; set; } = [];
private UserResponse? Selected { get; set; }
private string? ButtonText { get; set; }
private string Username { get; set; } = "";
private string Host { get; set; } = "";
private bool Waiting { get; set; }
private bool Searching { get; set; }
private bool SearchQueue { get; set; }
private async Task CloseDialog()
{
await _module.InvokeVoidAsync("closeDialog", Dialog);
}
public async Task Select(EventCallback<UserResponse?> action, string text, string? buttonText = null)
{
Action = action;
Text = text;
Users = [];
Selected = null;
ButtonText = buttonText;
Username = "";
Host = "";
Waiting = false;
Searching = false;
SearchQueue = false;
StateHasChanged();
await _module.InvokeVoidAsync("openDialog", Dialog);
await Search();
}
private async Task ConfirmAction()
{
if (Selected == null) return;
Waiting = true;
await Action.InvokeAsync(Selected);
Waiting = false;
await CloseDialog();
}
private async Task CancelAction()
{
await Action.InvokeAsync(null);
await CloseDialog();
}
private async Task Search()
{
if (Searching)
{
SearchQueue = true;
return;
}
do
{
Searching = true;
SearchQueue = false;
try
{
var res = await Api.Search.SearchAcctsAsync(string.IsNullOrWhiteSpace(Username) ? null : Username.Trim(), string.IsNullOrWhiteSpace(Host) ? null : Host.Trim());
Users = res;
if (Selected != null && Users.All(p => p.Id != Selected.Id))
{
Selected = null;
}
}
catch (ApiException) { }
Searching = false;
} while (SearchQueue);
StateHasChanged();
}
private async Task HandleKeys(KeyboardEventArgs e)
{
if (e is { Code: "Enter" })
{
if (!Waiting && Selected != null) await ConfirmAction();
}
if (e is { Code: "Escape" })
{
if (!Waiting) await CancelAction();
}
}
protected override async Task OnInitializedAsync()
{
_module = await Js.InvokeAsync<IJSObjectReference>("import",
"./Components/UserDialog.razor.js");
GlobalComponentSvc.UserDialog = this;
}
}

View file

@ -0,0 +1,82 @@
.dialog {
margin: auto;
overflow-y: scroll;
}
.dialog::backdrop {
opacity: 50%;
background-color: black;
}
.select {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
background-color: var(--background-color);
border-radius: 1rem;
margin: auto;
padding: 1rem;
width: max-content;
max-width: 45rem;
color: var(--font-color);
text-align: center;
text-wrap: wrap;
word-break: break-word;
}
.select:not(.waiting) {
min-width: 12rem;
}
.select .search {
display: flex;
align-items: center;
}
.user-entry {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem;
border-radius: 0.5rem;
text-align: left;
cursor: pointer;
}
.user-entry.selected {
background-color: var(--hover-color);
}
.user-entry .name-section {
display: flex;
flex-direction: column;
overflow: clip;
text-overflow: ellipsis;
.identifier {
font-size: 0.8em;
text-overflow: ellipsis;
overflow: clip;
}
}
.buttons {
display: flex;
}
.submit-btn {
background: var(--accent-color);
color: var(--font-color);
}
@media (max-width: 1000px) {
.select {
max-width: 100%;
}
.select .search {
flex-direction: column;
}
}

View file

@ -0,0 +1,7 @@
export function openDialog(element) {
element.showModal()
}
export function closeDialog(element) {
element.close()
}

View file

@ -16,4 +16,11 @@ internal class SearchControllerModel(ApiClient api)
public Task<RedirectResponse?> LookupAsync(string target) => public Task<RedirectResponse?> LookupAsync(string target) =>
api.CallNullableAsync<RedirectResponse>(HttpMethod.Get, "/search/lookup", QueryString.Create("target", target)); api.CallNullableAsync<RedirectResponse>(HttpMethod.Get, "/search/lookup", QueryString.Create("target", target));
public Task<List<UserResponse>> SearchAcctsAsync(string? username, string? host) =>
api.CallAsync<List<UserResponse>>(HttpMethod.Get, "/search/acct",
(username != null
? QueryString.Create("username", username)
: QueryString.Empty)
+ (host != null ? QueryString.Create("host", host) : QueryString.Empty));
} }

View file

@ -10,4 +10,5 @@ public class GlobalComponentSvc
public NoticeDialog? NoticeDialog { get; set; } public NoticeDialog? NoticeDialog { get; set; }
public PromptDialog? PromptDialog { get; set; } public PromptDialog? PromptDialog { get; set; }
public SelectDialog? SelectDialog { get; set; } public SelectDialog? SelectDialog { get; set; }
public UserDialog? UserDialog { get; set; }
} }