diff --git a/db/gen/chat.sql.go b/db/gen/chat.sql.go new file mode 100644 index 0000000..2d956cd --- /dev/null +++ b/db/gen/chat.sql.go @@ -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 +} diff --git a/db/gen/models.go b/db/gen/models.go index b7e8603..f539b79 100644 --- a/db/gen/models.go +++ b/db/gen/models.go @@ -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 diff --git a/db/migrations/006_add_chat_messages.sql b/db/migrations/006_add_chat_messages.sql new file mode 100644 index 0000000..d800f71 --- /dev/null +++ b/db/migrations/006_add_chat_messages.sql @@ -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; diff --git a/db/persister.go b/db/persister.go index 3fb16c0..5e68b17 100644 --- a/db/persister.go +++ b/db/persister.go @@ -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 +} diff --git a/db/queries/chat.sql b/db/queries/chat.sql new file mode 100644 index 0000000..5f14840 --- /dev/null +++ b/db/queries/chat.sql @@ -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; diff --git a/main.go b/main.go index d941c15..a74a5f2 100644 --- a/main.go +++ b/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