Reworked message system, implemented in ManageUsers and EmailSignup

This commit is contained in:
Mia Rose Winter 2024-02-14 20:59:42 +01:00
parent 33e8d490ac
commit cfad069d41
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
8 changed files with 110 additions and 29 deletions

View file

@ -0,0 +1,45 @@
@using Wave.Utilities
@implements IDisposable
@inject IMessageDisplay Messages
@if (Message is {} message) {
<div class="alert @message.Type" role="alert">
<div> </div>
<div>
@if (message.Title is null) {
@message.Body
} else {
<span class="font-bold">@message.Title</span>
<span><small>@message.Body</small></span>
}
</div>
@if (CanDelete) {
<button class="btn btn-sm btn-square btn-ghost" onclick="this.parentElement.remove();">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
}
</div>
}
@code {
[Parameter]
public bool CanDelete { get; set; }
private IMessageDisplay.Message? Message { get; set; }
protected override void OnInitialized() {
Messages.OnMessage += OnMessage;
}
private bool OnMessage(IMessageDisplay.Message message) {
Message = message;
StateHasChanged();
return true;
}
public void Dispose() {
Messages.OnMessage -= OnMessage;
}
}

View file

@ -28,7 +28,14 @@
<SectionOutlet SectionName="GlobalHeader" /> <SectionOutlet SectionName="GlobalHeader" />
<input id="narrow-reading-toggle" type="checkbox" class="narrow-reading-toggle" /> <input id="narrow-reading-toggle" type="checkbox" class="narrow-reading-toggle" />
<div class="flex-1 container mx-auto px-4 md:px-12 py-8 reading-toggle-target h-full"> <div class="flex-1 container mx-auto px-4 md:px-12 py-8 reading-toggle-target h-full">
<AlertComponent CanDelete="true" />
@Body @Body
@if (HttpContext is null || HttpContext?.GetEndpoint()?
.Metadata.GetMetadata<RenderModeAttribute>()?
.Mode is not null) {
// for some reason that's how you test for interactive render modes
<ToastComponent />
}
</div> </div>
</main> </main>
<footer class="flex flex-col md:flex-row items-center justify-center p-4 gap-y-3 gap-x-4 bg-base-300 text-base-content"> <footer class="flex flex-col md:flex-row items-center justify-center p-4 gap-y-3 gap-x-4 bg-base-300 text-base-content">
@ -87,4 +94,6 @@
@code { @code {
[CascadingParameter(Name = "UserTheme")] [CascadingParameter(Name = "UserTheme")]
private string? UserTheme { get; set; } private string? UserTheme { get; set; }
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
} }

View file

@ -7,6 +7,7 @@
@using System.Net @using System.Net
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Wave.Services @using Wave.Services
@using Wave.Utilities
@inject ILogger<EmailSignup> Logger @inject ILogger<EmailSignup> Logger
@inject IStringLocalizer<EmailSignup> Localizer @inject IStringLocalizer<EmailSignup> Localizer
@ -16,15 +17,10 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IAdvancedEmailSender EmailSender @inject IAdvancedEmailSender EmailSender
@inject EmailTemplateService TemplateService @inject EmailTemplateService TemplateService
@inject IMessageDisplay Messages
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle> <PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@if (!string.IsNullOrWhiteSpace(Message)) {
<div class="alert alert-success">
<span>@Message</span>
</div>
}
<BoardComponent CenterContent="true"> <BoardComponent CenterContent="true">
<BoardCardComponent Heading="@Localizer["Title"]"> <BoardCardComponent Heading="@Localizer["Title"]">
<EditForm method="post" FormName="email-signup" Model="Model" OnValidSubmit="OnValidSubmit"> <EditForm method="post" FormName="email-signup" Model="Model" OnValidSubmit="OnValidSubmit">
@ -55,8 +51,6 @@
[Parameter, SupplyParameterFromQuery(Name = "token")] [Parameter, SupplyParameterFromQuery(Name = "token")]
public string? Token { get; set; } public string? Token { get; set; }
private string Message { get; set; } = string.Empty;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
if (Features.Value.EmailSubscriptions is not true) if (Features.Value.EmailSubscriptions is not true)
throw new ApplicationException("Email subscriptions not enabled."); throw new ApplicationException("Email subscriptions not enabled.");
@ -66,19 +60,19 @@
var id = await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: false); var id = await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: false);
if (id is null) { if (id is null) {
Message = Localizer["Failure_Message"]; Messages.ShowError(Localizer["Failure_Message"]);
return; return;
} }
await using var context = await ContextFactory.CreateDbContextAsync(); await using var context = await ContextFactory.CreateDbContextAsync();
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == id); var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Id == id);
if (subscriber is null) { if (subscriber is null) {
Message = Localizer["Failure_Message"]; Messages.ShowError(Localizer["Failure_Message"]);
return; return;
} }
subscriber.Unsubscribed = false; subscriber.Unsubscribed = false;
await context.SaveChangesAsync(); await context.SaveChangesAsync();
Message = Localizer["Success_Message"]; Messages.ShowSuccess(Localizer["Success_Message"]);
await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true); await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true);
@ -92,7 +86,7 @@
Localizer["WelcomeEmailSubject"], Localizer["WelcomeEmailTitle"], Localizer["WelcomeEmailBody"], articles); Localizer["WelcomeEmailSubject"], Localizer["WelcomeEmailTitle"], Localizer["WelcomeEmailBody"], articles);
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Error trying to confirm subscriber."); Logger.LogError(ex, "Error trying to confirm subscriber.");
Message = Localizer["Failure_Message"]; Messages.ShowError(Localizer["Failure_Message"]);
} }
} }
@ -101,7 +95,7 @@
throw new ApplicationException("Email subscriptions not enabled."); throw new ApplicationException("Email subscriptions not enabled.");
try { try {
Message = Localizer["Submit_Message"]; Messages.ShowSuccess(Localizer["Submit_Message"]);
await using var context = await ContextFactory.CreateDbContextAsync(); await using var context = await ContextFactory.CreateDbContextAsync();
var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email); var subscriber = context.Set<EmailSubscriber>().IgnoreQueryFilters().FirstOrDefault(s => s.Email == Model.Email);
@ -127,7 +121,6 @@
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Failed to create subscriber/send confirmation mail."); Logger.LogError(ex, "Failed to create subscriber/send confirmation mail.");
Message = Localizer["Failure_Message"];
} }
} }

View file

@ -11,6 +11,7 @@
@inject RoleManager<IdentityRole> RoleManager @inject RoleManager<IdentityRole> RoleManager
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject IStringLocalizer<ManageUsers> Localizer @inject IStringLocalizer<ManageUsers> Localizer
@inject IMessageDisplay Toast
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle> <PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@ -55,12 +56,9 @@
} }
</section> </section>
<ToastComponent @ref="Toast" />
@code { @code {
[CascadingParameter(Name = "TitlePrefix")] [CascadingParameter(Name = "TitlePrefix")]
private string TitlePrefix { get; set; } = default!; private string TitlePrefix { get; set; } = default!;
public IMessageDisplay Toast { get; set; } = null!;
private string ModalId { get; } = "UserDialog"; private string ModalId { get; } = "UserDialog";
[CascadingParameter] [CascadingParameter]

View file

@ -1,9 +1,11 @@
@using Wave.Utilities @using Wave.Utilities
@implements IMessageDisplay @implements IDisposable
@inject IMessageDisplay Messages
@rendermode InteractiveServer
<div class="toast toast-start z-10" role="alert"> <div class="toast toast-start z-10" role="alert">
@foreach (var message in Messages) { @foreach (var message in MessagesDisplayed) {
<div class="alert @message.Type" @onclick="() => Messages.Remove(message)"> <div class="alert @message.Type" @onclick="() => MessagesDisplayed.Remove(message)">
@if (message.Title is null) { @if (message.Title is null) {
@message.Body @message.Body
} else { } else {
@ -15,10 +17,19 @@
</div> </div>
@code { @code {
private List<IMessageDisplay.Message> Messages { get; } = []; private List<IMessageDisplay.Message> MessagesDisplayed { get; } = [];
public void ShowMessage(IMessageDisplay.Message message) { protected override void OnInitialized() {
Messages.Add(message); Messages.OnMessage += OnMessage;
}
private bool OnMessage(IMessageDisplay.Message message) {
MessagesDisplayed.Add(message);
StateHasChanged(); StateHasChanged();
return true;
}
public void Dispose() {
Messages.OnMessage -= OnMessage;
} }
} }

View file

@ -129,6 +129,7 @@
logMessages.Add("No Email provider configured."); logMessages.Add("No Email provider configured.");
} }
builder.Services.AddSingleton<IMessageDisplay, MessageService>();
builder.Services.AddSingleton<FileSystemService>(); builder.Services.AddSingleton<FileSystemService>();
builder.Services.AddSingleton<EmailTemplateService>(); builder.Services.AddSingleton<EmailTemplateService>();
builder.Services.AddHostedService<EmailBackgroundWorker>(); builder.Services.AddHostedService<EmailBackgroundWorker>();

View file

@ -0,0 +1,19 @@
using Wave.Utilities;
using static Wave.Utilities.IMessageDisplay;
namespace Wave.Services;
public class MessageService : IMessageDisplay {
private Queue<Message> Messages { get; } = new();
public event Func<Message, bool>? OnMessage;
public IReadOnlyList<Message> GetMessages() => [.. Messages];
public Message? Pop() => Messages.TryDequeue(out var m) ? m : null;
public void ShowMessage(Message message) {
if (OnMessage?.Invoke(message) is not true) {
Messages.Enqueue(message);
}
}
}

View file

@ -1,16 +1,21 @@
namespace Wave.Utilities; namespace Wave.Utilities;
public interface IMessageDisplay { public interface IMessageDisplay {
public void ShowMessage(Message message); IReadOnlyList<Message> GetMessages();
event Func<Message, bool>? OnMessage;
Message? Pop();
void ShowMessage(Message message);
public void ShowInfo(string message, string? title = null) void ShowInfo(string message, string? title = null)
=> ShowMessage(new Message(message, "alert-info", title, DateTimeOffset.UtcNow)); => ShowMessage(new Message(message, "alert-info", title, DateTimeOffset.UtcNow));
public void ShowSuccess(string message, string? title = null) void ShowSuccess(string message, string? title = null)
=> ShowMessage(new Message(message, "alert-success", title, DateTimeOffset.UtcNow)); => ShowMessage(new Message(message, "alert-success", title, DateTimeOffset.UtcNow));
public void ShowWarning(string message, string? title = null) void ShowWarning(string message, string? title = null)
=> ShowMessage(new Message(message, "alert-warning", title, DateTimeOffset.UtcNow)); => ShowMessage(new Message(message, "alert-warning", title, DateTimeOffset.UtcNow));
public void ShowError(string message, string? title = null) void ShowError(string message, string? title = null)
=> ShowMessage(new Message(message, "alert-error", title, DateTimeOffset.UtcNow)); => ShowMessage(new Message(message, "alert-error", title, DateTimeOffset.UtcNow));
public sealed record Message(string Body, string Type, string? Title, DateTimeOffset Created); sealed record Message(string Body, string Type, string? Title, DateTimeOffset Created);
} }