From c27b0dbf8a251e7f026e30cfcbf2492d134e93a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Pilgaard=20Gr=C3=B8ndahl?= Date: Fri, 18 Aug 2023 22:30:15 +0200 Subject: [PATCH] added a chat service to handle chat api access and caching --- src/web/Client/Features/Chat/ChatService.cs | 136 ++++++++++++++++++ src/web/Client/Features/Chat/IChatClient.cs | 2 +- .../Features/Chat/LargeChatComponent.razor | 62 ++------ src/web/Client/Program.cs | 2 + src/web/Client/_Imports.razor | 4 + src/web/Shared/Chat/ChatDto.cs | 2 +- 6 files changed, 156 insertions(+), 52 deletions(-) create mode 100644 src/web/Client/Features/Chat/ChatService.cs diff --git a/src/web/Client/Features/Chat/ChatService.cs b/src/web/Client/Features/Chat/ChatService.cs new file mode 100644 index 00000000..587dc845 --- /dev/null +++ b/src/web/Client/Features/Chat/ChatService.cs @@ -0,0 +1,136 @@ +using Jordnaer.Client.Models; +using System.Net; +using Jordnaer.Shared; +using Microsoft.Extensions.Caching.Memory; +using MudBlazor; +using Refit; + +namespace Jordnaer.Client.Features.Chat; + +public interface IChatService +{ + ValueTask> GetChats(string userId); + ValueTask> GetChatMessages(Guid chatId); + ValueTask StartChat(ChatDto chat); + ValueTask SendMessage(ChatMessageDto message); +} + +public class ChatService : IChatService +{ + private readonly IMemoryCache _cache; + private readonly IChatClient _chatClient; + private readonly ISnackbar _snackbar; + + public ChatService(IMemoryCache cache, IChatClient chatClient, ISnackbar snackbar) + { + _cache = cache; + _chatClient = chatClient; + _snackbar = snackbar; + } + + public async ValueTask> GetChats(string userId) + { + string key = $"{userId}-chats"; + bool cacheWasEmpty = false; + var cachedChats = await _cache.GetOrCreateAsync(key, async entry => + { + cacheWasEmpty = true; + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7); + + return HandleApiResponse(await _chatClient.GetChats(userId)); + }); + + if (cacheWasEmpty) + { + return cachedChats ?? new List(); + } + + var newChats = HandleApiResponse(await _chatClient.GetChats(userId, cachedChats?.Count ?? 0)); + if (cachedChats is null) + { + return newChats; + } + + cachedChats.AddRange(newChats); + + return cachedChats; + } + + public async ValueTask> GetChatMessages(Guid chatId) + { + string key = $"{chatId}-chatmessages"; + bool cacheWasEmpty = false; + var cachedMessages = await _cache.GetOrCreateAsync(key, async entry => + { + cacheWasEmpty = true; + + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7); + + return HandleApiResponse(await _chatClient.GetChatMessages(chatId)); + }); + + if (cacheWasEmpty) + { + return cachedMessages ?? new List(); + } + var newMessages = HandleApiResponse(await _chatClient.GetChatMessages(chatId, cachedMessages?.Count ?? 0)); + if (cachedMessages is null) + { + return newMessages; + } + + cachedMessages.AddRange(newMessages); + + return cachedMessages; + } + + public async ValueTask StartChat(ChatDto chat) => HandleApiResponse(await _chatClient.StartChat(chat)); + + public async ValueTask SendMessage(ChatMessageDto message) => HandleApiResponse(await _chatClient.SendMessage(message)); + + /// + /// Checks the StatusCode of the and shows a popup if IsSuccessStatusCode is false.. + /// + /// + /// + private T HandleApiResponse(IApiResponse response) where T : new() + { + switch (response.StatusCode) + { + case { } when response.IsSuccessStatusCode: + return response.Content!; + + case HttpStatusCode.TooManyRequests: + _snackbar.Add(ErrorMessages.High_Load, Severity.Info); + break; + + default: + _snackbar.Add(ErrorMessages.Something_Went_Wrong_Refresh, Severity.Warning); + break; + } + + return new T(); + } + + + /// + /// Checks the StatusCode of the and shows a popup if IsSuccessStatusCode is false.. + /// + /// + private void HandleApiResponse(IApiResponse response) + { + switch (response.StatusCode) + { + case { } when response.IsSuccessStatusCode: + return; + + case HttpStatusCode.TooManyRequests: + _snackbar.Add(ErrorMessages.High_Load, Severity.Info); + break; + + default: + _snackbar.Add(ErrorMessages.Something_Went_Wrong_Refresh, Severity.Warning); + break; + } + } +} diff --git a/src/web/Client/Features/Chat/IChatClient.cs b/src/web/Client/Features/Chat/IChatClient.cs index d17b2440..4476c15a 100644 --- a/src/web/Client/Features/Chat/IChatClient.cs +++ b/src/web/Client/Features/Chat/IChatClient.cs @@ -9,7 +9,7 @@ public interface IChatClient Task>> GetChats(string userId, int skip = 0, int take = int.MaxValue); [Get($"/api/chat/{MessagingConstants.GetChatMessages}/{{chatId}}")] - Task>> GetChatMessages(Guid chatId, int skip = 0, int take = 15); + Task>> GetChatMessages(Guid chatId, int skip = 0, int take = int.MaxValue); [Post($"/api/chat/{MessagingConstants.StartChat}")] Task StartChat([Body] ChatDto chat); diff --git a/src/web/Client/Features/Chat/LargeChatComponent.razor b/src/web/Client/Features/Chat/LargeChatComponent.razor index 96272ad8..f504c39b 100644 --- a/src/web/Client/Features/Chat/LargeChatComponent.razor +++ b/src/web/Client/Features/Chat/LargeChatComponent.razor @@ -1,14 +1,7 @@ -@using System.Globalization -@using System.Linq.Expressions -@using System.Security.Claims -@using MassTransit -@using Microsoft.VisualBasic.CompilerServices -@inject IChatClient ChatClient +@inject IChatService ChatService @inject IProfileCache ProfileCache -@inject AuthenticationStateProvider AuthenticationStateProvider @inject ISnackbar Snackbar @inject IJSRuntime JsRuntime -@inject IScrollManager ScrollManager @attribute [Authorize] @if (_currentUser is null) @@ -26,11 +19,6 @@ @foreach (var chat in _chats) { - @* TODO - - Fetch all messages now found in cache - - Fetch all chats not found in cache - - Virtualize chat (load at the top with scroll listener) - *@ @@ -104,7 +92,6 @@ private bool _isActiveChatPublished = true; private bool _isLoading = true; - // TODO: Select currently displayed chat using this, when we update to dotnet 8 [SupplyParameterFromQuery] public Guid? ChatId { get; set; } @@ -120,21 +107,7 @@ _currentUserSlim = _currentUser.ToUserSlim(); - var response = await ChatClient.GetChats(_currentUser.Id); - switch (response.StatusCode) - { - case var _ when response.IsSuccessStatusCode: - _chats = response.Content!; - break; - - case HttpStatusCode.TooManyRequests: - Snackbar.Add(ErrorMessages.High_Load, Severity.Info); - break; - - default: - Snackbar.Add(ErrorMessages.Something_Went_Wrong_Refresh, Severity.Warning); - break; - } + _chats = await ChatService.GetChats(_currentUser.Id); _isLoading = false; } @@ -147,23 +120,11 @@ return; } - var response = await ChatClient.GetChatMessages(_activeChat.Id, _activeChat.Messages.Count, int.MaxValue); - switch (response.StatusCode) - { - case var _ when response.IsSuccessStatusCode: - _activeChat.Messages.AddRange(response.Content!); - StateHasChanged(); - await ScrollToBottom(); - break; - - case HttpStatusCode.TooManyRequests: - Snackbar.Add(ErrorMessages.High_Load, Severity.Info); - break; - - default: - Snackbar.Add(ErrorMessages.Something_Went_Wrong_Refresh, Severity.Warning); - break; - } + _activeChat.Messages = await ChatService.GetChatMessages(_activeChat.Id); + + StateHasChanged(); + + await ScrollToBottom(); } private async Task SelectChat(ChatDto chat) @@ -172,6 +133,8 @@ if (_isActiveChatPublished) { await LoadMessages(); + // We call this twice to ensure we're at the very bottom. Silly but easy + await ScrollToBottom(); } } @@ -200,13 +163,12 @@ if (_isActiveChatPublished) { - // TODO: Error handling - await ChatClient.SendMessage(message); + await ChatService.SendMessage(message); } else { - // TODO: Error handling - await ChatClient.StartChat(_activeChat); + await ChatService.StartChat(_activeChat); + _isActiveChatPublished = true; } await ScrollToBottom(); diff --git a/src/web/Client/Program.cs b/src/web/Client/Program.cs index 5b1ee937..a9ef2e6b 100644 --- a/src/web/Client/Program.cs +++ b/src/web/Client/Program.cs @@ -65,6 +65,8 @@ builder.Services.Configure( builder.Configuration.GetSection(DataForsyningenOptions.SectionName)); +builder.Services.AddScoped(); + var host = builder.Build(); await host.RunAsync(); diff --git a/src/web/Client/_Imports.razor b/src/web/Client/_Imports.razor index 88f62d5d..ed7c7711 100644 --- a/src/web/Client/_Imports.razor +++ b/src/web/Client/_Imports.razor @@ -28,3 +28,7 @@ @using MudExtensions @using Blazored.LocalStorage @using Blazored.SessionStorage +@using System.Globalization +@using System.Linq.Expressions +@using System.Security.Claims +@using MassTransit diff --git a/src/web/Shared/Chat/ChatDto.cs b/src/web/Shared/Chat/ChatDto.cs index 38903831..1c4c5bb7 100644 --- a/src/web/Shared/Chat/ChatDto.cs +++ b/src/web/Shared/Chat/ChatDto.cs @@ -12,7 +12,7 @@ public class ChatDto /// public string? DisplayName { get; init; } - public List Messages { get; init; } = new(); + public List Messages { get; set; } = new(); public List Recipients { get; init; } = new(); public DateTime LastMessageSentUtc { get; init; }