Files
via/internal/examples/nats-chatroom/chat.go
Ryan Hamamura 719b389be6 refactor: split nats-chatroom into modules with profile, layout, and auth
Extract chat, profile, types, and user data into separate files.
Embed CSS via go:embed. Add a layout with nav, session-based profile
persistence, and a middleware guard that redirects to /profile when
no identity is set.
2026-02-13 10:55:04 -10:00

149 lines
3.0 KiB
Go

package main
import (
"sync"
"time"
"github.com/ryanhamamura/via"
"github.com/ryanhamamura/via/h"
)
var (
WithSignal = via.WithSignal
)
func ChatPage(c *via.Context) {
currentUser := UserInfo{
Name: c.Session().GetString(SessionKeyUsername),
Emoji: c.Session().GetString(SessionKeyEmoji),
}
roomSignal := c.Signal("Go")
statement := c.Signal("")
var messages []ChatMessage
var messagesMu sync.Mutex
currentRoom := "Go"
var currentSub via.Subscription
subscribeToRoom := func(room string) {
if currentSub != nil {
currentSub.Unsubscribe()
}
subject := "chat.room." + room
if hist, err := via.ReplayHistory[ChatMessage](v, subject, 50); err == nil {
messages = hist
}
sub, _ := via.Subscribe(c, subject, func(msg ChatMessage) {
messagesMu.Lock()
messages = append(messages, msg)
if len(messages) > 50 {
messages = messages[len(messages)-50:]
}
messagesMu.Unlock()
c.Sync()
})
currentSub = sub
currentRoom = room
}
subscribeToRoom("Go")
// Heartbeat — keeps connected indicator alive
connected := true
c.OnInterval(30*time.Second, func() {
connected = true
c.Sync()
})
switchRoom := c.Action(func() {
newRoom := roomSignal.String()
if newRoom != currentRoom {
messagesMu.Lock()
messages = nil
messagesMu.Unlock()
subscribeToRoom(newRoom)
c.Sync()
}
})
say := c.Action(func() {
msg := statement.String()
if msg == "" {
msg = randomDevQuote()
}
statement.SetValue("")
via.Publish(c, "chat.room."+currentRoom, ChatMessage{
User: currentUser,
Message: msg,
Time: time.Now().UnixMilli(),
})
})
c.View(func() h.H {
var tabs []h.H
for _, name := range roomNames {
isCurrent := name == currentRoom
tabs = append(tabs, h.Li(
h.A(
h.If(isCurrent, h.Attr("aria-current", "page")),
h.Text(name),
switchRoom.OnClick(WithSignal(roomSignal, name)),
),
))
}
messagesMu.Lock()
chatHistoryChildren := []h.H{
h.Class("chat-history"),
h.Script(h.Raw(`new MutationObserver(()=>scrollChatToBottom()).observe(document.querySelector('.chat-history'), {childList:true})`)),
}
for _, msg := range messages {
chatHistoryChildren = append(chatHistoryChildren,
h.Div(h.Class("chat-message"),
h.Div(h.Class("avatar"), h.Attr("title", msg.User.Name), h.Text(msg.User.Emoji)),
h.Div(h.Class("bubble"),
h.P(h.Text(msg.Message)),
),
),
)
}
messagesMu.Unlock()
_ = connected
return h.Div(h.Class("chat-page"),
h.Nav(
h.Attr("role", "tab-control"),
h.Ul(tabs...),
h.Span(h.Class("nats-badge"),
h.Span(h.Class("status-dot")),
h.Text("NATS"),
),
),
h.Div(chatHistoryChildren...),
h.Div(
h.Class("chat-input"),
h.DataIgnoreMorph(),
currentUser.Avatar(),
h.FieldSet(
h.Attr("role", "group"),
h.Input(
h.Type("text"),
h.Placeholder(currentUser.Name+" says..."),
statement.Bind(),
h.Attr("autofocus"),
say.OnKeyDown("Enter"),
),
h.Button(h.Text("Send"), say.OnClick()),
),
),
)
})
}