From 94d281b051502ae9573d0a6d0b2f94ac6f970436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Pilgaard=20Gr=C3=B8ndahl?= Date: Sun, 25 Feb 2024 20:25:59 +0100 Subject: [PATCH] bug free chat!!! --- .../Components/Account/Pages/Register.razor | 14 +- .../Jordnaer/Consumers/SendMessageConsumer.cs | 7 +- .../Features/Chat/ChatComponent.razor | 49 +-- .../Features/Chat/ChatSignalRClient.cs | 14 +- .../Chat/UnreadMessageSignalRClient.cs | 14 +- src/web/Jordnaer/Pages/Chat/ChatPage.razor | 345 +++++++++++++++++- .../SignalR/AuthenticatedSignalRClientBase.cs | 32 +- 7 files changed, 407 insertions(+), 68 deletions(-) diff --git a/src/web/Jordnaer/Components/Account/Pages/Register.razor b/src/web/Jordnaer/Components/Account/Pages/Register.razor index d5580175..dcbfc7b1 100644 --- a/src/web/Jordnaer/Components/Account/Pages/Register.razor +++ b/src/web/Jordnaer/Components/Account/Pages/Register.razor @@ -24,11 +24,6 @@

Opret en ny konto.


-
- - - -
@@ -87,7 +82,7 @@ var userId = await UserManager.GetUserIdAsync(user); - var userProfile = new UserProfile { Id = user.Id, UserName = Input.Username }; + var userProfile = new UserProfile { Id = user.Id }; await using var context = await DbContextFactory.CreateDbContextAsync(); context.UserProfiles.Add(userProfile); await context.SaveChangesAsync(); @@ -135,13 +130,6 @@ private sealed class InputModel { - [Required(ErrorMessage = "Påkrævet.")] - [StringLength(30, MinimumLength = 2, ErrorMessage = "{0] skal være mellem {2} og {1} karakter langt.")] - [RegularExpression("^[\\w@-_\\.^][^\\\\]+$", - ErrorMessage = "Brugernavn må kun bestå af bogstaver, tal, og udvalgte tegn.")] - [Display(Name = "Brugernavn")] - public string Username { get; set; } = ""; - [Required(ErrorMessage = "Påkrævet.")] [EmailAddress(ErrorMessage = "Email skal være gyldig.")] [Display(Name = "Email")] diff --git a/src/web/Jordnaer/Consumers/SendMessageConsumer.cs b/src/web/Jordnaer/Consumers/SendMessageConsumer.cs index 1a7cad48..fcda8c2e 100644 --- a/src/web/Jordnaer/Consumers/SendMessageConsumer.cs +++ b/src/web/Jordnaer/Consumers/SendMessageConsumer.cs @@ -1,4 +1,5 @@ using Jordnaer.Database; +using Jordnaer.Extensions; using Jordnaer.Features.Chat; using Jordnaer.Shared; using MassTransit; @@ -50,12 +51,12 @@ public async Task Consume(ConsumeContext consumeContext) }); } - await _chatHub.Clients.Users(recipientIds).ReceiveChatMessage(chatMessage); - try { await _context.SaveChangesAsync(consumeContext.CancellationToken); + await _chatHub.Clients.Users(recipientIds).ReceiveChatMessage(chatMessage); + await _context.Chats .Where(chat => chat.Id == chatMessage.ChatId) .ExecuteUpdateAsync(call => @@ -64,7 +65,7 @@ await _context.Chats } catch (Exception exception) { - _logger.LogError(exception, "Exception occurred while processing {command} command", nameof(SendMessage)); + _logger.LogException(exception); throw; } } diff --git a/src/web/Jordnaer/Features/Chat/ChatComponent.razor b/src/web/Jordnaer/Features/Chat/ChatComponent.razor index 1dd630a8..f5f4ba6a 100644 --- a/src/web/Jordnaer/Features/Chat/ChatComponent.razor +++ b/src/web/Jordnaer/Features/Chat/ChatComponent.razor @@ -7,10 +7,13 @@ @inject ChatSignalRClient ChatSignalRClient @inject NavigationManager Navigation @implements IAsyncDisposable + +@rendermode @(new InteractiveServerRenderMode(false)) + @attribute [Authorize] + Description="Her kan du skrive med andre" /> @@ -136,7 +139,6 @@ var lastChatMessage = chat.Messages.LastOrDefault(); if (lastChatMessage is not null && lastChatMessage.SenderId == _currentUser.Id) { - await InvokeAsync(StateHasChanged); return; } @@ -166,16 +168,18 @@ if (_activeChat?.Id == chat.Id) { - LastMessageWasSentSuccessfullyByCurrentUser = false; + LastMessageWasSentSuccessfullyByCurrentUser = false; await ChatService.MarkMessagesAsReadAsync(_currentUser.Id, message.ChatId); await InvokeAsync(StateHasChanged); - await ScrollToBottom(); } else { - chat.UnreadMessageCount++; - _chats = _chats.OrderByDescending(c => c.UnreadMessageCount).ToList(); - await InvokeAsync(StateHasChanged); + await InvokeAsync(() => + { + chat.UnreadMessageCount++; + _chats = _chats.OrderByDescending(c => c.UnreadMessageCount).ToList(); + StateHasChanged(); + }); } }); @@ -186,7 +190,6 @@ { if (_activeChat is null) { - Snackbar.Add(ErrorMessages.Something_Went_Wrong_Refresh, Severity.Warning); return; } @@ -211,7 +214,7 @@ MarkMessageIfSuccessfullySentByCurrentUser(); - if (!Navigation.Uri.EndsWith($"/chat/{_activeChat.Id}")) + if (_activeChat is not null && !Navigation.Uri.EndsWith($"/chat/{_activeChat.Id}")) { Navigation.NavigateTo($"/chat/{_activeChat.Id}"); } @@ -219,19 +222,19 @@ private void MarkMessageIfSuccessfullySentByCurrentUser() { - if (_activeChat is null) - { - return; - } - - var lastMessage = _activeChat.Messages.LastOrDefault(); - if (lastMessage is null) - { - LastMessageWasSentSuccessfullyByCurrentUser = false; - return; - } - - LastMessageWasSentSuccessfullyByCurrentUser= lastMessage.SenderId == _currentUser.Id; + if (_activeChat is null) + { + return; + } + + var lastMessage = _activeChat.Messages.LastOrDefault(); + if (lastMessage is null) + { + LastMessageWasSentSuccessfullyByCurrentUser = false; + return; + } + + LastMessageWasSentSuccessfullyByCurrentUser = lastMessage.SenderId == _currentUser.Id; } private async Task HideFooter() => await JsRuntime.InvokeVoidAsync("utilities.hideElement", ".footer"); @@ -241,7 +244,7 @@ private void BackToList() { _activeChat = null; - Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("activeChat", (Guid?)null)); + Navigation.NavigateTo($"{Navigation.BaseUri}chat"); } private async Task SendMessage() diff --git a/src/web/Jordnaer/Features/Chat/ChatSignalRClient.cs b/src/web/Jordnaer/Features/Chat/ChatSignalRClient.cs index c31be336..016bba24 100644 --- a/src/web/Jordnaer/Features/Chat/ChatSignalRClient.cs +++ b/src/web/Jordnaer/Features/Chat/ChatSignalRClient.cs @@ -14,17 +14,23 @@ public class ChatSignalRClient( { public void OnMessageReceived(Func action) { - if (!Started && HubConnection is not null) + if (HubConnection is null) { - HubConnection.On(nameof(IChatHub.ReceiveChatMessage), action); + return; } + + HubConnection.Remove(nameof(IChatHub.ReceiveChatMessage)); + HubConnection.On(nameof(IChatHub.ReceiveChatMessage), action); } public void OnChatStarted(Func action) { - if (!Started && HubConnection is not null) + if (HubConnection is null) { - HubConnection.On(nameof(IChatHub.StartChat), action); + return; } + + HubConnection.Remove(nameof(IChatHub.StartChat)); + HubConnection.On(nameof(IChatHub.StartChat), action); } } \ No newline at end of file diff --git a/src/web/Jordnaer/Features/Chat/UnreadMessageSignalRClient.cs b/src/web/Jordnaer/Features/Chat/UnreadMessageSignalRClient.cs index 5705762b..50eccb1a 100644 --- a/src/web/Jordnaer/Features/Chat/UnreadMessageSignalRClient.cs +++ b/src/web/Jordnaer/Features/Chat/UnreadMessageSignalRClient.cs @@ -14,17 +14,23 @@ public class UnreadMessageSignalRClient( { public void OnMessageReceived(Func action) { - if (!Started && HubConnection is not null) + if (HubConnection is null) { - HubConnection.On(nameof(IChatHub.ReceiveChatMessage), action); + return; } + + HubConnection.Remove(nameof(IChatHub.ReceiveChatMessage)); + HubConnection.On(nameof(IChatHub.ReceiveChatMessage), action); } public void OnChatStarted(Func action) { - if (!Started && HubConnection is not null) + if (HubConnection is null) { - HubConnection.On(nameof(IChatHub.StartChat), action); + return; } + + HubConnection.Remove(nameof(IChatHub.StartChat)); + HubConnection.On(nameof(IChatHub.StartChat), action); } } \ No newline at end of file diff --git a/src/web/Jordnaer/Pages/Chat/ChatPage.razor b/src/web/Jordnaer/Pages/Chat/ChatPage.razor index 532e0e5f..1e19129d 100644 --- a/src/web/Jordnaer/Pages/Chat/ChatPage.razor +++ b/src/web/Jordnaer/Pages/Chat/ChatPage.razor @@ -1,14 +1,345 @@ @page "/chat" -@page "/chat/{ActiveChat:guid}" +@page "/chat/{ChatId:guid}" + +@inject IChatService ChatService +@inject IChatMessageCache ChatMessageCache +@inject IProfileCache ProfileCache +@inject IJSRuntime JsRuntime +@inject IBrowserViewportService BrowserViewportService +@inject ChatSignalRClient ChatSignalRClient +@inject NavigationManager Navigation +@implements IAsyncDisposable + +@rendermode @(new InteractiveServerRenderMode(false)) @attribute [Authorize] - - - -@code -{ + + + + + + + + @if ((_activeChat is null && _isMobile) || _isMobile is false) + { + + } + + @if (_isMobile is false) + { + + + + } + + @if (_activeChat is not null) + { + + @if (_isMobile) + { + + + @_activeChat.GetDisplayName(_currentUser.Id) + + } + + + + + + + @if (LastMessageWasSentSuccessfullyByCurrentUser) + { +
+ +
+ } + + + +
+
+ } +
+
+
+ +@code { [Parameter] - public Guid? ActiveChat { get; set; } + public Guid? ChatId { get; set; } + + private UserProfile _currentUser = null!; + private List _chats = []; + private ChatDto? _activeChat; + + private string _userText = string.Empty; + + private bool _isActiveChatPublished = true; + private bool _isLoading = true; + private bool _isMobile = true; + private readonly Guid _breakpointObserverId = Guid.NewGuid(); + + private bool LastMessageWasSentSuccessfullyByCurrentUser; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await ScrollToBottom(); + await FocusMessageInput(); + await HideFooter(); + await BrowserViewportService.SubscribeAsync( + observerId: _breakpointObserverId, + lambda: args => _isMobile = args.Breakpoint <= Breakpoint.Sm, + fireImmediately: true); + } + + protected override async Task OnInitializedAsync() + { + var currentUser = await ProfileCache.GetProfileAsync(); + if (currentUser is null) + { + return; + } + + _currentUser = currentUser; + + _chats = await ChatService.GetChatsAsync(_currentUser.Id); + if (ChatId is not null) + { + var chatFromRoute = _chats.FirstOrDefault(chat => chat.Id == ChatId); + if (chatFromRoute is not null) + { + await SelectChat(chatFromRoute); + } + } + + await StartSignalR(); + + _isLoading = false; + } + + private async Task StartSignalR() + { + ChatSignalRClient.OnChatStarted(async chat => + { + var lastChatMessage = chat.Messages.LastOrDefault(); + if (lastChatMessage is not null && lastChatMessage.SenderId == _currentUser.Id) + { + await InvokeAsync(StateHasChanged); + return; + } + + var chatDto = chat.ToChatDto(); + chatDto.UnreadMessageCount++; + _chats.Insert(0, chatDto); + + await InvokeAsync(StateHasChanged); + }); + + ChatSignalRClient.OnMessageReceived(async message => + { + var chat = _chats.FirstOrDefault(chat => chat.Id == message.ChatId); + if (chat is null) + { + return; + } + + if (message.SenderId == _currentUser.Id) + { + LastMessageWasSentSuccessfullyByCurrentUser = true; + await InvokeAsync(StateHasChanged); + return; + } + + chat.Messages.Add(message.ToChatMessageDto()); + + if (_activeChat?.Id == chat.Id) + { + LastMessageWasSentSuccessfullyByCurrentUser = false; + await ChatService.MarkMessagesAsReadAsync(_currentUser.Id, message.ChatId); + await InvokeAsync(StateHasChanged); + } + else + { + chat.UnreadMessageCount++; + _chats = _chats.OrderByDescending(c => c.UnreadMessageCount).ToList(); + await InvokeAsync(StateHasChanged); + } + }); + + await ChatSignalRClient.StartAsync(); + } + + public async Task LoadMessages() + { + if (_activeChat is null) + { + return; + } + + _activeChat.Messages = await ChatMessageCache.GetChatMessagesAsync(_currentUser.Id, _activeChat.Id); + + StateHasChanged(); + } + + private async Task SelectChat(ChatDto chat) + { + _activeChat = chat; + if (_isActiveChatPublished) + { + await LoadMessages(); + + if (chat.HasUnreadMessages) + { + await ChatService.MarkMessagesAsReadAsync(_currentUser.Id, _activeChat.Id); + chat.UnreadMessageCount = 0; + } + } + + MarkMessageIfSuccessfullySentByCurrentUser(); + + if (_activeChat is not null && !Navigation.Uri.EndsWith($"/chat/{_activeChat.Id}")) + { + Navigation.NavigateTo($"/chat/{_activeChat.Id}"); + } + } + + private void MarkMessageIfSuccessfullySentByCurrentUser() + { + if (_activeChat is null) + { + return; + } + + var lastMessage = _activeChat.Messages.LastOrDefault(); + if (lastMessage is null) + { + LastMessageWasSentSuccessfullyByCurrentUser = false; + return; + } + + LastMessageWasSentSuccessfullyByCurrentUser = lastMessage.SenderId == _currentUser.Id; + } + + private async Task HideFooter() => await JsRuntime.InvokeVoidAsync("utilities.hideElement", ".footer"); + private async Task FocusMessageInput() => await JsRuntime.InvokeVoidAsync("utilities.focusElement", "#chat-message-input"); + private async Task ScrollToBottom() => await JsRuntime.InvokeVoidAsync("scrollFunctions.scrollToBottomOfElement", ".chat-message-window"); + + private void BackToList() + { + _activeChat = null; + Navigation.NavigateTo($"{Navigation.BaseUri}chat"); + } + + private async Task SendMessage() + { + if (_activeChat is null || string.IsNullOrWhiteSpace(_userText)) + { + return; + } + + var message = new ChatMessageDto + { + ChatId = _activeChat.Id, + Id = NewId.NextGuid(), + SentUtc = DateTime.UtcNow, + SenderId = _currentUser.Id, + Text = _userText + }; + + _activeChat.Messages.Add(message); + _userText = string.Empty; + + if (_isActiveChatPublished) + { + await ChatService.SendMessageAsync(message); + } + else + { + await ChatService.StartChatAsync(_activeChat.ToStartChatCommand(_currentUser.Id)); + _isActiveChatPublished = true; + } + + await ScrollToBottom(); + } + + private async Task SendMessageOnEnter(KeyboardEventArgs keyboardEventArgs) + { + if (keyboardEventArgs is { Key: "Enter", ShiftKey: false }) + { + await SendMessage(); + + _userText = ""; + } + } + + private async Task StartNewChat(IEnumerable users) + { + var userList = users.ToList(); + if (userList.Count == 1) + { + var userIdsToFind = new[] { userList.First().Id, _currentUser.Id }; + var existingChat = _chats.FirstOrDefault(chat => chat.Recipients.Count == 2 && + userIdsToFind.All(id => chat.Recipients.Any(recipient => recipient.Id == id))); + if (existingChat is not null) + { + await SelectChat(existingChat); + await ScrollToBottom(); + await FocusMessageInput(); + return; + } + } + + var newChat = new ChatDto + { + Id = NewId.NextGuid(), + Recipients = [_currentUser.ToUserSlim()], + LastMessageSentUtc = DateTime.UtcNow, + StartedUtc = DateTime.UtcNow + }; + + foreach (var user in userList.Where(u => u.Id != _currentUser.Id)) + { + newChat.Recipients.Add(new UserSlim + { + DisplayName = user.DisplayName, + Id = user.Id, + ProfilePictureUrl = user.ProfilePictureUrl, + UserName = user.UserName + }); + } + + _chats.Insert(0, newChat); + + _isActiveChatPublished = false; + + await SelectChat(newChat); + await ScrollToBottom(); + await FocusMessageInput(); + } + + public async ValueTask DisposeAsync() + { + await BrowserViewportService.UnsubscribeAsync(_breakpointObserverId); + await ChatSignalRClient.StopAsync(); + } } diff --git a/src/web/Jordnaer/SignalR/AuthenticatedSignalRClientBase.cs b/src/web/Jordnaer/SignalR/AuthenticatedSignalRClientBase.cs index 297eb63a..f911f61c 100644 --- a/src/web/Jordnaer/SignalR/AuthenticatedSignalRClientBase.cs +++ b/src/web/Jordnaer/SignalR/AuthenticatedSignalRClientBase.cs @@ -29,7 +29,7 @@ protected AuthenticatedSignalRClientBase( .Build(); } - protected bool Started { get; private set; } + protected bool Started { get; private set; } public bool IsConnected => HubConnection?.State is HubConnectionState.Connected; @@ -40,34 +40,38 @@ public async Task StartAsync(CancellationToken cancellationToken = default) //TODO: SignalR is flaky on Azure, maybe locally, investigate if (!Started && HubConnection is not null) { - _logger.LogDebug("Starting SignalR Client"); - await HubConnection.StartAsync(cancellationToken); - Started = true; + _logger.LogDebug("Starting SignalR Client"); + + await HubConnection.StartAsync(cancellationToken); + + Started = true; + _logger.LogDebug("SignalR Client Started"); } } public async Task StopAsync(CancellationToken cancellationToken = default) { - if (HubConnection?.State is HubConnectionState.Connected) + if (Started && HubConnection is not null) { - _logger.LogDebug("Stopping SignalR Client"); - await HubConnection.StopAsync(cancellationToken); + _logger.LogDebug("Stopping SignalR Client"); + + await HubConnection.StopAsync(cancellationToken); + + Started = false; + _logger.LogDebug("SignalR Client stopped"); } - else - { - _logger.LogDebug("Stop SignalR was called, but the Connection is currently in the {State} state. " + - "No further action is taken.", HubConnection?.State); - } } public async ValueTask DisposeAsync() { if (HubConnection is not null) { - _logger.LogInformation("Disposing SignalR Client"); - await HubConnection.DisposeAsync(); + _logger.LogInformation("Disposing SignalR Client"); + + await HubConnection.DisposeAsync(); + _logger.LogInformation("SignalR Client disposed"); }