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.
149 lines
3.0 KiB
Go
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()),
|
|
),
|
|
),
|
|
)
|
|
})
|
|
}
|