✨ Typing indicator, mark as read server-side
This commit is contained in:
@ -1,16 +1,27 @@
|
||||
using System.Net.WebSockets;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using SystemClock = NodaTime.SystemClock;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection.Handlers;
|
||||
|
||||
public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
|
||||
public class MessageReadHandler(AppDatabase db, IMemoryCache cache, ChatRoomService crs) : IWebSocketPacketHandler
|
||||
{
|
||||
public string PacketType => "message.read";
|
||||
public string PacketType => "messages.read";
|
||||
|
||||
public async Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket)
|
||||
public const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
|
||||
|
||||
public async Task HandleAsync(
|
||||
Account.Account currentUser,
|
||||
string deviceId,
|
||||
WebSocketPacket packet,
|
||||
WebSocket socket,
|
||||
WebSocketService srv
|
||||
)
|
||||
{
|
||||
var request = packet.GetData<Chat.ChatController.MarkMessageReadRequest>();
|
||||
var request = packet.GetData<ChatController.MarkMessageReadRequest>();
|
||||
if (request is null)
|
||||
{
|
||||
await socket.SendAsync(
|
||||
@ -26,12 +37,24 @@ public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
|
||||
return;
|
||||
}
|
||||
|
||||
var existingStatus = await db.ChatStatuses
|
||||
.FirstOrDefaultAsync(x => x.MessageId == request.MessageId && x.Sender.AccountId == currentUser.Id);
|
||||
var sender = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
ChatMember? sender = null;
|
||||
var cacheKey = string.Format(ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
||||
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
|
||||
sender = cachedMember;
|
||||
else
|
||||
{
|
||||
sender = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (sender != null)
|
||||
{
|
||||
var cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
|
||||
cache.Set(cacheKey, sender, cacheOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (sender is null)
|
||||
{
|
||||
await socket.SendAsync(
|
||||
@ -47,16 +70,25 @@ public class MessageReadHandler(AppDatabase db) : IWebSocketPacketHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingStatus == null)
|
||||
db.ChatStatuses.Add(new MessageStatus
|
||||
{
|
||||
existingStatus = new MessageStatus
|
||||
{
|
||||
MessageId = request.MessageId,
|
||||
SenderId = sender.Id,
|
||||
};
|
||||
db.ChatStatuses.Add(existingStatus);
|
||||
}
|
||||
MessageId = request.MessageId,
|
||||
SenderId = sender.Id,
|
||||
ReadAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Broadcast read statuses
|
||||
var otherMembers = (await crs.ListRoomMembers(request.ChatRoomId)).Select(m => m.AccountId).ToList();
|
||||
foreach (var member in otherMembers)
|
||||
srv.SendPacketToAccount(member, packet);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
using System.Net.WebSockets;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection.Handlers;
|
||||
|
||||
public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, IMemoryCache cache) : IWebSocketPacketHandler
|
||||
{
|
||||
public string PacketType => "messages.typing";
|
||||
|
||||
public async Task HandleAsync(
|
||||
Account.Account currentUser,
|
||||
string deviceId,
|
||||
WebSocketPacket packet,
|
||||
WebSocket socket,
|
||||
WebSocketService srv
|
||||
)
|
||||
{
|
||||
var request = packet.GetData<ChatController.TypingMessageRequest>();
|
||||
if (request is null)
|
||||
{
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.Error,
|
||||
ErrorMessage = "Mark message as read requires you provide the ChatRoomId"
|
||||
}.ToBytes()),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMember? sender = null;
|
||||
var cacheKey = string.Format(MessageReadHandler.ChatMemberCacheKey, currentUser.Id, request.ChatRoomId);
|
||||
if (cache.TryGetValue(cacheKey, out ChatMember? cachedMember))
|
||||
sender = cachedMember;
|
||||
else
|
||||
{
|
||||
sender = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == request.ChatRoomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (sender != null)
|
||||
{
|
||||
var cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
|
||||
cache.Set(cacheKey, sender, cacheOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (sender is null)
|
||||
{
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.Error,
|
||||
ErrorMessage = "User is not a member of the chat room."
|
||||
}.ToBytes()),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Broadcast read statuses
|
||||
var otherMembers = (await crs.ListRoomMembers(request.ChatRoomId)).Select(m => m.AccountId).ToList();
|
||||
foreach (var member in otherMembers)
|
||||
srv.SendPacketToAccount(member, packet);
|
||||
}
|
||||
}
|
@ -5,5 +5,5 @@ namespace DysonNetwork.Sphere.Connection;
|
||||
public interface IWebSocketPacketHandler
|
||||
{
|
||||
string PacketType { get; }
|
||||
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket);
|
||||
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
|
||||
}
|
@ -37,13 +37,17 @@ public class WebSocketPacket
|
||||
/// <returns>Deserialized data of type T</returns>
|
||||
public T? GetData<T>()
|
||||
{
|
||||
if (Data == null)
|
||||
return default;
|
||||
if (Data is T typedData)
|
||||
return typedData;
|
||||
|
||||
var jsonOpts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
return JsonSerializer.Deserialize<T>(
|
||||
JsonSerializer.Serialize(Data)
|
||||
JsonSerializer.Serialize(Data, jsonOpts),
|
||||
jsonOpts
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class WebSocketService
|
||||
{
|
||||
if (_handlerMap.TryGetValue(packet.Type, out var handler))
|
||||
{
|
||||
await handler.HandleAsync(currentUser, deviceId, packet, socket);
|
||||
await handler.HandleAsync(currentUser, deviceId, packet, socket, this);
|
||||
return;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user