Refactor chat feature and add websockets support

The chat feature in the Chess Server web application is now refactored to use WebSockets, allowing real-time communication between players during a game. The message display logic has been moved to a separate script file, and a ChatController handles incoming WebSocket connections. This update has drastically improved chat interactivity and real-time performance.
This commit is contained in:
Anduin
2024-06-21 14:00:18 +00:00
parent 06d748814b
commit 8001031a22
8 changed files with 172 additions and 89 deletions
@@ -0,0 +1,60 @@
using System.Text.Json;
using Aiursoft.AiurObserver;
using Aiursoft.AiurObserver.Extensions;
using Aiursoft.AiurObserver.WebSocket.Server;
using Aiursoft.ChessServer.Attributes;
using Aiursoft.ChessServer.Data;
using Aiursoft.ChessServer.Models;
using Aiursoft.WebTools.Attributes;
using Microsoft.AspNetCore.Mvc;
namespace Aiursoft.ChessServer.Controllers;
[Route("chats")]
public class ChatController(InMemoryDatabase database) : ControllerBase
{
[Route("{id:int}.ws")]
[EnforceWebSocket]
public async Task GetWebSocket([FromRoute] int id, [FromQuery][IsGuid] string playerId)
{
if (!ModelState.IsValid)
{
return;
}
var playerGuid = Guid.Parse(playerId);
var player = database.GetOrAddPlayer(playerGuid);
var challenge = database.GetAcceptedChallenge(id);
if (challenge == null)
{
return;
}
var pusher = await HttpContext.AcceptWebSocketClient();
var outSub = challenge
.ChatChannel
.Map(t => new ChatMessageResponse(t, playerGuid))
.Subscribe(t => pusher.Send(JsonSerializer.Serialize(t), HttpContext.RequestAborted));
var inSub = pusher
.Filter(t => !string.IsNullOrWhiteSpace(t))
.Subscribe(async message =>
{
await challenge.ChatChannel.BroadcastAsync(new ChatMessage(message, player));
});
try
{
await pusher.Listen(HttpContext.RequestAborted);
}
catch (TaskCanceledException)
{
// Ignore. This happens when the client closes the connection.
}
finally
{
await pusher.Close(HttpContext.RequestAborted);
outSub.Unsubscribe();
inSub.Unsubscribe();
}
}
}
@@ -55,6 +55,9 @@ public class AcceptedChallenge : Challenge
public Player Accepter { get; set; }
public Game Game { get; set; }
public AsyncObservable<ChatMessage> ChatChannel { get; init; } = new();
public Player GetWhitePlayer() => _creatorIsWhite ? Creator : Accepter;
public Player GetBlackPlayer() => _creatorIsWhite ? Accepter : Creator;
@@ -79,4 +82,17 @@ public class AcceptedChallenge : Challenge
}
return "m";
}
}
public class ChatMessage(string content, Player sender)
{
public string Content { get; set; } = content;
public Player Sender { get; set; } = sender;
}
public class ChatMessageResponse(ChatMessage message, Guid currentUserId)
{
public string Content { get; set; } = message.Content;
public string SenderNickName { get; set; } = message.Sender.NickName;
public bool IsMe { get; set; } = message.Sender.Id == currentUserId;
}
@@ -12,7 +12,7 @@
</div>
<div class="col-md-6">
@* Chat *@
<vc:chat></vc:chat>
<vc:chat game-id="@Model"></vc:chat>
</div>
</div>
</main>
@@ -5,8 +5,16 @@ namespace Aiursoft.ChessServer.Views.Shared.Components.Chat;
public class Chat : ViewComponent
{
public IViewComponentResult Invoke()
public IViewComponentResult Invoke(int gameId)
{
return View();
return View(new ChatModel
{
GameId = gameId
});
}
}
public class ChatModel
{
public int GameId { get; init; }
}
@@ -1,4 +1,6 @@
<div id="chat" class="w-100">
@model Aiursoft.ChessServer.Views.Shared.Components.Chat.ChatModel
<div id="chat" class="w-100">
<div class="container overflow-y-scroll scrollbar-width-none aspect-1-1" id="messages-box">
</div>
@@ -9,86 +11,8 @@
</form>
<script type="module">
let messagesBox;
let inputMessage;
window.addEventListener("DOMContentLoaded", async () => {
messagesBox = document.getElementById('messages-box');
inputMessage = document.getElementById('inputMessage');
scrollToNewestMessage();
mockMessage();
document.getElementById("chatSendForm").onsubmit = function (e) {
sendNewMessage();
return false;
}
});
function sendNewMessage() {
let msg = inputMessage.value;
if (msg.trim() === '') {
return;
}
/* sending message */
appendMyNewMessage(msg);
}
function scrollToNewestMessage() {
let lastChild = messagesBox.lastElementChild;
if (lastChild) {
lastChild.scrollIntoView({ behavior: "smooth" });
}
}
function appendMyNewMessage(message) {
let t = document.getElementById('messageFromMe').content;
let txt = t.querySelector("[data-message]");
txt.textContent = message;
let clone = document.importNode(t, true);
messagesBox.appendChild(clone);
scrollToNewestMessage();
}
function appendOpponentNewMessage(message) {
let t = document.getElementById('messageFromOpponent').content;
let txt = t.querySelector("[data-message]");
txt.textContent = message;
let clone = document.importNode(t, true);
messagesBox.appendChild(clone);
scrollToNewestMessage();
}
/**
* this is a mock for demonstrate
*/
function mockMessage() {
const messages = [
{ opponent: true, message: "Hello" },
{ opponent: false, message: "Hello" },
{ opponent: true, message: "You lost your queen~" },
{ opponent: false, message: "GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 GIVE MY QUEEN BACK!🤬 " },
{ opponent: true, message: "No way~" },
{ opponent: true, message: "HAHAHAHAHAHAHAHAHAHAHAHAH" },
{ opponent: false, message: "Ass!" },
];
messages.forEach((v) => {
if (v.opponent) {
appendOpponentNewMessage(v.message);
} else {
appendMyNewMessage(v.message);
}
});
}
</script>
<template id="messageFromOpponent">
@@ -113,3 +37,14 @@
</div>
</template>
</div>
<script type="module">
import initChat from "/scripts/chat.js";
import { getUserId } from "/scripts/player.js";
window.addEventListener('DOMContentLoaded', async () => {
var playerId = getUserId();
initChat(playerId, @Model.GameId);
});
</script>
@@ -9,9 +9,6 @@ import initGameBoard from "/scripts/chessboard.js";
import { getUserId, getPlayerColor } from "/scripts/player.js";
window.addEventListener('DOMContentLoaded', async () => {
// Ask the user to enter w or b:
//const player = prompt("Please enter your color (w or b):");
var playerId = getUserId();
var playerColor = await getPlayerColor(@Model.GameId);
initGameBoard(playerColor, playerId, @Model.GameId);
@@ -0,0 +1,67 @@
const initChat = function (playerId, gameId) {
const messagesBox = document.getElementById('messages-box');
const inputMessage = document.getElementById('inputMessage');
const chatSendForm = document.getElementById('chatSendForm');
const wsScheme = window.location.protocol === "https:" ? "wss://" : "ws://";
const socket = new WebSocket(
`${wsScheme}${window.location.host}/chats/${gameId}.ws?playerId=${playerId}`
);
chatSendForm.onsubmit = function sendNewMessage(e) {
e.preventDefault();
let msg = inputMessage.value;
if (msg.trim() === '') {
return;
}
socket.send(msg);
inputMessage.value = '';
}
function scrollToNewestMessage() {
let lastChild = messagesBox.lastElementChild;
if (lastChild) {
lastChild.scrollIntoView({ behavior: "smooth" });
}
}
function appendMyNewMessage(message) {
let t = document.getElementById('messageFromMe').content;
let txt = t.querySelector("[data-message]");
txt.textContent = message;
let clone = document.importNode(t, true);
messagesBox.appendChild(clone);
scrollToNewestMessage();
}
function appendOpponentNewMessage(message) {
let t = document.getElementById('messageFromOpponent').content;
let txt = t.querySelector("[data-message]");
txt.textContent = message;
let clone = document.importNode(t, true);
messagesBox.appendChild(clone);
scrollToNewestMessage();
}
socket.onmessage = function (event) {
// event.data may be:
// {"Content":"zxczc","SenderNickName":"Anonymous 1431","IsMe":true}
const serverMessage = JSON.parse(event.data);
if (serverMessage.IsMe) {
appendMyNewMessage(serverMessage.Content);
} else {
appendOpponentNewMessage(serverMessage.Content);
}
};
socket.onclose = function () {
setTimeout(function () {
initChat(playerId, gameId);
}, 1000);
};
}
export default initChat;
@@ -1,4 +1,4 @@
import {Chess} from "../node_modules/chess.js/dist/esm/chess.js";
import { Chess } from "../node_modules/chess.js/dist/esm/chess.js";
const initGameBoard = function (color, player, gameId) {
fetch(`/games/${gameId}.fen`)
@@ -132,7 +132,7 @@ const initGameBoard = function (color, player, gameId) {
socket.onclose = function () {
setTimeout(function () {
initGameBoard(player, gameId);
initGameBoard(color, player, gameId);
}, 1000);
};
});