Skip to content

Commit 4723f27

Browse files
committed
Format chat messages
1 parent 0cf5ca8 commit 4723f27

File tree

7 files changed

+185
-76
lines changed

7 files changed

+185
-76
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ require (
310310
github.com/xaionaro-go/typing v0.0.0-20221123235249-2229101d38ba
311311
github.com/xaionaro-go/unsafetools v0.0.0-20241024014258-a46e1ce3763e
312312
github.com/xaionaro-go/xcontext v0.0.0-20250111150717-e70e1f5b299c
313-
github.com/xaionaro-go/xfyne v0.0.0-20241018233531-26123724a6c6
313+
github.com/xaionaro-go/xfyne v0.0.0-20250615190411-4c96281f6e25
314314
github.com/xaionaro-go/xlogrus v0.0.0-20250111150201-60557109545a
315315
github.com/xaionaro-go/xpath v0.0.0-20250111145115-55f5728f643f
316316
github.com/xaionaro-go/xsync v0.0.0-20250614210231-b74f647f859f

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,8 +1178,8 @@ github.com/xaionaro-go/unsafetools v0.0.0-20241024014258-a46e1ce3763e h1:FV+/FVP
11781178
github.com/xaionaro-go/unsafetools v0.0.0-20241024014258-a46e1ce3763e/go.mod h1:ERewyGVM0zYnWA9nxdHPIC3xc9Yrf5CgAnBITuP3FRE=
11791179
github.com/xaionaro-go/xcontext v0.0.0-20250111150717-e70e1f5b299c h1://sE/WLpO7wpcVsp1FfTNAG9JmL60KfXiogNVKYivTA=
11801180
github.com/xaionaro-go/xcontext v0.0.0-20250111150717-e70e1f5b299c/go.mod h1:MGRT1+2m2adVRc4aAn0RVGysjsSRKN9VCyBJLHvQd4k=
1181-
github.com/xaionaro-go/xfyne v0.0.0-20241018233531-26123724a6c6 h1:UI1MghFC+1yGP1EkDmCOVV7k25fQTL0HM4cHnXLc/uM=
1182-
github.com/xaionaro-go/xfyne v0.0.0-20241018233531-26123724a6c6/go.mod h1:PAiPIoLcqxpi4P0st8qiIK+cWfGQM0iWBE30SH8+xOQ=
1181+
github.com/xaionaro-go/xfyne v0.0.0-20250615190411-4c96281f6e25 h1:zGpt+U8zOCB/gMOpJZ1IzqjfU85fvbL9LUvSL/R/xEk=
1182+
github.com/xaionaro-go/xfyne v0.0.0-20250615190411-4c96281f6e25/go.mod h1:52pwXTJFBlreHnA0sf8EoJvw5MZZNPSRm9kpdSqHOuk=
11831183
github.com/xaionaro-go/xlogrus v0.0.0-20250111150201-60557109545a h1:EoNRdOtBMnZedKfW7/4xtGmGHC+L+jfmLe5UOnF1tqc=
11841184
github.com/xaionaro-go/xlogrus v0.0.0-20250111150201-60557109545a/go.mod h1:RPfWNuwqJykkA2TEvisXqHgy1ypA/1H2HBdIRSeVJ9o=
11851185
github.com/xaionaro-go/xpath v0.0.0-20250111145115-55f5728f643f h1:ofxY1akRlVdJ/AEDj0EakK4Aez8fzeWTTe2mCAZiJ0A=

pkg/streampanel/chat.go

Lines changed: 119 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ type chatUI struct {
2828
CanvasObject fyne.CanvasObject
2929
Panel *Panel
3030
List *widget.List
31-
ItemHeight map[streamcontrol.ChatMessageID]uint
32-
TotalListHeight uint
33-
MessagesHistoryLocker sync.Mutex
31+
MessagesHistoryLocker xsync.Mutex
3432
MessagesHistory []api.ChatMessage
3533
EnableButtons bool
3634
ReverseOrder bool
35+
SuppressNotifications bool
3736
OnAdd func(context.Context, api.ChatMessage)
3837
OnRemove func(context.Context, api.ChatMessage)
3938

@@ -42,6 +41,11 @@ type chatUI struct {
4241

4342
CurrentlyPlayingChatMessageSoundCount int32
4443

44+
ItemLocker xsync.Mutex
45+
ItemsByCanvasObject map[fyne.CanvasObject]*chatItem
46+
ItemsByMessageID map[streamcontrol.ChatMessageID]*chatItem
47+
TotalListHeight uint
48+
4549
// TODO: do not store ctx in a struct:
4650
ctx context.Context
4751
}
@@ -50,18 +54,21 @@ func newChatUI(
5054
ctx context.Context,
5155
enableButtons bool,
5256
reverseOrder bool,
57+
suppressNotifications bool,
5358
panel *Panel,
5459
) (_ret *chatUI, _err error) {
5560
logger.Debugf(ctx, "newChatUI")
5661
defer func() { logger.Debugf(ctx, "/newChatUI: %v %v", _ret, _err) }()
5762

5863
ui := &chatUI{
59-
Panel: panel,
60-
EnableButtons: enableButtons,
61-
ReverseOrder: reverseOrder,
62-
CapabilitiesCache: make(map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}),
63-
ItemHeight: map[streamcontrol.ChatMessageID]uint{},
64-
ctx: ctx,
64+
Panel: panel,
65+
EnableButtons: enableButtons,
66+
ReverseOrder: reverseOrder,
67+
SuppressNotifications: suppressNotifications,
68+
CapabilitiesCache: make(map[streamcontrol.PlatformName]map[streamcontrol.Capability]struct{}),
69+
ItemsByCanvasObject: map[fyne.CanvasObject]*chatItem{},
70+
ItemsByMessageID: map[streamcontrol.ChatMessageID]*chatItem{},
71+
ctx: ctx,
6572
}
6673
if err := ui.init(ctx); err != nil {
6774
return nil, err
@@ -169,7 +176,7 @@ func (ui *chatUI) messageReceiverLoop(
169176
logger.Errorf(ctx, "message channel got closed")
170177
return
171178
}
172-
ui.onReceiveMessage(ctx, msg, false)
179+
ui.onReceiveMessage(ctx, msg, ui.SuppressNotifications)
173180
}
174181
}
175182
}
@@ -184,12 +191,20 @@ func (ui *chatUI) onReceiveMessage(
184191
if onAdd := ui.OnAdd; onAdd != nil {
185192
defer onAdd(ctx, msg)
186193
}
187-
ui.MessagesHistoryLocker.Lock()
188-
defer ui.MessagesHistoryLocker.Unlock()
194+
ui.MessagesHistoryLocker.ManualLock(ctx)
195+
prevLen := len(ui.MessagesHistory)
196+
defer ui.List.RefreshItem(prevLen)
197+
defer ui.MessagesHistoryLocker.ManualUnlock(ctx)
189198
ui.MessagesHistory = append(ui.MessagesHistory, msg)
190-
observability.Go(ctx, func() {
191-
ui.List.Refresh()
199+
notificationsEnabled := xsync.DoR1(ctx, &ui.Panel.configLocker, func() bool {
200+
return ui.Panel.Config.Chat.NotificationsEnabled()
192201
})
202+
if muteNotifications {
203+
notificationsEnabled = false
204+
}
205+
if !notificationsEnabled {
206+
return
207+
}
193208
observability.GoSafe(ctx, func() {
194209
commandTemplate := xsync.DoR1(ctx, &ui.Panel.configLocker, func() string {
195210
return ui.Panel.Config.Chat.CommandOnReceiveMessage
@@ -202,15 +217,6 @@ func (ui *chatUI) onReceiveMessage(
202217

203218
ui.Panel.execCommand(ctx, commandTemplate, msg)
204219
})
205-
notificationsEnabled := xsync.DoR1(ctx, &ui.Panel.configLocker, func() bool {
206-
return ui.Panel.Config.Chat.NotificationsEnabled()
207-
})
208-
if muteNotifications {
209-
notificationsEnabled = false
210-
}
211-
if !notificationsEnabled {
212-
return
213-
}
214220
observability.GoSafe(ctx, func() {
215221
logger.Debugf(ctx, "SendNotification")
216222
defer logger.Debugf(ctx, "/SendNotification")
@@ -242,30 +248,81 @@ func (ui *chatUI) onReceiveMessage(
242248
}
243249

244250
func (ui *chatUI) listLength() int {
245-
ui.MessagesHistoryLocker.Lock()
246-
defer ui.MessagesHistoryLocker.Unlock()
251+
ctx := context.TODO()
252+
ui.MessagesHistoryLocker.ManualLock(ctx)
253+
defer ui.MessagesHistoryLocker.ManualUnlock(ctx)
247254
return len(ui.MessagesHistory)
248255
}
249256

257+
type chatItem struct {
258+
BanUserButton *widget.Button
259+
RemoveMessageButton *widget.Button
260+
TimestampSegment *widget.TextSegment
261+
UsernameSegment *widget.TextSegment
262+
MessageSegment *widget.TextSegment
263+
Text *widget.RichText
264+
Height uint
265+
266+
*fyne.Container
267+
}
268+
250269
func (ui *chatUI) listCreateItem() fyne.CanvasObject {
251-
banUserButton := widget.NewButtonWithIcon("", theme.ErrorIcon(), func() {})
252-
removeMsgButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {})
253-
label := widget.NewLabel("<...loading...>")
254-
label.Wrapping = fyne.TextWrapWord
270+
item := &chatItem{
271+
Container: &fyne.Container{},
272+
TimestampSegment: &widget.TextSegment{
273+
Style: widget.RichTextStyle{Inline: true, TextStyle: fyne.TextStyle{Bold: true}},
274+
},
275+
UsernameSegment: &widget.TextSegment{
276+
Style: widget.RichTextStyle{
277+
Inline: true,
278+
SizeName: theme.SizeNameSubHeadingText,
279+
TextStyle: fyne.TextStyle{Bold: true},
280+
},
281+
},
282+
MessageSegment: &widget.TextSegment{
283+
Style: widget.RichTextStyle{
284+
Inline: true,
285+
SizeName: theme.SizeNameSubHeadingText,
286+
TextStyle: fyne.TextStyle{Bold: true},
287+
},
288+
},
289+
}
290+
255291
var leftPanel fyne.CanvasObject
256292
if ui.EnableButtons {
293+
item.BanUserButton = widget.NewButtonWithIcon("", theme.ErrorIcon(), func() {})
294+
item.RemoveMessageButton = widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {})
257295
leftPanel = container.NewHBox(
258-
banUserButton,
259-
removeMsgButton,
296+
item.BanUserButton,
297+
item.RemoveMessageButton,
260298
)
261299
}
262-
return container.NewBorder(
300+
item.Text = widget.NewRichText(
301+
item.TimestampSegment,
302+
&widget.TextSegment{
303+
Text: " ",
304+
Style: widget.RichTextStyle{Inline: true},
305+
},
306+
item.UsernameSegment,
307+
&widget.TextSegment{
308+
Text: " ",
309+
Style: widget.RichTextStyle{Inline: true},
310+
},
311+
item.MessageSegment,
312+
)
313+
item.Text.Wrapping = fyne.TextWrapWord
314+
item.Container = container.NewBorder(
263315
nil,
264316
nil,
265317
leftPanel,
266318
nil,
267-
label,
319+
item.Text,
268320
)
321+
ctx := context.TODO()
322+
ui.ItemLocker.Do(ctx, func() {
323+
ui.ItemsByCanvasObject[item.Container] = item
324+
})
325+
return item.Container
269326
}
270327

271328
func (ui *chatUI) getPlatformCapabilities(
@@ -299,8 +356,8 @@ func (ui *chatUI) listUpdateItem(
299356
obj fyne.CanvasObject,
300357
) {
301358
ctx := context.TODO()
302-
ui.MessagesHistoryLocker.Lock()
303-
defer ui.MessagesHistoryLocker.Unlock()
359+
ui.MessagesHistoryLocker.ManualLock(ctx)
360+
defer ui.MessagesHistoryLocker.ManualUnlock(ctx)
304361
var entryID int
305362
if ui.ReverseOrder {
306363
entryID = len(ui.MessagesHistory) - 1 - rowID
@@ -319,12 +376,11 @@ func (ui *chatUI) listUpdateItem(
319376
platCaps = map[streamcontrol.Capability]struct{}{}
320377
}
321378

322-
containerPtr := obj.(*fyne.Container)
323-
objs := containerPtr.Objects
324-
label := objs[0].(*widget.Label)
379+
item := xsync.DoR1(ctx, &ui.ItemLocker, func() *chatItem {
380+
return ui.ItemsByCanvasObject[obj]
381+
})
325382
if ui.EnableButtons {
326-
subContainer := objs[1].(*fyne.Container)
327-
banUserButton := subContainer.Objects[0].(*widget.Button)
383+
banUserButton := item.BanUserButton
328384
banUserButton.OnTapped = func() {
329385
w := dialog.NewConfirm(
330386
"Banning an user",
@@ -344,7 +400,7 @@ func (ui *chatUI) listUpdateItem(
344400
} else {
345401
banUserButton.Enable()
346402
}
347-
removeMsgButton := subContainer.Objects[1].(*widget.Button)
403+
removeMsgButton := item.RemoveMessageButton
348404
removeMsgButton.OnTapped = func() {
349405
w := dialog.NewConfirm(
350406
"Removing a message",
@@ -366,20 +422,22 @@ func (ui *chatUI) listUpdateItem(
366422
removeMsgButton.Enable()
367423
}
368424
}
369-
label.SetText(fmt.Sprintf(
370-
"%s: %s: %s: %s",
371-
msg.CreatedAt.Format("15:04:05"),
372-
msg.Platform,
373-
msg.Username,
374-
msg.Message,
375-
))
376-
377-
requiredHeight := containerPtr.MinSize().Height
425+
item.TimestampSegment.Text = msg.CreatedAt.Format("15:04:05")
426+
item.TimestampSegment.Style.ColorName = colorForPlatform(msg.Platform)
427+
item.UsernameSegment.Text = msg.Username
428+
item.UsernameSegment.Style.ColorName = colorForUsername(msg.Username)
429+
item.MessageSegment.Text = msg.Message
430+
item.Text.Refresh()
431+
logger.Tracef(ctx, "%d: updated message is: '%s'", rowID, msg.Message)
432+
433+
requiredHeight := item.Container.MinSize().Height
378434
logger.Tracef(ctx, "%d: requiredHeight == %f", rowID, requiredHeight)
379435

380-
prevHeight := ui.ItemHeight[msg.MessageID]
381-
ui.TotalListHeight += uint(requiredHeight) - prevHeight
382-
ui.ItemHeight[msg.MessageID] = uint(requiredHeight)
436+
ui.ItemLocker.Do(ctx, func() {
437+
ui.ItemsByMessageID[msg.MessageID] = item
438+
ui.TotalListHeight += uint(requiredHeight) - item.Height
439+
ui.ItemsByMessageID[msg.MessageID].Height = uint(requiredHeight)
440+
})
383441

384442
// TODO: think of how to get rid of this racy hack:
385443
observability.Go(ctx, func() { ui.List.SetItemHeight(rowID, requiredHeight) })
@@ -407,8 +465,8 @@ func (ui *chatUI) onRemoveClicked(
407465
ctx := context.TODO()
408466
logger.Debugf(ctx, "onRemoveClicked(%s)", itemID)
409467
defer func() { logger.Debugf(ctx, "/onRemoveClicked(%s)", itemID) }()
410-
ui.MessagesHistoryLocker.Lock()
411-
defer ui.MessagesHistoryLocker.Unlock()
468+
ui.MessagesHistoryLocker.ManualLock(ctx)
469+
defer ui.MessagesHistoryLocker.ManualUnlock(ctx)
412470
if itemID < 0 || itemID >= len(ui.MessagesHistory) {
413471
return
414472
}
@@ -417,9 +475,12 @@ func (ui *chatUI) onRemoveClicked(
417475
defer onRemove(ctx, msg)
418476
}
419477
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)
478+
ui.ItemLocker.Do(ctx, func() {
479+
item := ui.ItemsByMessageID[msg.MessageID]
480+
ui.TotalListHeight -= item.Height
481+
delete(ui.ItemsByMessageID, msg.MessageID)
482+
delete(ui.ItemsByCanvasObject, item.Container)
483+
})
423484
err := ui.Panel.chatMessageRemove(ui.ctx, msg.Platform, msg.MessageID)
424485
if err != nil {
425486
ui.Panel.DisplayError(err)

pkg/streampanel/color.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package streampanel
2+
3+
import (
4+
"crypto/sha1"
5+
"encoding/binary"
6+
7+
"fyne.io/fyne/v2"
8+
"fyne.io/fyne/v2/theme"
9+
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
10+
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/kick"
11+
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
12+
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
13+
)
14+
15+
func colorForPlatform(platID streamcontrol.PlatformName) fyne.ThemeColorName {
16+
switch platID {
17+
case twitch.ID:
18+
return theme.ColorNameHyperlink
19+
case kick.ID:
20+
return theme.ColorNameSuccess
21+
case youtube.ID:
22+
return theme.ColorNameError
23+
default:
24+
return ""
25+
}
26+
}
27+
28+
var brightColors = []fyne.ThemeColorName{
29+
theme.ColorNameError,
30+
theme.ColorNameForeground,
31+
theme.ColorNameHyperlink,
32+
theme.ColorNamePrimary,
33+
theme.ColorNameSelection,
34+
theme.ColorNameSuccess,
35+
theme.ColorNameWarning,
36+
}
37+
38+
func colorForUsername(username string) fyne.ThemeColorName {
39+
h := sha1.Sum([]byte(username))
40+
h64 := binary.BigEndian.Uint64(h[:])
41+
42+
return brightColors[h64%uint64(len(brightColors))]
43+
}

0 commit comments

Comments
 (0)