Chatroom 2 (#10)
* Remove unused method. Don't panic if unknown room. * Need a Connected() check for rooms publishing - don't do the work of rendering for a dead connection * Make vars private. * Linter issues * Remove Connected() * Mutation observer. Publish 4x / second. --------- Co-authored-by: João Gonçalves <joao.goncalves01@gmail.com>
This commit is contained in:
@@ -83,7 +83,7 @@ func (a *actionTrigger) OnChange(options ...ActionTriggerOption) h.H {
|
|||||||
return h.Data("on:change__debounce.200ms", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data("on:change__debounce.200ms", buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OneyDown returns a via.h DOM attribute that triggers when a key is pressed.
|
// OnKeyDown returns a via.h DOM attribute that triggers when a key is pressed.
|
||||||
// key: optional, see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
// key: optional, see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
||||||
// Example: OnKeyDown("Enter")
|
// Example: OnKeyDown("Enter")
|
||||||
func (a *actionTrigger) OnKeyDown(key string, options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnKeyDown(key string, options ...ActionTriggerOption) h.H {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
// Plugin is a func that can mutate the given *via.V app runtime. It is useful to integrate popular JS/CSS UI libraries or tools.
|
// Plugin is a func that can mutate the given *via.V app runtime. It is useful to integrate popular JS/CSS UI libraries or tools.
|
||||||
type Plugin func(v *V)
|
type Plugin func(v *V)
|
||||||
|
|
||||||
// Config defines configuration options for the via application.
|
// Options defines configuration options for the via application
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// The development mode flag. If true, enables server and browser auto-reload on `.go` file changes.
|
// The development mode flag. If true, enables server and browser auto-reload on `.go` file changes.
|
||||||
DevMode bool
|
DevMode bool
|
||||||
|
|||||||
32
context.go
32
context.go
@@ -182,9 +182,7 @@ func (c *Context) injectSignals(sigs map[string]any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync pushes the current view state and signal changes to the browser immediately
|
func (c *Context) getSSE() *datastar.ServerSentEventGenerator {
|
||||||
// over the live SSE event stream.
|
|
||||||
func (c *Context) Sync() {
|
|
||||||
// components use parent page sse stream
|
// components use parent page sse stream
|
||||||
var sse *datastar.ServerSentEventGenerator
|
var sse *datastar.ServerSentEventGenerator
|
||||||
if c.isComponent() {
|
if c.isComponent() {
|
||||||
@@ -192,6 +190,13 @@ func (c *Context) Sync() {
|
|||||||
} else {
|
} else {
|
||||||
sse = c.sse
|
sse = c.sse
|
||||||
}
|
}
|
||||||
|
return sse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync pushes the current view state and signal changes to the browser immediately
|
||||||
|
// over the live SSE event stream.
|
||||||
|
func (c *Context) Sync() {
|
||||||
|
sse := c.getSSE()
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logWarn(c, "view out of sync: no sse stream")
|
c.app.logWarn(c, "view out of sync: no sse stream")
|
||||||
return
|
return
|
||||||
@@ -234,12 +239,7 @@ func (c *Context) Sync() {
|
|||||||
// Then, the merge will only occur if the ID of the top level element in the patch
|
// Then, the merge will only occur if the ID of the top level element in the patch
|
||||||
// matches 'my-element'.
|
// matches 'my-element'.
|
||||||
func (c *Context) SyncElements(elem h.H) {
|
func (c *Context) SyncElements(elem h.H) {
|
||||||
var sse *datastar.ServerSentEventGenerator
|
sse := c.getSSE()
|
||||||
if c.isComponent() {
|
|
||||||
sse = c.parentPageCtx.sse
|
|
||||||
} else {
|
|
||||||
sse = c.sse
|
|
||||||
}
|
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logWarn(c, "elements out of sync: no sse stream")
|
c.app.logWarn(c, "elements out of sync: no sse stream")
|
||||||
return
|
return
|
||||||
@@ -260,12 +260,7 @@ func (c *Context) SyncElements(elem h.H) {
|
|||||||
// SyncSignals pushes the current signal changes to the browser immediately
|
// SyncSignals pushes the current signal changes to the browser immediately
|
||||||
// over the live SSE event stream.
|
// over the live SSE event stream.
|
||||||
func (c *Context) SyncSignals() {
|
func (c *Context) SyncSignals() {
|
||||||
var sse *datastar.ServerSentEventGenerator
|
sse := c.getSSE()
|
||||||
if c.isComponent() {
|
|
||||||
sse = c.parentPageCtx.sse
|
|
||||||
} else {
|
|
||||||
sse = c.sse
|
|
||||||
}
|
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logWarn(c, "signals out of sync: no sse stream")
|
c.app.logWarn(c, "signals out of sync: no sse stream")
|
||||||
return
|
return
|
||||||
@@ -285,12 +280,7 @@ func (c *Context) SyncSignals() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) ExecScript(s string) {
|
func (c *Context) ExecScript(s string) {
|
||||||
var sse *datastar.ServerSentEventGenerator
|
sse := c.getSSE()
|
||||||
if c.isComponent() {
|
|
||||||
sse = c.parentPageCtx.sse
|
|
||||||
} else {
|
|
||||||
sse = c.sse
|
|
||||||
}
|
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logWarn(c, "script out of sync: no sse stream")
|
c.app.logWarn(c, "script out of sync: no sse stream")
|
||||||
return
|
return
|
||||||
|
|||||||
2
h/h.go
2
h/h.go
@@ -31,7 +31,7 @@ func Textf(format string, a ...any) H {
|
|||||||
return g.Textf(format, a...)
|
return g.Textf(format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// / Raw creates a text DOM [Node] that just Renders the unescaped string t.
|
// Raw creates a text DOM [Node] that just Renders the unescaped string t.
|
||||||
func Raw(s string) H {
|
func Raw(s string) H {
|
||||||
return g.Raw(s)
|
return g.Raw(s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ func main() {
|
|||||||
`)), h.Script(h.Raw(`
|
`)), h.Script(h.Raw(`
|
||||||
function scrollChatToBottom() {
|
function scrollChatToBottom() {
|
||||||
const chatHistory = document.querySelector('.chat-history');
|
const chatHistory = document.querySelector('.chat-history');
|
||||||
if (chatHistory) chatHistory.scrollTop = chatHistory.scrollHeight;
|
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
setInterval(scrollChatToBottom, 100);
|
|
||||||
`)),
|
`)),
|
||||||
)
|
)
|
||||||
rooms := NewRooms[Chat, UserInfo]("Clojure", "Dotnet", "Go", "Java", "JS", "Kotlin", "Python", "Rust")
|
rooms := NewRooms[Chat, UserInfo]("Clojure", "Dotnet", "Go", "Java", "JS", "Kotlin", "Python", "Rust")
|
||||||
@@ -100,7 +99,10 @@ func main() {
|
|||||||
var currentRoom *Room[Chat, UserInfo]
|
var currentRoom *Room[Chat, UserInfo]
|
||||||
|
|
||||||
switchRoom := func() {
|
switchRoom := func() {
|
||||||
newRoom, _ := rooms.Get(string(roomName.String()))
|
newRoom, ok := rooms.Get(string(roomName.String()))
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Println(">> switchRoom to ", newRoom.Name)
|
fmt.Println(">> switchRoom to ", newRoom.Name)
|
||||||
if currentRoom != nil && currentRoom != newRoom {
|
if currentRoom != nil && currentRoom != newRoom {
|
||||||
fmt.Println("LEAVING old room")
|
fmt.Println("LEAVING old room")
|
||||||
@@ -129,8 +131,8 @@ func main() {
|
|||||||
if currentRoom != nil {
|
if currentRoom != nil {
|
||||||
currentRoom.UpdateData(func(chat *Chat) {
|
currentRoom.UpdateData(func(chat *Chat) {
|
||||||
chat.Entries = append(chat.Entries, ChatEntry{
|
chat.Entries = append(chat.Entries, ChatEntry{
|
||||||
User: currentUser,
|
user: currentUser,
|
||||||
Message: msg,
|
message: msg,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
statement.SetValue("")
|
statement.SetValue("")
|
||||||
@@ -178,10 +180,10 @@ func main() {
|
|||||||
})
|
})
|
||||||
for _, entry := range chat.Entries {
|
for _, entry := range chat.Entries {
|
||||||
|
|
||||||
messageChildren := []h.H{h.Class("chat-message"), entry.User.Avatar()}
|
messageChildren := []h.H{h.Class("chat-message"), entry.user.Avatar()}
|
||||||
messageChildren = append(messageChildren,
|
messageChildren = append(messageChildren,
|
||||||
h.Div(h.Class("bubble"),
|
h.Div(h.Class("bubble"),
|
||||||
h.P(h.Text(entry.Message)),
|
h.P(h.Text(entry.message)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,7 +191,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chatHistory := []h.H{h.Class("chat-history")}
|
chatHistory := []h.H{
|
||||||
|
h.Class("chat-history"),
|
||||||
|
h.Script(h.Raw(`new MutationObserver((mutations)=>{scrollChatToBottom()}).observe(document.querySelector('.chat-history'), {childList:true})`)),
|
||||||
|
}
|
||||||
chatHistory = append(chatHistory, messages...)
|
chatHistory = append(chatHistory, messages...)
|
||||||
|
|
||||||
return h.Main(h.Class("container"),
|
return h.Main(h.Class("container"),
|
||||||
@@ -205,7 +210,7 @@ func main() {
|
|||||||
h.Attr("role", "group"),
|
h.Attr("role", "group"),
|
||||||
h.Input(
|
h.Input(
|
||||||
h.Type("text"),
|
h.Type("text"),
|
||||||
h.Placeholder(fmt.Sprintf("%s says...", currentUser.Name)),
|
h.Placeholder(currentUser.Name+" says..."),
|
||||||
statement.Bind(),
|
statement.Bind(),
|
||||||
h.Attr("autofocus"),
|
h.Attr("autofocus"),
|
||||||
say.OnKeyDown("Enter"),
|
say.OnKeyDown("Enter"),
|
||||||
@@ -234,8 +239,8 @@ func (u *UserInfo) Avatar() h.H {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChatEntry struct {
|
type ChatEntry struct {
|
||||||
User UserInfo
|
user UserInfo
|
||||||
Message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (r *Room[TR, TU]) Publish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get room data. This is a copy.
|
// GetData returns a copy of room data.
|
||||||
// Accepts an optional subset function to transform data before copying.
|
// Accepts an optional subset function to transform data before copying.
|
||||||
func (r *Room[TR, TU]) GetData(subsetFn ...func(*TR) TR) TR {
|
func (r *Room[TR, TU]) GetData(subsetFn ...func(*TR) TR) TR {
|
||||||
r.dataMu.RLock()
|
r.dataMu.RLock()
|
||||||
@@ -116,12 +116,6 @@ func (r *Room[TR, TU]) Leave(u *TU) {
|
|||||||
r.leave <- u
|
r.leave <- u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Room[TR, TU]) MemberCount() int {
|
|
||||||
r.membersMu.RLock()
|
|
||||||
defer r.membersMu.RUnlock()
|
|
||||||
return len(r.members)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRoom[TR any, TU comparable](n string) *Room[TR, TU] {
|
func NewRoom[TR any, TU comparable](n string) *Room[TR, TU] {
|
||||||
return &Room[TR, TU]{
|
return &Room[TR, TU]{
|
||||||
Name: n,
|
Name: n,
|
||||||
@@ -135,7 +129,7 @@ func NewRoom[TR any, TU comparable](n string) *Room[TR, TU] {
|
|||||||
|
|
||||||
func (r *Room[TR, TU]) run() {
|
func (r *Room[TR, TU]) run() {
|
||||||
defer close(r.done)
|
defer close(r.done)
|
||||||
publishTicker := time.NewTicker(400 * time.Millisecond)
|
publishTicker := time.NewTicker(250 * time.Millisecond)
|
||||||
defer publishTicker.Stop()
|
defer publishTicker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ func (ds *DummySyncable) Sync() {
|
|||||||
ds.room.GetData()
|
ds.room.GetData()
|
||||||
ds.timesCalled++
|
ds.timesCalled++
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoomJoinLeaveChannels(t *testing.T) {
|
func TestRoomJoinLeaveChannels(t *testing.T) {
|
||||||
rooms := NewRooms[RoomData, TestUserInfo](string("a"))
|
rooms := NewRooms[RoomData, TestUserInfo](string("a"))
|
||||||
rm, _ := rooms.Get("a")
|
rm, _ := rooms.Get("a")
|
||||||
@@ -75,7 +74,7 @@ func TestRoomJoinLeaveChannels(t *testing.T) {
|
|||||||
time.Sleep(1 * time.Millisecond)
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
|
||||||
assert.Equal(t, rm.dirty, false)
|
assert.Equal(t, rm.dirty, false)
|
||||||
assert.Equal(t, rm.MemberCount(), 1)
|
assert.Equal(t, len(rm.members), 1)
|
||||||
|
|
||||||
// Room Data
|
// Room Data
|
||||||
rm.UpdateData(func(data *RoomData) {
|
rm.UpdateData(func(data *RoomData) {
|
||||||
@@ -96,5 +95,5 @@ func TestRoomJoinLeaveChannels(t *testing.T) {
|
|||||||
rm.Leave(&u1)
|
rm.Leave(&u1)
|
||||||
time.Sleep(1 * time.Millisecond)
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
|
||||||
assert.Equal(t, rm.MemberCount(), 0)
|
assert.Equal(t, len(rm.members), 0)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user