feat: persist chat messages to SQLite
Chat messages were ephemeral — lost on page refresh or late join. Add a chat_messages table and load the last 50 messages on connect so players see conversation history.
This commit is contained in:
71
db/gen/chat.sql.go
Normal file
71
db/gen/chat.sql.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: chat.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createChatMessage = `-- name: CreateChatMessage :exec
|
||||
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type CreateChatMessageParams struct {
|
||||
GameID string
|
||||
Nickname string
|
||||
Color int64
|
||||
Message string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createChatMessage,
|
||||
arg.GameID,
|
||||
arg.Nickname,
|
||||
arg.Color,
|
||||
arg.Message,
|
||||
arg.CreatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getChatMessages = `-- name: GetChatMessages :many
|
||||
SELECT id, game_id, nickname, color, message, created_at FROM chat_messages
|
||||
WHERE game_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 50
|
||||
`
|
||||
|
||||
func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMessage, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChatMessages, gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ChatMessage
|
||||
for rows.Next() {
|
||||
var i ChatMessage
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GameID,
|
||||
&i.Nickname,
|
||||
&i.Color,
|
||||
&i.Message,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -8,6 +8,15 @@ import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
ID int64
|
||||
GameID string
|
||||
Nickname string
|
||||
Color int64
|
||||
Message string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
ID string
|
||||
Board string
|
||||
|
||||
15
db/migrations/006_add_chat_messages.sql
Normal file
15
db/migrations/006_add_chat_messages.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE chat_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
game_id TEXT NOT NULL,
|
||||
nickname TEXT NOT NULL,
|
||||
color INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_chat_messages_game ON chat_messages(game_id, created_at);
|
||||
|
||||
-- +goose Down
|
||||
DROP INDEX IF EXISTS idx_chat_messages_game;
|
||||
DROP TABLE IF EXISTS chat_messages;
|
||||
@@ -3,10 +3,12 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"slices"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/gen"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
"github.com/ryanhamamura/c4/ui"
|
||||
)
|
||||
|
||||
type GamePersister struct {
|
||||
@@ -286,3 +288,40 @@ func (p *SnakePersister) DeleteSnakeGame(id string) error {
|
||||
ctx := context.Background()
|
||||
return p.queries.DeleteSnakeGame(ctx, id)
|
||||
}
|
||||
|
||||
type ChatPersister struct {
|
||||
queries *gen.Queries
|
||||
}
|
||||
|
||||
func NewChatPersister(q *gen.Queries) *ChatPersister {
|
||||
return &ChatPersister{queries: q}
|
||||
}
|
||||
|
||||
func (p *ChatPersister) SaveChatMessage(gameID string, msg ui.C4ChatMessage) error {
|
||||
return p.queries.CreateChatMessage(context.Background(), gen.CreateChatMessageParams{
|
||||
GameID: gameID,
|
||||
Nickname: msg.Nickname,
|
||||
Color: int64(msg.Color),
|
||||
Message: msg.Message,
|
||||
CreatedAt: msg.Time,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *ChatPersister) LoadChatMessages(gameID string) ([]ui.C4ChatMessage, error) {
|
||||
rows, err := p.queries.GetChatMessages(context.Background(), gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs := make([]ui.C4ChatMessage, len(rows))
|
||||
for i, r := range rows {
|
||||
msgs[i] = ui.C4ChatMessage{
|
||||
Nickname: r.Nickname,
|
||||
Color: int(r.Color),
|
||||
Message: r.Message,
|
||||
Time: r.CreatedAt,
|
||||
}
|
||||
}
|
||||
// Query returns newest-first; reverse to oldest-first for display
|
||||
slices.Reverse(msgs)
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
9
db/queries/chat.sql
Normal file
9
db/queries/chat.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- name: CreateChatMessage :exec
|
||||
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
-- name: GetChatMessages :many
|
||||
SELECT * FROM chat_messages
|
||||
WHERE game_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 50;
|
||||
11
main.go
11
main.go
@@ -27,9 +27,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
store = game.NewGameStore()
|
||||
snakeStore = snake.NewSnakeStore()
|
||||
queries *gen.Queries
|
||||
store = game.NewGameStore()
|
||||
snakeStore = snake.NewSnakeStore()
|
||||
queries *gen.Queries
|
||||
chatPersister *db.ChatPersister
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
@@ -61,6 +62,7 @@ func main() {
|
||||
queries = gen.New(db.DB)
|
||||
store.SetPersister(db.NewGamePersister(queries))
|
||||
snakeStore.SetPersister(db.NewSnakePersister(queries))
|
||||
chatPersister = db.NewChatPersister(queries)
|
||||
|
||||
sessionManager, err := via.NewSQLiteSessionManager(db.DB)
|
||||
if err != nil {
|
||||
@@ -356,7 +358,7 @@ func main() {
|
||||
colSignal := c.Signal(0)
|
||||
showGuestPrompt := c.Signal(false)
|
||||
chatMsg := c.Signal("")
|
||||
var chatMessages []ui.C4ChatMessage
|
||||
chatMessages, _ := chatPersister.LoadChatMessages(gameID)
|
||||
var chatMu sync.Mutex
|
||||
|
||||
goToLogin := c.Action(func() {
|
||||
@@ -462,6 +464,7 @@ func main() {
|
||||
Message: msg,
|
||||
Time: time.Now().UnixMilli(),
|
||||
}
|
||||
chatPersister.SaveChatMessage(gameID, cm)
|
||||
data, err := json.Marshal(cm)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user