Skip to content

Commit

Permalink
unread messages work now \o/
Browse files Browse the repository at this point in the history
  • Loading branch information
NielsPilgaard committed Aug 27, 2023
1 parent d5a84a0 commit 4f091b7
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 36 deletions.
23 changes: 14 additions & 9 deletions src/web/Client/Features/Chat/ChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ namespace Jordnaer.Client.Features.Chat;
public interface IChatService
{
ValueTask<List<ChatDto>> GetChats(string userId);
ValueTask<Dictionary<Guid, int>> GetUnreadMessages(string userId);
ValueTask<List<ChatMessageDto>> GetChatMessages(Guid chatId);
ValueTask StartChat(ChatDto chat);
ValueTask StartChat(StartChat chat);
ValueTask SendMessage(ChatMessageDto message);
ValueTask MarkMessagesAsRead(Guid chatId);
}

public class ChatService : IChatService
Expand Down Expand Up @@ -48,16 +50,17 @@ public async ValueTask<List<ChatDto>> GetChats(string userId)
var newChats = HandleApiResponse(await _chatClient.GetChats(userId, cachedChats?.Count ?? 0));
if (cachedChats is null)
{
return newChats.OrderByDescending(chat => chat.HasUnreadMessages).ToList();
return newChats;
}

cachedChats.AddRange(newChats);

cachedChats = cachedChats.OrderByDescending(chat => chat.HasUnreadMessages).ToList();

return cachedChats;
}

public async ValueTask<Dictionary<Guid, int>> GetUnreadMessages(string userId)
=> HandleApiResponse(await _chatClient.GetUnreadMessages(userId));

public async ValueTask<List<ChatMessageDto>> GetChatMessages(Guid chatId)
{
string key = $"{chatId}-chatmessages";
Expand Down Expand Up @@ -86,21 +89,23 @@ public async ValueTask<List<ChatMessageDto>> GetChatMessages(Guid chatId)
return cachedMessages;
}

public async ValueTask StartChat(ChatDto chat) => HandleApiResponse(await _chatClient.StartChat(chat));
public async ValueTask StartChat(StartChat chat) => HandleApiResponse(await _chatClient.StartChat(chat));

public async ValueTask SendMessage(ChatMessageDto message) => HandleApiResponse(await _chatClient.SendMessage(message));

public async ValueTask MarkMessagesAsRead(Guid chatId) => await _chatClient.MarkMessagesAsRead(chatId);

/// <summary>
/// Checks the <c>StatusCode</c> of the <see cref="IApiResponse"/> and shows a popup if <c>IsSuccessStatusCode</c> is false..
/// Checks the <c>StatusCode</c> of the <see cref="IApiResponse"/> and shows a popup if <c>IsSuccessStatusCode</c> is false.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="response"></param>
private T HandleApiResponse<T>(IApiResponse<T> response) where T : new()
{
switch (response.StatusCode)
{
case { } when response.IsSuccessStatusCode:
return response.Content!;
case { } when response is { IsSuccessStatusCode: true, Content: not null }:
return response.Content;

case HttpStatusCode.TooManyRequests:
_snackbar.Add(ErrorMessages.High_Load, Severity.Info);
Expand All @@ -116,7 +121,7 @@ public async ValueTask<List<ChatMessageDto>> GetChatMessages(Guid chatId)


/// <summary>
/// Checks the <c>StatusCode</c> of the <see cref="IApiResponse"/> and shows a popup if <c>IsSuccessStatusCode</c> is false..
/// Checks the <c>StatusCode</c> of the <see cref="IApiResponse"/> and shows a popup if <c>IsSuccessStatusCode</c> is false.
/// </summary>
/// <param name="response"></param>
private void HandleApiResponse(IApiResponse response)
Expand Down
8 changes: 7 additions & 1 deletion src/web/Client/Features/Chat/IChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ public interface IChatClient
[Get("/api/chat/{userId}")]
Task<IApiResponse<List<ChatDto>>> GetChats(string userId, int skip = 0, int take = int.MaxValue);

[Get($"/api/chat/{MessagingConstants.GetUnreadMessages}/{{userId}}")]
Task<IApiResponse<Dictionary<Guid, int>>> GetUnreadMessages(string userId);

[Get($"/api/chat/{MessagingConstants.GetChatMessages}/{{chatId}}")]
Task<IApiResponse<List<ChatMessageDto>>> GetChatMessages(Guid chatId, int skip = 0, int take = int.MaxValue);

[Post($"/api/chat/{MessagingConstants.StartChat}")]
Task<IApiResponse> StartChat([Body] ChatDto chat);
Task<IApiResponse> StartChat([Body] StartChat startChat);

[Post($"/api/chat/{MessagingConstants.SendMessage}")]
Task<IApiResponse> SendMessage([Body] ChatMessageDto message);

[Post($"/api/chat/{MessagingConstants.MarkMessagesAsRead}/{{chatId}}")]
Task<IApiResponse> MarkMessagesAsRead(Guid chatId);
}
28 changes: 19 additions & 9 deletions src/web/Client/Features/Chat/LargeChatComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
<MudImage Src="@chat.GetChatImage(_currentUser.Id)" loading="lazy" Alt="Avatar" />
</MudAvatar>
<MudText>
@if (chat.HasUnreadMessages)
@if (_unreadMessages.TryGetValue(chat.Id, out var unreadMessageCount) && unreadMessageCount > 0)

Check warning on line 28 in src/web/Client/Features/Chat/LargeChatComponent.razor

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.
{
<b>@chat.GetDisplayName(_currentUser.Id) (@chat.UnreadMessageCount}</b>
<b>@chat.GetDisplayName(_currentUser.Id) (@unreadMessageCount)</b>
}
else
{
Expand All @@ -50,6 +50,7 @@
@if (_activeChat != null)
{
<MudList Class="chat-message-list" DisablePadding Dense>
@* ReSharper disable once UnusedParameter.Local *@
<Virtualize Items="_activeChat.Messages" Context="message" OverscanCount="8" ItemSize="80">
@if (IsMessageFromSelf(message))
{
Expand Down Expand Up @@ -95,6 +96,7 @@
private UserSlim _currentUserSlim = null!;
private List<ChatDto> _chats = new();
private ChatDto? _activeChat;
private Dictionary<Guid, int>? _unreadMessages;

private string _userText = string.Empty;

Expand All @@ -105,8 +107,6 @@
[SupplyParameterFromQuery]
public Guid? ChatId { get; set; }

// TODO: Implement logic for marking messages as read, make sure to update the local cache
protected override async Task OnInitializedAsync()
{
_currentUser = await ProfileCache.GetOrCreateProfileAsync();
Expand All @@ -119,7 +119,13 @@
_currentUserSlim = _currentUser.ToUserSlim();

_chats = await ChatService.GetChats(_currentUser.Id);
// TODO: Fetch a dictionary of [chatId] { unreadMessageCount } to work around caching

_unreadMessages = await ChatService.GetUnreadMessages(_currentUser.Id);

_chats = _chats
.OrderByDescending(chat => _unreadMessages.ContainsKey(chat.Id))
.ToList();

_isLoading = false;
}

Expand All @@ -146,6 +152,9 @@
await LoadMessages();
// We call this twice to ensure we're at the very bottom. Silly but easy
await ScrollToBottom();

await ChatService.MarkMessagesAsRead(_activeChat.Id);
_unreadMessages?.Remove(_activeChat.Id);
}
}

Expand Down Expand Up @@ -178,7 +187,7 @@
}
else
{
await ChatService.StartChat(_activeChat);
await ChatService.StartChat(_activeChat.ToStartChatCommand(_currentUser!.Id));
_isActiveChatPublished = true;
}

Expand All @@ -197,9 +206,10 @@

private async Task StartNewChat(IEnumerable<UserSlim> users)
{
if (users.Count() == 1)
var userList = users.ToList();
if (userList.Count() == 1)
{
var userIdsToFind = new[] { users.First().Id, _currentUser!.Id };
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)
Expand All @@ -218,7 +228,7 @@
StartedUtc = DateTime.UtcNow
};

foreach (var user in users.Where(u => u.Id != _currentUser!.Id))
foreach (var user in userList.Where(u => u.Id != _currentUser!.Id))
{
newChat.Recipients.Add(new UserSlim
{
Expand Down
64 changes: 53 additions & 11 deletions src/web/Server/Features/Chat/ChatApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,55 @@ async Task<Results<Ok<List<ChatDto>>, UnauthorizedHttpResult>> (
Id = chat.Id,
LastMessageSentUtc = chat.LastMessageSentUtc,
StartedUtc = chat.StartedUtc,
Recipients = chat.Recipients.Select(recipient => recipient.ToUserSlim()).ToList(),
UnreadMessageCount = context.UnreadMessages
.Count(unreadMessage =>
unreadMessage.ChatId == chat.Id &&
unreadMessage.RecipientId == userId)
Recipients = chat.Recipients.Select(recipient => recipient.ToUserSlim()).ToList()
})
.AsSingleQuery()
.ToListAsync(cancellationToken);
return TypedResults.Ok(chats);
});

group.MapGet($"{MessagingConstants.GetUnreadMessages}/{{userId}}",
async Task<Results<Ok<Dictionary<Guid, int>>, UnauthorizedHttpResult>> (
[FromRoute] string userId,
[FromServices] CurrentUser currentUser,
[FromServices] JordnaerDbContext context,
CancellationToken cancellationToken) =>
{
if (currentUser.Id != userId)
{
return TypedResults.Unauthorized();
}
var chatsWithUnreadMessages = await context.UnreadMessages
.AsNoTracking()
.Where(unreadMessage => unreadMessage.RecipientId == currentUser.Id)
.GroupBy(message => message.ChatId)
.ToDictionaryAsync(unreadMessages => unreadMessages.Key,
unreadMessages => unreadMessages.Count(),
cancellationToken);
return TypedResults.Ok(chatsWithUnreadMessages);
});

group.MapPost($"{MessagingConstants.MarkMessagesAsRead}/{{chatId:guid}}",
async Task<Results<NoContent, BadRequest>> (
[FromRoute] Guid chatId,
[FromServices] CurrentUser currentUser,
[FromServices] JordnaerDbContext context,
CancellationToken cancellationToken) =>
{
int rowsModified = await context
.UnreadMessages
.Where(unreadMessage => unreadMessage.ChatId == chatId &&
unreadMessage.RecipientId == currentUser.Id)
.ExecuteDeleteAsync(cancellationToken);
return rowsModified > 0
? TypedResults.NoContent()
: TypedResults.BadRequest();
});

group.MapGet($"{MessagingConstants.GetChatMessages}/{{chatId:guid}}",
async Task<Results<Ok<List<ChatMessageDto>>, UnauthorizedHttpResult>> (
[FromRoute] Guid chatId,
Expand Down Expand Up @@ -103,6 +141,7 @@ async Task<bool> CurrentUserIsNotPartOfChat()
return TypedResults.Ok(chatMessages);
});

group.MapPost(MessagingConstants.StartChat,
async Task<Results<NoContent, BadRequest, UnauthorizedHttpResult>> (
[FromBody] StartChat chat,
Expand Down Expand Up @@ -192,14 +231,17 @@ async Task<Results<NoContent, BadRequest, UnauthorizedHttpResult>> (
.Select(userChat => userChat.UserProfileId)
.ToListAsync(cancellationToken);
context.UnreadMessages.AddRange(recipientIds.Select(recipientId => new UnreadMessage
{
RecipientId = recipientId,
ChatId = chatMessage.ChatId,
MessageSentUtc = chatMessage.SentUtc
}));
context.UnreadMessages.AddRange(recipientIds
.Where(recipientId => recipientId != chatMessage.Sender.Id)
.Select(recipientId => new UnreadMessage
{
RecipientId = recipientId,
ChatId = chatMessage.ChatId,
MessageSentUtc = chatMessage.SentUtc
}));
await context.Chats
.Where(chat => chat.Id == chatMessage.ChatId)
.ExecuteUpdateAsync(call =>
call.SetProperty(chat => chat.LastMessageSentUtc, DateTime.UtcNow),
cancellationToken);
Expand Down
4 changes: 0 additions & 4 deletions src/web/Shared/Chat/ChatDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ public class ChatDto
/// </summary>
public string? DisplayName { get; init; }

public int UnreadMessageCount { get; init; }

public List<ChatMessageDto> Messages { get; set; } = new();
public List<UserSlim> Recipients { get; init; } = new();

public DateTime LastMessageSentUtc { get; init; }
public DateTime StartedUtc { get; init; } = DateTime.UtcNow;

public bool HasUnreadMessages => UnreadMessageCount > 0;
}
15 changes: 15 additions & 0 deletions src/web/Shared/Extensions/ChatDtoExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Jordnaer.Shared;

public static class ChatDtoExtensions
{
public static StartChat ToStartChatCommand(this ChatDto chatDto, string initiatorId) =>
new()
{
InitiatorId = initiatorId,
Id = chatDto.Id,
Recipients = chatDto.Recipients,
Messages = chatDto.Messages,
LastMessageSentUtc = chatDto.LastMessageSentUtc,
StartedUtc = chatDto.StartedUtc
};
}
4 changes: 2 additions & 2 deletions src/web/Shared/MessagingConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ namespace Jordnaer.Shared;

public static class MessagingConstants
{
public static readonly string[] QueueNames = { StartChat, SendMessage, SetChatName };

public const string StartChat = "start-chat";
public const string SendMessage = "send-message";
public const string SetChatName = "set-chat-name";
public const string GetChatMessages = "messages";
public const string GetUnreadMessages = "unread-messages";
public const string MarkMessagesAsRead = "messages-read";
}

0 comments on commit 4f091b7

Please sign in to comment.