Skip to content

Commit

Permalink
Add status messages
Browse files Browse the repository at this point in the history
Added Connected and Disconnected  messages.
Changed history to support different message types.
Added JSON annotations to base record to allow polymorphic serialization.
  • Loading branch information
kova98 committed Oct 20, 2023
1 parent 6fe229e commit f1a37a4
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 25 deletions.
20 changes: 16 additions & 4 deletions Chat.Api/Messages.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
namespace Chat.Api;
using System.Text.Json.Serialization;

namespace Chat.Api;

// Required for polymorphic deserialization
[JsonDerivedType(typeof(ChatMessage))]
[JsonDerivedType(typeof(UserList))]
[JsonDerivedType(typeof(UserConnected))]
[JsonDerivedType(typeof(UserDisconnected))]
[JsonDerivedType(typeof(History))]
record Message(string Type);

record ChatMessage(string Name, string Content) : Message("ChatMessage");
record ChatMessage(string Name, string Content) : Message(nameof(ChatMessage));

record UserList(string[] Users) : Message(nameof(UserList));

record UserConnected(string Name) : Message(nameof(UserConnected));

record UserList(string[] Users) : Message("UserList");
record UserDisconnected(string Name) : Message(nameof(UserDisconnected));

record History(ChatMessage[] Messages) : Message("History");
record History(Message[] Messages) : Message(nameof(History));
26 changes: 16 additions & 10 deletions Chat.Api/WebSocketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Chat.Api;

public class WebSocketService(ILogger<WebSocketService> logger)
{
private static readonly ConcurrentQueue<ChatMessage> History = new();
private static readonly ConcurrentQueue<Message> History = new();
private static readonly ConcurrentDictionary<string, WebSocket> Connections = new();

public async Task HandleWebSocket(HttpContext context, string name)
Expand All @@ -21,10 +21,14 @@ public async Task HandleWebSocket(HttpContext context, string name)
}

Connections.TryAdd(name, socket);


var everyoneElse = Connections.Where(x => x.Key != name).Select(x => x.Value).ToArray();
var userConnected = new UserConnected(name);
await BroadcastMessage(userConnected, everyoneElse);
History.Enqueue(userConnected);

await SendMessage(socket, new History(History.ToArray()));
await BroadcastMessage(new UserList(Connections.Keys.ToArray()));
await SendMessage(socket, new UserList(Connections.Keys.ToArray()));

try
{
Expand Down Expand Up @@ -73,9 +77,10 @@ await Receive(socket, async (result, buffer) =>

private async Task RemoveUser(string name)
{
logger.LogInformation("User '{name}' disconnected.", name);
Connections.TryRemove(name, out _);
await BroadcastMessage(new UserList(Connections.Keys.ToArray()));
var msg = new UserDisconnected(name);
History.Enqueue(msg);
await BroadcastMessage(msg);
}

private static async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, ArraySegment<byte>> handleMessage)
Expand All @@ -88,7 +93,7 @@ private static async Task Receive(WebSocket socket, Action<WebSocketReceiveResul
}
}

private async Task SendMessage(WebSocket socket, object message)
private async Task SendMessage(WebSocket socket, Message message)
{
logger.LogInformation("Sending message: {message}", message);

Expand All @@ -97,19 +102,20 @@ private async Task SendMessage(WebSocket socket, object message)
await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}

private async Task BroadcastMessage(object message)
private async Task BroadcastMessage(Message message, IEnumerable<WebSocket>? receivers = null)
{
logger.LogInformation("Broadcasting message: {message}", message);

foreach (var (key, socket) in Connections)
var messageString = JsonSerializer.Serialize(message);
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(messageString));

foreach (var socket in receivers ?? Connections.Values)
{
if (socket.State != WebSocketState.Open)
{
continue;
}

var messageString = JsonSerializer.Serialize(message);
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(messageString));
await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
Expand Down
57 changes: 47 additions & 10 deletions web-client/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ function enableEnterToSubmit(input, action) {
});
}

function connect () {
function connect() {
name = nameInput.value.trim();
const nameParam = encodeURIComponent(name);
socket = new WebSocket(serverAddress + '/ws?name=' + nameParam);
setStatus('Connecting...');

socket.onopen = function (event) {
goToChat();
messageInput.focus();
Expand All @@ -35,7 +35,7 @@ function connect () {
socket.onerror = function (event) {
setStatus('Could not reach server.');
}

socket.onmessage = function (event) {
const message = JSON.parse(event.data);
switch (message.Type) {
Expand All @@ -48,7 +48,13 @@ function connect () {
case 'History':
handleHistory(message);
break;
default:
case 'UserConnected':
handleUserConnected(message);
break;
case 'UserDisconnected':
handleUserDisconnected(message);
break;
default:
console.log('Unknown message type: ' + message.Type);
}
};
Expand All @@ -63,11 +69,38 @@ function connect () {

function handleHistory(message) {
for (let i = 0; i < message.Messages.length; i++) {
handleChatMessage(message.Messages[i]);
switch (message.Messages[i].Type) {
case 'ChatMessage':
handleChatMessage(message.Messages[i]);
break;
case 'UserConnected':
handleUserConnected(message.Messages[i]);
break;
case 'UserDisconnected':
handleUserDisconnected(message.Messages[i]);
break;
default:
console.log('Unknown message type: ' + message.Messages[i].Type);
}
}
}

function handleUserList(message){
function handleUserConnected(message) {
users.push(message.Name);
updateUsersDisplay();
addMessage('* ' + message.Name + ' connected.', 'status-message');
}

function handleUserDisconnected(message) {
const index = users.indexOf(message.Name);
if (index > -1) {
users.splice(index, 1);
}
updateUsersDisplay();
addMessage('* ' + message.Name + ' disconnected.', 'status-message');
}

function handleUserList(message) {
users = message.Users;
updateUsersDisplay();
}
Expand All @@ -87,10 +120,14 @@ function updateUsersDisplay() {
}

function handleChatMessage(messageObject) {
const text = messageObject.Name + ': ' + messageObject.Content;
addMessage(text);
}
function addMessage(text, type) {
const message = document.createElement('div');
message.innerText = messageObject.Name + ': ' + messageObject.Content;
message.className = 'message';

message.innerText = text;
message.className = type || 'message';
const messages = document.getElementById('messages');
messages.appendChild(message);
messages.scrollTop = messages.scrollHeight
Expand All @@ -100,7 +137,7 @@ function sendMessage() {
const messageInput = document.getElementById('messageInput');
const messageText = messageInput.value.trim();
if (name && messageText) {
const messageObj = { Type: 'ChatMessage', Name: name, Content: messageText}
const messageObj = {Type: 'ChatMessage', Name: name, Content: messageText}
socket.send(JSON.stringify(messageObj));
messageInput.value = '';
messageInput.focus();
Expand Down
5 changes: 4 additions & 1 deletion web-client/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ body {
max-width: 300px;
}


.status {
margin-bottom: 12px;
color: var(--font-color);
Expand Down Expand Up @@ -125,6 +124,10 @@ h1 {
overflow-wrap: break-word;
}

.status-message {
color: grey;
}

.input-section {
margin-top: 16px;
}
Expand Down

0 comments on commit f1a37a4

Please sign in to comment.