Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/sidebar/guilds/guilds.go
366 views
1
package guilds
2
3
import (
4
"context"
5
"log/slog"
6
"sort"
7
8
"github.com/diamondburned/arikawa/v3/discord"
9
"github.com/diamondburned/arikawa/v3/gateway"
10
"github.com/diamondburned/gotk4/pkg/gtk/v4"
11
"github.com/diamondburned/gotkit/app"
12
"github.com/diamondburned/gotkit/gtkutil"
13
"github.com/diamondburned/gotkit/gtkutil/cssutil"
14
"github.com/diamondburned/ningen/v3/states/read"
15
"github.com/pkg/errors"
16
"libdb.so/dissent/internal/gtkcord"
17
)
18
19
// ViewChild is a child inside the guilds view. It is either a *Guild or a
20
// *Folder containing more *Guilds.
21
type ViewChild interface {
22
gtk.Widgetter
23
viewChild()
24
}
25
26
// View contains a list of guilds and folders.
27
type View struct {
28
*gtk.Box
29
Children []ViewChild
30
31
current currentGuild
32
33
ctx context.Context
34
}
35
36
var viewCSS = cssutil.Applier("guild-view", `
37
.guild-view {
38
margin: 4px 0;
39
}
40
.guild-view button:active:not(:hover) {
41
background: initial;
42
}
43
`)
44
45
// NewView creates a new View.
46
func NewView(ctx context.Context) *View {
47
v := View{
48
ctx: ctx,
49
}
50
51
v.Box = gtk.NewBox(gtk.OrientationVertical, 0)
52
viewCSS(v)
53
54
cancellable := gtkutil.WithVisibility(ctx, v)
55
56
state := gtkcord.FromContext(ctx)
57
state.BindHandler(cancellable, func(ev gateway.Event) {
58
switch ev := ev.(type) {
59
case *gateway.ReadyEvent, *gateway.ResumedEvent:
60
// Recreate the whole list in case we have some new info.
61
v.Invalidate()
62
63
case *read.UpdateEvent:
64
if guild := v.Guild(ev.GuildID); guild != nil {
65
guild.InvalidateUnread()
66
}
67
case *gateway.ChannelCreateEvent:
68
if ev.GuildID.IsValid() {
69
if guild := v.Guild(ev.GuildID); guild != nil {
70
guild.InvalidateUnread()
71
}
72
}
73
case *gateway.GuildCreateEvent:
74
if guild := v.Guild(ev.ID); guild != nil {
75
guild.Update(&ev.Guild)
76
} else {
77
v.AddGuild(&ev.Guild)
78
}
79
case *gateway.GuildUpdateEvent:
80
if guild := v.Guild(ev.ID); guild != nil {
81
guild.Invalidate()
82
}
83
case *gateway.GuildDeleteEvent:
84
if ev.Unavailable {
85
if guild := v.Guild(ev.ID); guild != nil {
86
guild.SetUnavailable()
87
88
parent := gtk.BaseWidget(guild.Parent())
89
parent.ActivateAction("win.reset-view", nil)
90
return
91
}
92
}
93
94
guild := v.RemoveGuild(ev.ID)
95
if guild != nil && guild.IsSelected() {
96
parent := gtk.BaseWidget(guild.Parent())
97
parent.ActivateAction("win.reset-view", nil)
98
}
99
}
100
})
101
102
return &v
103
}
104
105
// InvalidateUnreads invalidates the unread states of all guilds.
106
func (v *View) InvalidateUnreads() {
107
for _, child := range v.Children {
108
if child, ok := child.(*Guild); ok {
109
child.InvalidateUnread()
110
}
111
}
112
}
113
114
// Invalidate invalidates the view and recreates everything. Use with care.
115
func (v *View) Invalidate() {
116
// TODO: reselect.
117
118
state := gtkcord.FromContext(v.ctx)
119
ready := state.Ready()
120
121
if ready.UserSettings != nil {
122
switch {
123
case ready.UserSettings.GuildFolders != nil:
124
v.SetFolders(ready.UserSettings.GuildFolders)
125
case ready.UserSettings.GuildPositions != nil:
126
v.SetGuildsFromIDs(ready.UserSettings.GuildPositions)
127
}
128
}
129
130
guilds, err := state.Cabinet.Guilds()
131
if err != nil {
132
app.Error(v.ctx, errors.Wrap(err, "cannot get guilds"))
133
return
134
}
135
136
// Sort so that the guilds that we've joined last are at the bottom.
137
// This means we can prepend guilds as we go, and the latest one will be
138
// prepended to the top.
139
sort.Slice(guilds, func(i, j int) bool {
140
ti, ok := state.GuildState.JoinedAt(guilds[i].ID)
141
if !ok {
142
return false // put last
143
}
144
tj, ok := state.GuildState.JoinedAt(guilds[j].ID)
145
if !ok {
146
return true
147
}
148
return ti.Before(tj)
149
})
150
151
// Construct a map of shownGuilds guilds, so we know to not create a
152
// guild if it's already shown.
153
shownGuilds := make(map[discord.GuildID]struct{}, 200)
154
v.eachGuild(func(g *Guild) bool {
155
shownGuilds[g.ID()] = struct{}{}
156
return false
157
})
158
159
for i, guild := range guilds {
160
_, shown := shownGuilds[guild.ID]
161
if shown {
162
continue
163
}
164
165
g := NewGuild(v.ctx, guild.ID)
166
g.Update(&guilds[i])
167
168
// Prepend the guild.
169
v.prepend(g)
170
}
171
}
172
173
// SetFolders sets the guild folders to use.
174
func (v *View) SetFolders(folders []gateway.GuildFolder) {
175
restore := v.saveSelection()
176
defer restore()
177
178
v.clear()
179
180
for i, folder := range folders {
181
if folder.ID == 0 {
182
// Contains a single guild, so we just unbox it.
183
g := NewGuild(v.ctx, folder.GuildIDs[0])
184
g.Invalidate()
185
186
v.append(g)
187
continue
188
}
189
190
f := NewFolder(v.ctx)
191
f.Set(&folders[i])
192
193
v.append(f)
194
}
195
}
196
197
// AddGuild prepends a single guild into the view.
198
func (v *View) AddGuild(guild *discord.Guild) {
199
g := NewGuild(v.ctx, guild.ID)
200
g.Update(guild)
201
202
v.Box.Prepend(g)
203
v.Children = append([]ViewChild{g}, v.Children...)
204
}
205
206
// RemoveGuild removes the given guild.
207
func (v *View) RemoveGuild(id discord.GuildID) *Guild {
208
guild := v.Guild(id)
209
if guild == nil {
210
return nil
211
}
212
213
if folder := guild.ParentFolder(); folder != nil {
214
folder.Remove(guild.ID())
215
if len(folder.Guilds) == 0 {
216
v.remove(folder)
217
}
218
} else {
219
v.remove(guild)
220
}
221
222
return guild
223
}
224
225
// SetGuildsFromIDs calls SetGuilds with guilds fetched from the state by the
226
// given ID list.
227
func (v *View) SetGuildsFromIDs(guildIDs []discord.GuildID) {
228
restore := v.saveSelection()
229
defer restore()
230
231
v.clear()
232
233
for _, id := range guildIDs {
234
g := NewGuild(v.ctx, id)
235
g.Invalidate()
236
237
v.append(g)
238
}
239
}
240
241
// SetGuilds sets the guilds shown.
242
func (v *View) SetGuilds(guilds []discord.Guild) {
243
restore := v.saveSelection()
244
defer restore()
245
246
v.clear()
247
248
for i, guild := range guilds {
249
g := NewGuild(v.ctx, guild.ID)
250
g.Update(&guilds[i])
251
252
v.append(g)
253
}
254
}
255
256
func (v *View) append(this ViewChild) {
257
v.Children = append(v.Children, this)
258
v.Box.Append(this)
259
}
260
261
func (v *View) prepend(this ViewChild) {
262
v.Children = append(v.Children, nil)
263
copy(v.Children[1:], v.Children)
264
v.Children[0] = this
265
266
v.Box.Prepend(this)
267
}
268
269
func (v *View) remove(this ViewChild) {
270
for i, child := range v.Children {
271
if child == this {
272
v.Children = append(v.Children[:i], v.Children[i+1:]...)
273
v.Box.Remove(child)
274
break
275
}
276
}
277
}
278
279
func (v *View) clear() {
280
for _, child := range v.Children {
281
v.Box.Remove(child)
282
}
283
v.Children = nil
284
}
285
286
// SelectedGuildID returns the selected guild ID, if any.
287
func (v *View) SelectedGuildID() discord.GuildID {
288
if v.current.guild == nil {
289
return 0
290
}
291
return v.current.guild.id
292
}
293
294
// Guild finds a guild inside View by its ID.
295
func (v *View) Guild(id discord.GuildID) *Guild {
296
var guild *Guild
297
v.eachGuild(func(g *Guild) bool {
298
if g.ID() == id {
299
guild = g
300
return true
301
}
302
return false
303
})
304
return guild
305
}
306
307
func (v *View) eachGuild(f func(*Guild) (stop bool)) {
308
for _, child := range v.Children {
309
switch child := child.(type) {
310
case *Guild:
311
if f(child) {
312
return
313
}
314
case *Folder:
315
for _, guild := range child.Guilds {
316
if f(guild) {
317
return
318
}
319
}
320
}
321
}
322
}
323
324
// SetSelectedGuild sets the selected guild. It does not propagate the selection
325
// to the sidebar. If the ID is invalid, it unselects the current guild
326
// selection.
327
func (v *View) SetSelectedGuild(id discord.GuildID) {
328
if !id.IsValid() {
329
v.Unselect()
330
return
331
}
332
333
guild := v.Guild(id)
334
if guild == nil {
335
slog.Error(
336
"cannot select guild since it's not found in guild view",
337
"guild_id", id)
338
v.Unselect()
339
return
340
}
341
342
current := currentGuild{
343
guild: guild,
344
folder: guild.ParentFolder(),
345
}
346
347
if current != v.current {
348
v.Unselect()
349
v.current = current
350
v.current.SetSelected(true)
351
}
352
}
353
354
// Unselect unselects any guilds inside this guild view. Use this when the
355
// window is showing a channel that's not from any guild.
356
func (v *View) Unselect() {
357
v.current.Unselect()
358
v.current = currentGuild{}
359
}
360
361
// saveSelection saves the current guild selection to be restored later using
362
// the returned callback.
363
func (v *View) saveSelection() (restore func()) {
364
if v.current.guild == nil {
365
// Nothing to restore.
366
return func() {}
367
}
368
369
guildID := v.current.guild.id
370
return func() {
371
parent := gtk.BaseWidget(v.Parent())
372
parent.ActivateAction("win.open-guild", gtkcord.NewGuildIDVariant(guildID))
373
}
374
}
375
376
type currentGuild struct {
377
guild *Guild
378
folder *Folder
379
}
380
381
func (c currentGuild) Unselect() {
382
c.SetSelected(false)
383
}
384
385
func (c currentGuild) SetSelected(selected bool) {
386
if c.folder != nil {
387
c.folder.SetSelected(selected)
388
}
389
if c.guild != nil {
390
c.guild.SetSelected(selected)
391
}
392
}
393
394