refactor: extract standalone chat package from game-specific handlers
Create chat/ package with Message type, Room (NATS pub/sub + buffer), DB persistence helpers, and a unified templ component parameterized by Config (CSS prefix, post URL, color function, key propagation). Both c4game and snakegame now use chat.Room for message management and chatcomponents.Chat for rendering, eliminating the duplicated ChatMessage types, chat templ components, chatAutoScroll scripts, color functions, and inline buffer management.
This commit is contained in:
@@ -1,22 +1,39 @@
|
||||
package snakegame
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
|
||||
"github.com/ryanhamamura/c4/chat"
|
||||
chatcomponents "github.com/ryanhamamura/c4/chat/components"
|
||||
"github.com/ryanhamamura/c4/features/snakegame/components"
|
||||
"github.com/ryanhamamura/c4/features/snakegame/pages"
|
||||
"github.com/ryanhamamura/c4/sessions"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
)
|
||||
|
||||
func snakeChatColor(slot int) string {
|
||||
if slot >= 0 && slot < len(snake.SnakeColors) {
|
||||
return snake.SnakeColors[slot]
|
||||
}
|
||||
return "#666"
|
||||
}
|
||||
|
||||
func snakeChatConfig(gameID string) chatcomponents.Config {
|
||||
return chatcomponents.Config{
|
||||
CSSPrefix: "snake",
|
||||
PostURL: fmt.Sprintf("/snake/%s/chat", gameID),
|
||||
Color: snakeChatColor,
|
||||
StopKeyPropagation: true,
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSnakePage(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gameID := chi.URLParam(r, "id")
|
||||
@@ -32,20 +49,19 @@ func HandleSnakePage(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.
|
||||
|
||||
// Auto-join if nickname exists and not already in game
|
||||
if nickname != "" && si.GetPlayerSlot(playerID) < 0 {
|
||||
player := &snake.Player{
|
||||
p := &snake.Player{
|
||||
ID: playerID,
|
||||
Nickname: nickname,
|
||||
}
|
||||
if userID != "" {
|
||||
player.UserID = &userID
|
||||
p.UserID = &userID
|
||||
}
|
||||
si.Join(player)
|
||||
si.Join(p)
|
||||
}
|
||||
|
||||
mySlot := si.GetPlayerSlot(playerID)
|
||||
|
||||
if mySlot < 0 {
|
||||
// Not in game yet
|
||||
isGuest := r.URL.Query().Get("guest") == "1"
|
||||
if userID == "" && !isGuest {
|
||||
if err := pages.JoinPage(gameID).Render(r.Context(), w); err != nil {
|
||||
@@ -60,7 +76,7 @@ func HandleSnakePage(snakeStore *snake.SnakeStore, sm *scs.SessionManager) http.
|
||||
}
|
||||
|
||||
sg := si.GetGame()
|
||||
if err := pages.GamePage(sg, mySlot, nil, gameID).Render(r.Context(), w); err != nil {
|
||||
if err := pages.GamePage(sg, mySlot, nil, snakeChatConfig(gameID), gameID).Render(r.Context(), w); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -82,13 +98,15 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
|
||||
datastar.WithBrotli(datastar.WithBrotliLevel(5)),
|
||||
))
|
||||
|
||||
chatCfg := snakeChatConfig(gameID)
|
||||
|
||||
// Send initial render
|
||||
sg := si.GetGame()
|
||||
sse.PatchElementTempl(components.Board(sg)) //nolint:errcheck
|
||||
sse.PatchElementTempl(components.StatusBanner(sg, mySlot, gameID)) //nolint:errcheck
|
||||
sse.PatchElementTempl(components.PlayerList(sg, mySlot)) //nolint:errcheck
|
||||
if sg.Mode == snake.ModeMultiplayer {
|
||||
sse.PatchElementTempl(components.Chat(nil, gameID)) //nolint:errcheck
|
||||
sse.PatchElementTempl(chatcomponents.Chat(nil, chatCfg)) //nolint:errcheck
|
||||
if sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown {
|
||||
sse.PatchElementTempl(components.InviteLink(gameID)) //nolint:errcheck
|
||||
}
|
||||
@@ -105,12 +123,11 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
|
||||
// Chat subscription (multiplayer only)
|
||||
var chatCh chan *nats.Msg
|
||||
var chatSub *nats.Subscription
|
||||
var chatMessages []components.ChatMessage
|
||||
var chatMu sync.Mutex
|
||||
var room *chat.Room
|
||||
|
||||
if sg.Mode == snake.ModeMultiplayer {
|
||||
chatCh = make(chan *nats.Msg, 64)
|
||||
chatSub, err = nc.ChanSubscribe("snake.chat."+gameID, chatCh)
|
||||
room = chat.NewRoom(nc, "snake.chat."+gameID, nil)
|
||||
chatCh, chatSub, err = room.Subscribe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -153,20 +170,11 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
var cm components.ChatMessage
|
||||
if err := json.Unmarshal(msg.Data, &cm); err != nil {
|
||||
_, snapshot := room.Receive(msg.Data)
|
||||
if snapshot == nil {
|
||||
continue
|
||||
}
|
||||
chatMu.Lock()
|
||||
chatMessages = append(chatMessages, cm)
|
||||
if len(chatMessages) > 50 {
|
||||
chatMessages = chatMessages[len(chatMessages)-50:]
|
||||
}
|
||||
msgs := make([]components.ChatMessage, len(chatMessages))
|
||||
copy(msgs, chatMessages)
|
||||
chatMu.Unlock()
|
||||
|
||||
if err := sse.PatchElementTempl(components.Chat(msgs, gameID)); err != nil {
|
||||
if err := sse.PatchElementTempl(chatcomponents.Chat(snapshot, chatCfg)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -233,16 +241,14 @@ func HandleSendChat(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Session
|
||||
}
|
||||
|
||||
sg := si.GetGame()
|
||||
cm := components.ChatMessage{
|
||||
msg := chat.Message{
|
||||
Nickname: sg.Players[slot].Nickname,
|
||||
Slot: slot,
|
||||
Message: signals.ChatMsg,
|
||||
}
|
||||
data, err := json.Marshal(cm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nc.Publish("snake.chat."+gameID, data) //nolint:errcheck
|
||||
|
||||
room := chat.NewRoom(nc, "snake.chat."+gameID, nil)
|
||||
room.Send(msg)
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
sse.MarshalAndPatchSignals(map[string]any{"chatMsg": ""}) //nolint:errcheck
|
||||
@@ -278,14 +284,14 @@ func HandleSetNickname(snakeStore *snake.SnakeStore, sm *scs.SessionManager) htt
|
||||
userID := sessions.GetUserID(sm, r)
|
||||
|
||||
if si.GetPlayerSlot(playerID) < 0 {
|
||||
player := &snake.Player{
|
||||
p := &snake.Player{
|
||||
ID: playerID,
|
||||
Nickname: signals.Nickname,
|
||||
}
|
||||
if userID != "" {
|
||||
player.UserID = &userID
|
||||
p.UserID = &userID
|
||||
}
|
||||
si.Join(player)
|
||||
si.Join(p)
|
||||
}
|
||||
|
||||
sse := datastar.NewSSE(w, r)
|
||||
|
||||
Reference in New Issue
Block a user