From 6abc3065e6621d15b44ae49f2290285cf7c89839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Pilgaard=20Gr=C3=B8ndahl?= Date: Tue, 5 Sep 2023 21:57:00 +0200 Subject: [PATCH 1/3] split chat into components, mark chat as active if id is in route, focus input automatically --- .../Features/Chat/ChatMessageList.razor | 46 ++++++ .../Client/Features/Chat/ChatSelector.razor | 53 +++++++ .../Features/Chat/LargeChatComponent.razor | 143 +++++------------- src/web/Client/Pages/Chat/ChatPage.razor | 10 +- src/web/Client/wwwroot/js/scroll.js | 7 + 5 files changed, 157 insertions(+), 102 deletions(-) create mode 100644 src/web/Client/Features/Chat/ChatMessageList.razor create mode 100644 src/web/Client/Features/Chat/ChatSelector.razor diff --git a/src/web/Client/Features/Chat/ChatMessageList.razor b/src/web/Client/Features/Chat/ChatMessageList.razor new file mode 100644 index 00000000..d166dc5b --- /dev/null +++ b/src/web/Client/Features/Chat/ChatMessageList.razor @@ -0,0 +1,46 @@ + + + @if (IsMessageFromSelf(message)) + { + + + + + + + + + } + else + { + + + + + + + + @message.Text + + + + + } + + +@code +{ + [Parameter] + public required UserProfile CurrentUser { get; set; } = null!; + [Parameter] + public required ChatDto? ActiveChat { get; set; } + + private bool IsMessageFromSelf(ChatMessageDto message) => message.SenderId == CurrentUser.Id; + + private string GetRecipientsProfilePictureUrl(ChatMessageDto message) + => ActiveChat? + .Recipients + .FirstOrDefault(recipient => recipient.Id == message.SenderId)?.ProfilePictureUrl + ?? ProfileConstants.Default_Profile_Picture; +} diff --git a/src/web/Client/Features/Chat/ChatSelector.razor b/src/web/Client/Features/Chat/ChatSelector.razor new file mode 100644 index 00000000..0f602454 --- /dev/null +++ b/src/web/Client/Features/Chat/ChatSelector.razor @@ -0,0 +1,53 @@ + + + + + + @foreach (var chat in Chats) + { + + + + + + + @if (chat.HasUnreadMessages) + { + @chat.GetDisplayName(CurrentUser.Id) (@chat.UnreadMessageCount) + } + else + { + @chat.GetDisplayName(CurrentUser.Id) + } + + + + + + } + + + +@code{ + [Parameter] + public required UserProfile CurrentUser { get; set; } = null!; + [Parameter] + public required List Chats { get; set; } = null!; + + [Parameter] + public required EventCallback SelectChatCallback { get; set; } + [Parameter] + public required EventCallback> StartNewChatCallback { get; set; } + + + private async Task StartNewChat(IEnumerable users) + { + await StartNewChatCallback.InvokeAsync(users); + } + + private async Task SelectChat(ChatDto chat) + { + await SelectChatCallback.InvokeAsync(chat); + } + +} diff --git a/src/web/Client/Features/Chat/LargeChatComponent.razor b/src/web/Client/Features/Chat/LargeChatComponent.razor index 24585c30..864adf5e 100644 --- a/src/web/Client/Features/Chat/LargeChatComponent.razor +++ b/src/web/Client/Features/Chat/LargeChatComponent.razor @@ -13,115 +13,59 @@ - - - - - - @foreach (var chat in _chats) - { - - - - - - - @if (chat.HasUnreadMessages) - { - @chat.GetDisplayName(_currentUser.Id) (@chat.UnreadMessageCount) - } - else - { - @chat.GetDisplayName(_currentUser.Id) - } - - - - - - } - - + + - + - + @if (_activeChat != null) { - @* ReSharper disable once UnusedParameter.Local *@ - - @if (IsMessageFromSelf(message)) - { - - - - - - - - - } - else - { - - - - - - - - @message.Text - - - - - } - - + + + + Selector=".message-sent-successfully-container"/> + + @if (LastMessageWasSentSuccessfullyByCurrentUser) + { +
+ +
+ } - @if (LastMessageWasSentSuccessfullyByCurrentUser) - { -
- -
- } -
+
+ OnAdornmentClick="SendMessage"/>
} -
-
+ @code { - // TODO: Select currently displayed chat using this, when we update to dotnet 8 - [SupplyParameterFromQuery] + [Parameter] public Guid? ChatId { get; set; } private UserProfile? _currentUser; - private UserSlim _currentUserSlim = null!; private List _chats = new(); private ChatDto? _activeChat; @@ -163,9 +107,11 @@ return; } - _currentUserSlim = _currentUser.ToUserSlim(); - _chats = await ChatService.GetChats(_currentUser.Id); + if (ChatId is not null) + { + _activeChat = _chats.FirstOrDefault(chat => chat.Id == ChatId); + } ChatSignalRClient.OnChatStarted(chat => { @@ -227,8 +173,6 @@ _activeChat.Messages = await ChatService.GetChatMessages(_activeChat.Id); StateHasChanged(); - - await ScrollToBottom(); } private async Task SelectChat(ChatDto chat) @@ -237,9 +181,11 @@ if (_isActiveChatPublished) { await LoadMessages(); - // We call this twice to ensure we're at the very bottom. Silly but easy + await ScrollToBottom(); + await FocusMessageInput(); + if (chat.HasUnreadMessages) { await ChatService.MarkMessagesAsRead(_activeChat.Id); @@ -248,6 +194,8 @@ } } + private async Task FocusMessageInput() => await JsRuntime.InvokeVoidAsync("scrollFunctions.focusMessageInput"); + private async Task ScrollToBottom() => await JsRuntime.InvokeVoidAsync("scrollFunctions.scrollToTheBottomOfChat"); private void BackToList() => _activeChat = null; @@ -264,7 +212,7 @@ ChatId = _activeChat.Id, Id = NewId.NextGuid(), SentUtc = DateTime.UtcNow, - SenderId = _currentUserSlim.Id, + SenderId = _currentUser!.Id, Text = _userText }; @@ -292,12 +240,10 @@ } } - private bool IsMessageFromSelf(ChatMessageDto message) => message.SenderId == _currentUser!.Id; - private async Task StartNewChat(IEnumerable users) { var userList = users.ToList(); - if (userList.Count() == 1) + if (userList.Count == 1) { var userIdsToFind = new[] { userList.First().Id, _currentUser!.Id }; var existingChat = _chats.FirstOrDefault(chat => chat.Recipients.Count == 2 && @@ -311,12 +257,12 @@ } var newChat = new ChatDto - { - Id = NewId.NextGuid(), - Recipients = new List { _currentUserSlim }, - LastMessageSentUtc = DateTime.UtcNow, - StartedUtc = DateTime.UtcNow - }; + { + Id = NewId.NextGuid(), + Recipients = new List { _currentUser!.ToUserSlim() }, + LastMessageSentUtc = DateTime.UtcNow, + StartedUtc = DateTime.UtcNow + }; foreach (var user in userList.Where(u => u.Id != _currentUser!.Id)) { @@ -331,12 +277,7 @@ _chats.Insert(0, newChat); _isActiveChatPublished = false; + await SelectChat(newChat); } - - private string GetRecipientsProfilePictureUrl(ChatMessageDto message) - => _activeChat? - .Recipients - .FirstOrDefault(recipient => recipient.Id == message.SenderId)?.ProfilePictureUrl - ?? ProfileConstants.Default_Profile_Picture; } diff --git a/src/web/Client/Pages/Chat/ChatPage.razor b/src/web/Client/Pages/Chat/ChatPage.razor index dbdc244f..05f0a903 100644 --- a/src/web/Client/Pages/Chat/ChatPage.razor +++ b/src/web/Client/Pages/Chat/ChatPage.razor @@ -1,4 +1,6 @@ @page "/chat" +@page "/chat/{chatId:guid}" + @attribute [Authorize] @@ -9,8 +11,14 @@ - + + +@code +{ + [Parameter] + public Guid? ChatId { get; set; } +} diff --git a/src/web/Client/wwwroot/js/scroll.js b/src/web/Client/wwwroot/js/scroll.js index b8e8b16c..c37603ef 100644 --- a/src/web/Client/wwwroot/js/scroll.js +++ b/src/web/Client/wwwroot/js/scroll.js @@ -15,5 +15,12 @@ window.scrollFunctions = { if (!chatContainer) return; chatContainer.scrollTop = chatContainer.scrollHeight; + }, + focusMessageInput: function () { + const chatMessageInput = document.querySelector('#chat-message-input'); + + if (!chatMessageInput) return; + + chatMessageInput.focus(); } }; From 8044a14d2d55b60845c30d205951178ce075192a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Pilgaard=20Gr=C3=B8ndahl?= Date: Tue, 5 Sep 2023 22:32:21 +0200 Subject: [PATCH 2/3] mobile chat is functional, a bit more work is needed to get it right --- .../Client/Features/Chat/ChatSelector.razor | 2 +- .../Features/Chat/LargeChatComponent.razor | 43 ++++++++++++++----- src/web/Client/Pages/Chat/ChatPage.razor | 7 +-- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/web/Client/Features/Chat/ChatSelector.razor b/src/web/Client/Features/Chat/ChatSelector.razor index 0f602454..a887cce5 100644 --- a/src/web/Client/Features/Chat/ChatSelector.razor +++ b/src/web/Client/Features/Chat/ChatSelector.razor @@ -1,4 +1,4 @@ - + diff --git a/src/web/Client/Features/Chat/LargeChatComponent.razor b/src/web/Client/Features/Chat/LargeChatComponent.razor index 864adf5e..5cc800ca 100644 --- a/src/web/Client/Features/Chat/LargeChatComponent.razor +++ b/src/web/Client/Features/Chat/LargeChatComponent.razor @@ -3,6 +3,7 @@ @inject IProfileCache ProfileCache @inject ISnackbar Snackbar @inject IJSRuntime JsRuntime +@inject IBrowserViewportService BrowserViewportService @inject ChatSignalRClient ChatSignalRClient @attribute [Authorize] @@ -14,18 +15,35 @@ - + @if ((_activeChat is null && _isMobile) || _isMobile is false) + { + + } - - - + @if (_isMobile is false) + { + + + + } + + @if (_activeChat is not null) + { + + @if (_isMobile) + { + @*something like a banner is needed here, with display name and back button + Use MudAppBar + *@ + + + @_activeChat.GetDisplayName(_currentUser.Id) + + } - - @if (_activeChat != null) - { - + - } - + + } @@ -73,9 +91,11 @@ private bool _isActiveChatPublished = true; private bool _isLoading = true; + private bool _isMobile = true; private MudAnimate _messageSuccessfullySentAnimation = null!; private Dictionary _lastSuccessfullySentMessageForChat = new(); + private bool LastMessageWasSentSuccessfullyByCurrentUser { get @@ -107,6 +127,9 @@ return; } + var currentBreakpoint = await BrowserViewportService.GetCurrentBreakpointAsync(); + _isMobile = currentBreakpoint <= Breakpoint.SmAndDown; + _chats = await ChatService.GetChats(_currentUser.Id); if (ChatId is not null) { diff --git a/src/web/Client/Pages/Chat/ChatPage.razor b/src/web/Client/Pages/Chat/ChatPage.razor index 05f0a903..d5a43ddf 100644 --- a/src/web/Client/Pages/Chat/ChatPage.razor +++ b/src/web/Client/Pages/Chat/ChatPage.razor @@ -5,14 +5,9 @@ + - - - - - - From e3700a432f4507c98048738c09e0e0b711e8704b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Pilgaard=20Gr=C3=B8ndahl?= Date: Wed, 6 Sep 2023 20:59:17 +0200 Subject: [PATCH 3/3] finished the mobile chat!!!! --- ...hatComponent.razor => ChatComponent.razor} | 118 +++++++++++------- .../Features/Chat/SmallChatComponent.razor | 63 ---------- src/web/Client/Pages/Chat/ChatPage.razor | 10 +- src/web/Client/wwwroot/css/chat.css | 3 - src/web/Client/wwwroot/index.html | 29 ++--- src/web/Client/wwwroot/js/scroll.js | 15 +-- src/web/Client/wwwroot/js/utilities.js | 17 +++ 7 files changed, 112 insertions(+), 143 deletions(-) rename src/web/Client/Features/Chat/{LargeChatComponent.razor => ChatComponent.razor} (67%) delete mode 100644 src/web/Client/Features/Chat/SmallChatComponent.razor create mode 100644 src/web/Client/wwwroot/js/utilities.js diff --git a/src/web/Client/Features/Chat/LargeChatComponent.razor b/src/web/Client/Features/Chat/ChatComponent.razor similarity index 67% rename from src/web/Client/Features/Chat/LargeChatComponent.razor rename to src/web/Client/Features/Chat/ChatComponent.razor index 5cc800ca..ee86e826 100644 --- a/src/web/Client/Features/Chat/LargeChatComponent.razor +++ b/src/web/Client/Features/Chat/ChatComponent.razor @@ -5,6 +5,8 @@ @inject IJSRuntime JsRuntime @inject IBrowserViewportService BrowserViewportService @inject ChatSignalRClient ChatSignalRClient +@inject NavigationManager Navigation +@implements IAsyncDisposable @attribute [Authorize] @if (_currentUser is null) @@ -13,17 +15,17 @@ } - + @if ((_activeChat is null && _isMobile) || _isMobile is false) { - + } @if (_isMobile is false) { - + } @@ -32,47 +34,50 @@ @if (_isMobile) { - @*something like a banner is needed here, with display name and back button - Use MudAppBar - *@ - - - @_activeChat.GetDisplayName(_currentUser.Id) - + + + @_activeChat.GetDisplayName(_currentUser.Id) + } - + - + - @if (LastMessageWasSentSuccessfullyByCurrentUser) + @if (LastMessageWasSentSuccessfullyByCurrentUser) {
+ Class="message-sent-successfully" />
} -
+ + + FullWidth + Immediate + AutoFocus + Adornment="Adornment.End" + TextUpdateSuppression="false" + OnKeyDown="SendMessageOnEnter" + AdornmentIcon="@Icons.Material.Filled.Send" + AdornmentColor="@(string.IsNullOrEmpty(_userText) ? Color.Default : Color.Primary)" + OnAdornmentClick="SendMessage" /> + +
} @@ -92,6 +97,7 @@ private bool _isActiveChatPublished = true; private bool _isLoading = true; private bool _isMobile = true; + private Guid _breakpointObserverId = Guid.NewGuid(); private MudAnimate _messageSuccessfullySentAnimation = null!; private Dictionary _lastSuccessfullySentMessageForChat = new(); @@ -127,18 +133,33 @@ return; } - var currentBreakpoint = await BrowserViewportService.GetCurrentBreakpointAsync(); - _isMobile = currentBreakpoint <= Breakpoint.SmAndDown; + await HideFooter(); + + await BrowserViewportService.SubscribeAsync( + observerId: _breakpointObserverId, + lambda: args => _isMobile = args.Breakpoint <= Breakpoint.Sm, + fireImmediately: true); _chats = await ChatService.GetChats(_currentUser.Id); if (ChatId is not null) { - _activeChat = _chats.FirstOrDefault(chat => chat.Id == ChatId); + 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(chat => { - if (chat.InitiatorId == _currentUser.Id) + if (chat.InitiatorId == _currentUser!.Id) { return; } @@ -157,7 +178,8 @@ { return; } - if (message.SenderId == _currentUser.Id) + + if (message.SenderId == _currentUser!.Id) { _lastSuccessfullySentMessageForChat[chat.Id] = message.Id; StateHasChanged(); @@ -181,8 +203,6 @@ } }); await ChatSignalRClient.StartAsync(); - - _isLoading = false; } public async Task LoadMessages() @@ -215,13 +235,19 @@ chat.UnreadMessageCount = 0; } } - } - private async Task FocusMessageInput() => await JsRuntime.InvokeVoidAsync("scrollFunctions.focusMessageInput"); + Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("activeChat", _activeChat.Id)); + } - private async Task ScrollToBottom() => await JsRuntime.InvokeVoidAsync("scrollFunctions.scrollToTheBottomOfChat"); + 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; + private void BackToList() + { + _activeChat = null; + Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("activeChat", (Guid?)null)); + } private async Task SendMessage() { @@ -280,12 +306,12 @@ } var newChat = new ChatDto - { - Id = NewId.NextGuid(), - Recipients = new List { _currentUser!.ToUserSlim() }, - LastMessageSentUtc = DateTime.UtcNow, - StartedUtc = DateTime.UtcNow - }; + { + Id = NewId.NextGuid(), + Recipients = new List { _currentUser!.ToUserSlim() }, + LastMessageSentUtc = DateTime.UtcNow, + StartedUtc = DateTime.UtcNow + }; foreach (var user in userList.Where(u => u.Id != _currentUser!.Id)) { @@ -303,4 +329,6 @@ await SelectChat(newChat); } + + public async ValueTask DisposeAsync() => await BrowserViewportService.UnsubscribeAsync(_breakpointObserverId); } diff --git a/src/web/Client/Features/Chat/SmallChatComponent.razor b/src/web/Client/Features/Chat/SmallChatComponent.razor deleted file mode 100644 index 4a1f4852..00000000 --- a/src/web/Client/Features/Chat/SmallChatComponent.razor +++ /dev/null @@ -1,63 +0,0 @@ -@using System.Globalization -@using System.Security.Claims -@using MassTransit -@if (_activeChat is null) -{ - - @foreach (var chat in _chats) - { - - @chat.DisplayName - - } - - -} -else -{ - - - @_activeChat!.DisplayName - - - @foreach (var message in _activeChat!.Messages) - { - - @message.SentUtc.DisplayTimePassed(): @message.Text - - } - -} - -@code { - [Parameter] - public required UserProfile User { get; set; } - - protected override void OnInitialized() - { - } - - private List _chatUsers = new(); - - private List _chats = new(); - - private ChatDto? _activeChat; - - private void SelectChat(ChatDto chat) => _activeChat = chat; - - private void BackToList() => _activeChat = null; - - private string _newMessage = string.Empty; - - private void SendMessage() - { - } - - private void SendMessageOnEnter(KeyboardEventArgs keyboardEventArgs) - { - if (keyboardEventArgs.Key is "Enter") - { - SendMessage(); - } - } -} diff --git a/src/web/Client/Pages/Chat/ChatPage.razor b/src/web/Client/Pages/Chat/ChatPage.razor index d5a43ddf..6ba68e7f 100644 --- a/src/web/Client/Pages/Chat/ChatPage.razor +++ b/src/web/Client/Pages/Chat/ChatPage.razor @@ -1,19 +1,15 @@ @page "/chat" -@page "/chat/{chatId:guid}" @attribute [Authorize] - - - - + @code { - [Parameter] - public Guid? ChatId { get; set; } + [SupplyParameterFromQuery] + public Guid? ActiveChat { get; set; } } diff --git a/src/web/Client/wwwroot/css/chat.css b/src/web/Client/wwwroot/css/chat.css index 32bcb185..fd5b98f7 100644 --- a/src/web/Client/wwwroot/css/chat.css +++ b/src/web/Client/wwwroot/css/chat.css @@ -40,9 +40,6 @@ .message-sent-successfully { font-size: 14px; } -.flex-spacer { - flex-grow: 1; -} .chat-message-list .mud-input-control { flex: none; /* or flex: none; depending on what you want */ } diff --git a/src/web/Client/wwwroot/index.html b/src/web/Client/wwwroot/index.html index 094966f0..f0b0e42d 100644 --- a/src/web/Client/wwwroot/index.html +++ b/src/web/Client/wwwroot/index.html @@ -36,21 +36,22 @@ -
-
- Mini Møder Logo +
+
+ Mini Møder Logo +
-
-
- Der er sket en fejl. - Genindlæs - 🗙 -
- - - - - +
+ Der er sket en fejl. + Genindlæs + 🗙 +
+ + + + + + diff --git a/src/web/Client/wwwroot/js/scroll.js b/src/web/Client/wwwroot/js/scroll.js index c37603ef..66512eb0 100644 --- a/src/web/Client/wwwroot/js/scroll.js +++ b/src/web/Client/wwwroot/js/scroll.js @@ -9,18 +9,11 @@ window.scrollFunctions = { window.scrollTo(0, sessionStorage.getItem('scrollPosition')); }, 500); }, - scrollToTheBottomOfChat: function () { - const chatContainer = document.querySelector('.chat-message-window'); + scrollToBottomOfElement: function (selector) { + const element = document.querySelector(selector); - if (!chatContainer) return; + if (!element) return; - chatContainer.scrollTop = chatContainer.scrollHeight; - }, - focusMessageInput: function () { - const chatMessageInput = document.querySelector('#chat-message-input'); - - if (!chatMessageInput) return; - - chatMessageInput.focus(); + element.scrollTop = element.scrollHeight; } }; diff --git a/src/web/Client/wwwroot/js/utilities.js b/src/web/Client/wwwroot/js/utilities.js new file mode 100644 index 00000000..043f49a0 --- /dev/null +++ b/src/web/Client/wwwroot/js/utilities.js @@ -0,0 +1,17 @@ +window.utilities = { + hideElement: function (selector) { + const element = document.querySelector(selector); + + if (!element) return; + + element.style.setProperty("display", "none", "important") + }, + + focusElement: function (selector) { + const element = document.querySelector(selector); + + if (!element) return; + + element.focus(); + } +};