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:
@@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user