From 1777ec248bc1cd2df00544ba3323268910638a76 Mon Sep 17 00:00:00 2001 From: pancakes Date: Sat, 22 Feb 2025 11:53:44 +1000 Subject: [PATCH] [frontend/components] Add global user dialog --- .../Components/GlobalComponents.razor | 1 + .../Components/UserDialog.razor | 149 ++++++++++++++++++ .../Components/UserDialog.razor.css | 82 ++++++++++ .../Components/UserDialog.razor.js | 7 + .../ControllerModels/SearchControllerModel.cs | 7 + .../Core/Services/GlobalComponentSvc.cs | 1 + 6 files changed, 247 insertions(+) create mode 100644 Iceshrimp.Frontend/Components/UserDialog.razor create mode 100644 Iceshrimp.Frontend/Components/UserDialog.razor.css create mode 100644 Iceshrimp.Frontend/Components/UserDialog.razor.js diff --git a/Iceshrimp.Frontend/Components/GlobalComponents.razor b/Iceshrimp.Frontend/Components/GlobalComponents.razor index b2c0f66f..5bd1ce23 100644 --- a/Iceshrimp.Frontend/Components/GlobalComponents.razor +++ b/Iceshrimp.Frontend/Components/GlobalComponents.razor @@ -5,5 +5,6 @@ + @code { } diff --git a/Iceshrimp.Frontend/Components/UserDialog.razor b/Iceshrimp.Frontend/Components/UserDialog.razor new file mode 100644 index 00000000..892f0984 --- /dev/null +++ b/Iceshrimp.Frontend/Components/UserDialog.razor @@ -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 Loc; + + +
+ @if (Waiting) + { + + } + else + { + @Text + + + @foreach (var user in Users) + { +
+ +
+ + + @@@user.Username@(user.Host != null ? $"@{user.Host}" : "") + +
+
+ } + +
+ + +
+ } +
+
+ +@code { + private ElementReference Dialog { get; set; } + private IJSObjectReference _module = null!; + private EventCallback Action { get; set; } + private string Text { get; set; } = ""; + private List 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 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("import", + "./Components/UserDialog.razor.js"); + GlobalComponentSvc.UserDialog = this; + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/UserDialog.razor.css b/Iceshrimp.Frontend/Components/UserDialog.razor.css new file mode 100644 index 00000000..5fa64ad7 --- /dev/null +++ b/Iceshrimp.Frontend/Components/UserDialog.razor.css @@ -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; + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/UserDialog.razor.js b/Iceshrimp.Frontend/Components/UserDialog.razor.js new file mode 100644 index 00000000..1a098dcb --- /dev/null +++ b/Iceshrimp.Frontend/Components/UserDialog.razor.js @@ -0,0 +1,7 @@ +export function openDialog(element) { + element.showModal() +} + +export function closeDialog(element) { + element.close() +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/ControllerModels/SearchControllerModel.cs b/Iceshrimp.Frontend/Core/ControllerModels/SearchControllerModel.cs index d8ca179d..113de2a9 100644 --- a/Iceshrimp.Frontend/Core/ControllerModels/SearchControllerModel.cs +++ b/Iceshrimp.Frontend/Core/ControllerModels/SearchControllerModel.cs @@ -16,4 +16,11 @@ internal class SearchControllerModel(ApiClient api) public Task LookupAsync(string target) => api.CallNullableAsync(HttpMethod.Get, "/search/lookup", QueryString.Create("target", target)); + + public Task> SearchAcctsAsync(string? username, string? host) => + api.CallAsync>(HttpMethod.Get, "/search/acct", + (username != null + ? QueryString.Create("username", username) + : QueryString.Empty) + + (host != null ? QueryString.Create("host", host) : QueryString.Empty)); } \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/GlobalComponentSvc.cs b/Iceshrimp.Frontend/Core/Services/GlobalComponentSvc.cs index 748b6b4f..9d436095 100644 --- a/Iceshrimp.Frontend/Core/Services/GlobalComponentSvc.cs +++ b/Iceshrimp.Frontend/Core/Services/GlobalComponentSvc.cs @@ -10,4 +10,5 @@ public class GlobalComponentSvc public NoticeDialog? NoticeDialog { get; set; } public PromptDialog? PromptDialog { get; set; } public SelectDialog? SelectDialog { get; set; } + public UserDialog? UserDialog { get; set; } }