Skip to content

Commit 0cf5ca8

Browse files
committed
Add the chat widget to the dashboard
1 parent 20072c8 commit 0cf5ca8

File tree

3 files changed

+124
-47
lines changed

3 files changed

+124
-47
lines changed

pkg/streampanel/chat.go

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ type chatUI struct {
2828
CanvasObject fyne.CanvasObject
2929
Panel *Panel
3030
List *widget.List
31+
ItemHeight map[streamcontrol.ChatMessageID]uint
32+
TotalListHeight uint
3133
MessagesHistoryLocker sync.Mutex
3234
MessagesHistory []api.ChatMessage
3335
EnableButtons bool
36+
ReverseOrder bool
37+
OnAdd func(context.Context, api.ChatMessage)
38+
OnRemove func(context.Context, api.ChatMessage)
3439

3540
CapabilitiesCacheLocker sync.Mutex
3641
CapabilitiesCache map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}
@@ -44,6 +49,7 @@ type chatUI struct {
4449
func newChatUI(
4550
ctx context.Context,
4651
enableButtons bool,
52+
reverseOrder bool,
4753
panel *Panel,
4854
) (_ret *chatUI, _err error) {
4955
logger.Debugf(ctx, "newChatUI")
@@ -52,7 +58,9 @@ func newChatUI(
5258
ui := &chatUI{
5359
Panel: panel,
5460
EnableButtons: enableButtons,
61+
ReverseOrder: reverseOrder,
5562
CapabilitiesCache: make(map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}),
63+
ItemHeight: map[streamcontrol.ChatMessageID]uint{},
5664
ctx: ctx,
5765
}
5866
if err := ui.init(ctx); err != nil {
@@ -173,6 +181,9 @@ func (ui *chatUI) onReceiveMessage(
173181
) {
174182
logger.Debugf(ctx, "onReceiveMessage(ctx, %s)", spew.Sdump(msg))
175183
defer func() { logger.Tracef(ctx, "/onReceiveMessage(ctx, %s)", spew.Sdump(msg)) }()
184+
if onAdd := ui.OnAdd; onAdd != nil {
185+
defer onAdd(ctx, msg)
186+
}
176187
ui.MessagesHistoryLocker.Lock()
177188
defer ui.MessagesHistoryLocker.Unlock()
178189
ui.MessagesHistory = append(ui.MessagesHistory, msg)
@@ -290,7 +301,12 @@ func (ui *chatUI) listUpdateItem(
290301
ctx := context.TODO()
291302
ui.MessagesHistoryLocker.Lock()
292303
defer ui.MessagesHistoryLocker.Unlock()
293-
entryID := len(ui.MessagesHistory) - 1 - rowID
304+
var entryID int
305+
if ui.ReverseOrder {
306+
entryID = len(ui.MessagesHistory) - 1 - rowID
307+
} else {
308+
entryID = rowID
309+
}
294310
if entryID < 0 || entryID >= len(ui.MessagesHistory) {
295311
logger.Errorf(ctx, "invalid entry ID: %d", entryID)
296312
return
@@ -306,51 +322,53 @@ func (ui *chatUI) listUpdateItem(
306322
containerPtr := obj.(*fyne.Container)
307323
objs := containerPtr.Objects
308324
label := objs[0].(*widget.Label)
309-
subContainer := objs[1].(*fyne.Container)
310-
banUserButton := subContainer.Objects[0].(*widget.Button)
311-
banUserButton.OnTapped = func() {
312-
w := dialog.NewConfirm(
313-
"Banning an user",
314-
fmt.Sprintf("Are you sure you want to ban user '%s' on '%s'", msg.UserID, msg.Platform),
315-
func(b bool) {
316-
if !b {
317-
return
318-
}
319-
ui.onBanClicked(msg.Platform, msg.UserID)
320-
},
321-
ui.Panel.mainWindow,
322-
)
323-
w.Show()
324-
}
325-
if _, ok := platCaps[streamcontrol.CapabilityBanUser]; !ok {
326-
banUserButton.Disable()
327-
} else {
328-
banUserButton.Enable()
329-
}
330-
removeMsgButton := subContainer.Objects[1].(*widget.Button)
331-
removeMsgButton.OnTapped = func() {
332-
w := dialog.NewConfirm(
333-
"Removing a message",
334-
fmt.Sprintf("Are you sure you want to remove the message from '%s' on '%s'", msg.UserID, msg.Platform),
335-
func(b bool) {
336-
if !b {
337-
return
338-
}
339-
// TODO: think of consistency with onBanClicked
340-
ui.onRemoveClicked(entryID)
341-
},
342-
ui.Panel.mainWindow,
343-
)
344-
w.Show()
345-
}
346-
if _, ok := platCaps[streamcontrol.CapabilityDeleteChatMessage]; !ok {
347-
removeMsgButton.Disable()
348-
} else {
349-
removeMsgButton.Enable()
325+
if ui.EnableButtons {
326+
subContainer := objs[1].(*fyne.Container)
327+
banUserButton := subContainer.Objects[0].(*widget.Button)
328+
banUserButton.OnTapped = func() {
329+
w := dialog.NewConfirm(
330+
"Banning an user",
331+
fmt.Sprintf("Are you sure you want to ban user '%s' on '%s'", msg.UserID, msg.Platform),
332+
func(b bool) {
333+
if !b {
334+
return
335+
}
336+
ui.onBanClicked(msg.Platform, msg.UserID)
337+
},
338+
ui.Panel.mainWindow,
339+
)
340+
w.Show()
341+
}
342+
if _, ok := platCaps[streamcontrol.CapabilityBanUser]; !ok {
343+
banUserButton.Disable()
344+
} else {
345+
banUserButton.Enable()
346+
}
347+
removeMsgButton := subContainer.Objects[1].(*widget.Button)
348+
removeMsgButton.OnTapped = func() {
349+
w := dialog.NewConfirm(
350+
"Removing a message",
351+
fmt.Sprintf("Are you sure you want to remove the message from '%s' on '%s'", msg.UserID, msg.Platform),
352+
func(b bool) {
353+
if !b {
354+
return
355+
}
356+
// TODO: think of consistency with onBanClicked
357+
ui.onRemoveClicked(entryID)
358+
},
359+
ui.Panel.mainWindow,
360+
)
361+
w.Show()
362+
}
363+
if _, ok := platCaps[streamcontrol.CapabilityDeleteChatMessage]; !ok {
364+
removeMsgButton.Disable()
365+
} else {
366+
removeMsgButton.Enable()
367+
}
350368
}
351369
label.SetText(fmt.Sprintf(
352370
"%s: %s: %s: %s",
353-
msg.CreatedAt.Format("15:04"),
371+
msg.CreatedAt.Format("15:04:05"),
354372
msg.Platform,
355373
msg.Username,
356374
msg.Message,
@@ -359,6 +377,10 @@ func (ui *chatUI) listUpdateItem(
359377
requiredHeight := containerPtr.MinSize().Height
360378
logger.Tracef(ctx, "%d: requiredHeight == %f", rowID, requiredHeight)
361379

380+
prevHeight := ui.ItemHeight[msg.MessageID]
381+
ui.TotalListHeight += uint(requiredHeight) - prevHeight
382+
ui.ItemHeight[msg.MessageID] = uint(requiredHeight)
383+
362384
// TODO: think of how to get rid of this racy hack:
363385
observability.Go(ctx, func() { ui.List.SetItemHeight(rowID, requiredHeight) })
364386
}
@@ -391,7 +413,13 @@ func (ui *chatUI) onRemoveClicked(
391413
return
392414
}
393415
msg := ui.MessagesHistory[itemID]
416+
if onRemove := ui.OnRemove; onRemove != nil {
417+
defer onRemove(ctx, msg)
418+
}
394419
ui.MessagesHistory = append(ui.MessagesHistory[:itemID], ui.MessagesHistory[itemID+1:]...)
420+
prevHeight := ui.ItemHeight[msg.MessageID]
421+
ui.TotalListHeight -= prevHeight
422+
delete(ui.ItemHeight, msg.MessageID)
395423
err := ui.Panel.chatMessageRemove(ui.ctx, msg.Platform, msg.MessageID)
396424
if err != nil {
397425
ui.Panel.DisplayError(err)

pkg/streampanel/dashboard.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
3939
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
4040
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
41+
"github.com/xaionaro-go/streamctl/pkg/streamd/api"
4142
"github.com/xaionaro-go/streamctl/pkg/streamd/client"
4243
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
4344
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
@@ -88,6 +89,7 @@ type dashboardWindow struct {
8889
streamStatusLocker xsync.Mutex
8990
changedImages []*imageInfo
9091
lastFullUpdateAt time.Time
92+
chat *chatUI
9193

9294
iteratorReusableBuffer iterate.TwoDReusableBuffers
9395
}
@@ -247,9 +249,14 @@ func (w *dashboardWindow) renderStreamStatus(ctx context.Context) {
247249
func (p *Panel) newDashboardWindow(
248250
ctx context.Context,
249251
) *dashboardWindow {
252+
chatUI, err := newChatUI(ctx, false, false, p)
253+
if err != nil {
254+
p.DisplayError(fmt.Errorf("unable to start a chat UI: %w", err))
255+
}
250256
w := &dashboardWindow{
251257
Window: p.app.NewWindow("Dashboard"),
252258
Panel: p,
259+
chat: chatUI,
253260
streamStatus: map[streamcontrol.PlatformName]*widget.Label{
254261
obs.ID: widget.NewLabel(""),
255262
twitch.ID: widget.NewLabel(""),
@@ -292,18 +299,60 @@ func (p *Panel) newDashboardWindow(
292299
streamInfoItems.Add(container.NewHBox(layout.NewSpacer(), kcLabel, w.streamStatus[kick.ID]))
293300
streamInfoItems.Add(container.NewHBox(layout.NewSpacer(), ytLabel, w.streamStatus[youtube.ID]))
294301
streamInfoContainer := container.NewBorder(
295-
nil,
296302
nil,
297303
nil,
298304
streamInfoItems,
305+
nil,
299306
)
300307
w.imagesLayerObj = canvas.NewRaster(w.imagesLayer)
301308

302-
w.Window.SetContent(container.NewStack(
309+
layers := []fyne.CanvasObject{
303310
bgFyne,
311+
}
312+
if w.chat != nil {
313+
c := w.chat.List
314+
w.chat.OnAdd = func(ctx context.Context, _ api.ChatMessage) {
315+
screenHeight := w.Canvas().Size().Height
316+
demandedHeight := float32(w.chat.TotalListHeight) + c.Theme().Size(theme.SizeNamePadding)*float32(c.Length())
317+
logger.Tracef(ctx, "demanded height: %v; screen height: %v", demandedHeight, screenHeight)
318+
allowedHeight := math.Min(
319+
float64(screenHeight),
320+
float64(demandedHeight),
321+
)
322+
logger.Tracef(ctx, "allowed height: %v", allowedHeight)
323+
pos := fyne.NewPos(0, screenHeight-float32(allowedHeight))
324+
size := fyne.NewSize(w.Canvas().Size().Width, float32(allowedHeight))
325+
logger.Tracef(ctx, "resulting size and position: %#+v %#+v", size, pos)
326+
c.Resize(size)
327+
c.Move(pos)
328+
329+
c.ScrollToBottom()
330+
c.Refresh()
331+
}
332+
w.chat.OnAdd(ctx, api.ChatMessage{})
333+
layers = append(layers,
334+
c,
335+
)
336+
observability.Go(ctx, func() {
337+
t := time.NewTicker(time.Second)
338+
defer t.Stop()
339+
for {
340+
select {
341+
case <-ctx.Done():
342+
return
343+
case <-t.C:
344+
w.chat.OnAdd(ctx, api.ChatMessage{})
345+
}
346+
}
347+
})
348+
}
349+
layers = append(layers,
304350
w.imagesLayerObj,
305351
streamInfoContainer,
306-
))
352+
)
353+
354+
stack := container.NewStack(layers...)
355+
w.Window.SetContent(stack)
307356
w.Window.Show()
308357
return w
309358
}

pkg/streampanel/panel.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1658,7 +1658,7 @@ func (p *Panel) initMainWindow(
16581658
)
16591659

16601660
chatPage := container.NewBorder(nil, nil, nil, nil)
1661-
chatUI, err := newChatUI(ctx, true, p)
1661+
chatUI, err := newChatUI(ctx, true, true, p)
16621662
if err != nil {
16631663
logger.Errorf(ctx, "unable to initialize the page for chat: %v", err)
16641664
} else {

0 commit comments

Comments
 (0)