Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/messages/contentreactions.go
366 views
1
package messages
2
3
import (
4
"context"
5
"fmt"
6
"html"
7
"log/slog"
8
"strconv"
9
"strings"
10
11
"github.com/diamondburned/arikawa/v3/discord"
12
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
13
"github.com/diamondburned/gotk4/pkg/core/gioutil"
14
"github.com/diamondburned/gotk4/pkg/glib/v2"
15
"github.com/diamondburned/gotk4/pkg/gtk/v4"
16
"github.com/diamondburned/gotkit/app"
17
"github.com/diamondburned/gotkit/app/locale"
18
"github.com/diamondburned/gotkit/components/onlineimage"
19
"github.com/diamondburned/gotkit/gtkutil"
20
"github.com/diamondburned/gotkit/gtkutil/cssutil"
21
"github.com/diamondburned/gotkit/gtkutil/imgutil"
22
"libdb.so/dissent/internal/gtkcord"
23
)
24
25
type messageReaction struct {
26
discord.Reaction
27
GuildID discord.GuildID
28
ChannelID discord.ChannelID
29
MessageID discord.MessageID
30
}
31
32
func (r messageReaction) Equal(other messageReaction) bool {
33
return true &&
34
r.MessageID == other.MessageID &&
35
r.ChannelID == other.ChannelID &&
36
r.Me == other.Me &&
37
r.Count == other.Count &&
38
r.Emoji.APIString() == other.Emoji.APIString()
39
}
40
41
type contentReactions struct {
42
*gtk.FlowBox
43
44
// *gtk.ScrolledWindow
45
// grid *gtk.GridView
46
47
ctx context.Context
48
parent *Content
49
reactions *gioutil.ListModel[messageReaction]
50
}
51
52
var reactionsCSS = cssutil.Applier("message-reactions", `
53
.message-reactions {
54
padding: 0;
55
margin-top: 4px;
56
background: none;
57
}
58
.message-reactions > flowboxchild {
59
margin: 4px 0;
60
margin-right: 6px;
61
padding: 0;
62
}
63
`)
64
65
func newContentReactions(ctx context.Context, parent *Content) *contentReactions {
66
rs := contentReactions{
67
ctx: ctx,
68
parent: parent,
69
reactions: gioutil.NewListModel[messageReaction](),
70
}
71
72
// TODO: complain to the GTK devs about how broken GridView is.
73
// Why is it not reflowing widgets? and other mysteries to solve in the GTK
74
// framework.
75
76
// rs.grid = gtk.NewGridView(
77
// gtk.NewNoSelection(rs.reactions.ListModel),
78
// newContentReactionsFactory(ctx))
79
// rs.grid.SetOrientation(gtk.OrientationHorizontal)
80
// reactionsCSS(rs.grid)
81
//
82
// rs.ScrolledWindow = gtk.NewScrolledWindow()
83
// rs.ScrolledWindow.SetPolicy(gtk.PolicyNever, gtk.PolicyNever)
84
// rs.ScrolledWindow.SetPropagateNaturalWidth(true)
85
// rs.ScrolledWindow.SetPropagateNaturalHeight(false)
86
// rs.ScrolledWindow.SetChild(rs.grid)
87
88
rs.FlowBox = gtk.NewFlowBox()
89
rs.FlowBox.SetOrientation(gtk.OrientationHorizontal)
90
rs.FlowBox.SetHomogeneous(true)
91
rs.FlowBox.SetMaxChildrenPerLine(30)
92
rs.FlowBox.SetSelectionMode(gtk.SelectionNone)
93
reactionsCSS(rs)
94
95
rs.FlowBox.BindModel(rs.reactions.ListModel, func(o *glib.Object) gtk.Widgetter {
96
reaction := gioutil.ObjectValue[messageReaction](o)
97
w := newContentReaction()
98
w.SetReaction(ctx, rs.FlowBox, reaction)
99
return w
100
})
101
102
gtkutil.BindActionCallbackMap(rs, map[string]gtkutil.ActionCallback{
103
"reactions.toggle": {
104
ArgType: glib.NewVariantType("s"),
105
Func: func(args *glib.Variant) {
106
emoji := discord.APIEmoji(args.String())
107
selected := rs.isReacted(emoji)
108
109
client := gtkcord.FromContext(rs.ctx).Online()
110
gtkutil.Async(rs.ctx, func() func() {
111
var err error
112
if selected {
113
err = client.Unreact(rs.parent.ChannelID(), rs.parent.MessageID(), emoji)
114
} else {
115
err = client.React(rs.parent.ChannelID(), rs.parent.MessageID(), emoji)
116
}
117
118
if err != nil {
119
if selected {
120
err = fmt.Errorf("failed to react: %w", err)
121
} else {
122
err = fmt.Errorf("failed to unreact: %w", err)
123
}
124
app.Error(rs.ctx, err)
125
}
126
127
return nil
128
})
129
},
130
},
131
})
132
133
return &rs
134
}
135
136
func (rs *contentReactions) findReactionIx(emoji discord.APIEmoji) int {
137
var i int
138
foundIx := -1
139
140
iter := rs.reactions.All()
141
iter(func(reaction messageReaction) bool {
142
if reaction.Emoji.APIString() == emoji {
143
foundIx = i
144
return false
145
}
146
i++
147
return true
148
})
149
150
return foundIx
151
}
152
153
func (rs *contentReactions) isReacted(emoji discord.APIEmoji) bool {
154
ix := rs.findReactionIx(emoji)
155
if ix == -1 {
156
return false
157
}
158
return rs.reactions.At(ix).Me
159
}
160
161
// SetReactions sets the reactions of the message.
162
//
163
// TODO: implement Add and Remove event handlers directly in this container to
164
// avoid having to clear the whole list.
165
func (rs *contentReactions) SetReactions(reactions []discord.Reaction) {
166
messageReactions := make([]messageReaction, len(reactions))
167
for i, r := range reactions {
168
messageReactions[i] = messageReaction{
169
Reaction: r,
170
GuildID: rs.parent.view.GuildID(),
171
ChannelID: rs.parent.view.ChannelID(),
172
MessageID: rs.parent.MessageID(),
173
}
174
}
175
rs.reactions.Splice(0, rs.reactions.Len(), messageReactions...)
176
}
177
178
/*
179
func newContentReactionsFactory(ctx context.Context) *gtk.ListItemFactory {
180
reactionWidgets := make(map[uintptr]*contentReaction)
181
182
factory := gtk.NewSignalListItemFactory()
183
factory.ConnectSetup(func(item *gtk.ListItem) {
184
w := newContentReaction()
185
item.SetChild(w)
186
reactionWidgets[item.Native()] = w
187
})
188
factory.ConnectTeardown(func(item *gtk.ListItem) {
189
item.SetChild(nil)
190
delete(reactionWidgets, item.Native())
191
})
192
193
factory.ConnectBind(func(item *gtk.ListItem) {
194
reaction := gioutil.ObjectValue[messageReaction](item.Item())
195
196
w := reactionWidgets[item.Native()]
197
w.SetReaction(ctx, reaction)
198
})
199
factory.ConnectUnbind(func(item *gtk.ListItem) {
200
w := reactionWidgets[item.Native()]
201
w.Clear()
202
})
203
204
return &factory.ListItemFactory
205
}
206
*/
207
208
type reactionsLoadState uint8
209
210
const (
211
reactionsNotLoaded reactionsLoadState = iota
212
reactionsLoading
213
reactionsLoaded
214
)
215
216
type contentReaction struct {
217
*gtk.ToggleButton
218
iconBin *adw.Bin
219
countLabel *gtk.Label
220
221
reaction messageReaction
222
client *gtkcord.State
223
224
tooltip string
225
tooltipState reactionsLoadState
226
}
227
228
var reactionCSS = cssutil.Applier("message-reaction", `
229
.message-reaction {
230
/* min-width: 4em; */
231
min-width: 0;
232
min-height: 0;
233
padding: 0;
234
}
235
.message-reaction > box {
236
margin: 6px;
237
}
238
.message-reaction-emoji-icon {
239
min-width: 22px;
240
min-height: 22px;
241
}
242
.message-reaction-emoji-unicode {
243
font-size: 18px;
244
}
245
`)
246
247
func newContentReaction() *contentReaction {
248
r := contentReaction{}
249
250
r.ToggleButton = gtk.NewToggleButton()
251
r.ToggleButton.AddCSSClass("message-reaction")
252
r.ToggleButton.ConnectClicked(func() {
253
r.SetSensitive(false)
254
255
ok := r.ActivateAction("reactions.toggle", glib.NewVariantString(string(r.reaction.Emoji.APIString())))
256
if !ok {
257
slog.Error(
258
"failed to activate reactions.toggle",
259
"emoji", r.reaction.Emoji.APIString())
260
}
261
})
262
263
r.ToggleButton.SetHasTooltip(true)
264
r.ToggleButton.ConnectQueryTooltip(func(_, _ int, _ bool, tooltip *gtk.Tooltip) bool {
265
tooltip.SetText(locale.Get("Loading..."))
266
r.invalidateUsers(tooltip.SetMarkup)
267
return true
268
})
269
270
r.iconBin = adw.NewBin()
271
r.iconBin.AddCSSClass("message-reaction-icon")
272
273
r.countLabel = gtk.NewLabel("")
274
r.countLabel.AddCSSClass("message-reaction-count")
275
r.countLabel.SetHExpand(true)
276
r.countLabel.SetXAlign(1)
277
278
box := gtk.NewBox(gtk.OrientationHorizontal, 0)
279
box.Append(r.iconBin)
280
box.Append(r.countLabel)
281
282
r.ToggleButton.SetChild(box)
283
reactionCSS(r)
284
285
return &r
286
}
287
288
// SetReaction sets the reaction of the widget.
289
func (r *contentReaction) SetReaction(ctx context.Context, flowBox *gtk.FlowBox, reaction messageReaction) {
290
r.reaction = reaction
291
r.client = gtkcord.FromContext(ctx).Online()
292
293
if reaction.Emoji.IsCustom() {
294
emoji := onlineimage.NewPicture(ctx, imgutil.HTTPProvider)
295
emoji.AddCSSClass("message-reaction-emoji")
296
emoji.AddCSSClass("message-reaction-emoji-custom")
297
emoji.SetSizeRequest(gtkcord.InlineEmojiSize, gtkcord.InlineEmojiSize)
298
emoji.SetKeepAspectRatio(true)
299
emoji.SetURL(reaction.Emoji.EmojiURL())
300
301
// TODO: get this working:
302
// Currently, it just jitters in size. The button itself can still be
303
// sized small, FlowBox is just forcing it to be big. This does mean
304
// that it's not the GIF that is causing this.
305
306
// anim := emoji.EnableAnimation()
307
// anim.ConnectMotion(r)
308
309
r.iconBin.SetChild(emoji)
310
} else {
311
label := gtk.NewLabel(reaction.Emoji.Name)
312
label.AddCSSClass("message-reaction-emoji")
313
label.AddCSSClass("message-reaction-emoji-unicode")
314
315
r.iconBin.SetChild(label)
316
}
317
318
r.countLabel.SetLabel(strconv.Itoa(reaction.Count))
319
320
r.ToggleButton.SetActive(reaction.Me)
321
if reaction.Me {
322
r.AddCSSClass("message-reaction-me")
323
} else {
324
r.RemoveCSSClass("message-reaction-me")
325
}
326
}
327
328
func (r *contentReaction) Clear() {
329
r.reaction = messageReaction{}
330
r.client = nil
331
r.tooltipState = reactionsNotLoaded
332
r.iconBin.SetChild(nil)
333
r.ToggleButton.SetActive(false)
334
r.ToggleButton.RemoveCSSClass("message-reaction-me")
335
}
336
337
func (r *contentReaction) invalidateUsers(callback func(string)) {
338
if r.tooltipState != reactionsNotLoaded {
339
callback(r.tooltip)
340
return
341
}
342
343
r.tooltipState = reactionsLoading
344
r.tooltip = ""
345
346
reaction := r.reaction
347
client := r.client
348
349
var tooltip string
350
if reaction.Emoji.IsCustom() {
351
tooltip = ":" + html.EscapeString(reaction.Emoji.Name) + ":\n"
352
}
353
354
done := func(tooltip string, err error) {
355
glib.IdleAdd(func() {
356
if !r.reaction.Equal(reaction) {
357
// The reaction has changed,
358
// so we don't care about the result.
359
return
360
}
361
362
if err != nil {
363
r.tooltipState = reactionsNotLoaded
364
r.tooltip = tooltip + "<b>" + locale.Get("Error: ") + "</b>" + err.Error()
365
366
slog.Error(
367
"cannot load reaction tooltip",
368
"channel", reaction.ChannelID,
369
"message", reaction.MessageID,
370
"emoji", reaction.Emoji.APIString(),
371
"err", err)
372
} else {
373
r.tooltipState = reactionsLoaded
374
r.tooltip = tooltip
375
}
376
377
callback(r.tooltip)
378
})
379
}
380
381
go func() {
382
u, err := client.Reactions(
383
reaction.ChannelID,
384
reaction.MessageID,
385
reaction.Emoji.APIString(), 11)
386
if err != nil {
387
done(tooltip, err)
388
return
389
}
390
391
var hasMore bool
392
if len(u) > 10 {
393
hasMore = true
394
u = u[:10]
395
}
396
397
for _, user := range u {
398
tooltip += fmt.Sprintf(
399
`<span size="small">%s</span>`+"\n",
400
client.MemberMarkup(reaction.GuildID, &discord.GuildUser{User: user}),
401
)
402
}
403
404
if hasMore {
405
tooltip += "..."
406
} else {
407
tooltip = strings.TrimRight(tooltip, "\n")
408
}
409
410
done(tooltip, nil)
411
}()
412
}
413
414