Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/sidebar/channels/channels_model.go
366 views
1
package channels
2
3
import (
4
"log/slog"
5
"sort"
6
7
"github.com/diamondburned/arikawa/v3/discord"
8
"github.com/diamondburned/arikawa/v3/gateway"
9
"github.com/diamondburned/gotk4/pkg/core/glib"
10
"github.com/diamondburned/gotk4/pkg/gio/v2"
11
"github.com/diamondburned/gotk4/pkg/gtk/v4"
12
"libdb.so/dissent/internal/gtkcord"
13
"libdb.so/dissent/internal/signaling"
14
)
15
16
type modelManager struct {
17
*gtk.TreeListModel
18
state *gtkcord.State
19
guildID discord.GuildID
20
}
21
22
func newModelManager(state *gtkcord.State, guildID discord.GuildID) *modelManager {
23
m := &modelManager{
24
state: state,
25
guildID: guildID,
26
}
27
m.TreeListModel = gtk.NewTreeListModel(
28
m.Model(0), true, true,
29
func(item *glib.Object) *gio.ListModel {
30
chID := channelIDFromItem(item)
31
32
model := m.Model(chID)
33
if model == nil {
34
return nil
35
}
36
37
return &model.ListModel
38
})
39
return m
40
}
41
42
// Model returns the list model containing all channels within the given channel
43
// ID. If chID is 0, then the guild's root channels will be returned. This
44
// function may return nil, indicating that the channel will never have any
45
// children.
46
func (m *modelManager) Model(chID discord.ChannelID) *gtk.StringList {
47
model := gtk.NewStringList(nil)
48
49
list := newChannelList(m.state, model)
50
51
var unbind signaling.DisconnectStack
52
list.ConnectDestroy(func() { unbind.Disconnect() })
53
54
unbind.Push(
55
m.state.AddHandler(func(ev *gateway.ChannelCreateEvent) {
56
if ev.GuildID != m.guildID {
57
return
58
}
59
if ev.Channel.ParentID == chID {
60
list.Append(ev.Channel)
61
}
62
}),
63
m.state.AddHandler(func(ev *gateway.ChannelUpdateEvent) {
64
if ev.GuildID != m.guildID {
65
return
66
}
67
// Handle channel position moves.
68
if ev.Channel.ParentID == chID {
69
list.Append(ev.Channel)
70
} else {
71
list.Remove(ev.Channel.ID)
72
}
73
}),
74
m.state.AddHandler(func(ev *gateway.ThreadCreateEvent) {
75
if ev.GuildID != m.guildID {
76
return
77
}
78
if ev.Channel.ParentID == chID {
79
list.Append(ev.Channel)
80
}
81
}),
82
m.state.AddHandler(func(ev *gateway.ThreadDeleteEvent) {
83
if ev.GuildID != m.guildID {
84
return
85
}
86
if ev.ParentID == chID {
87
list.Remove(ev.ID)
88
}
89
}),
90
m.state.AddHandler(func(ev *gateway.ThreadListSyncEvent) {
91
if ev.GuildID != m.guildID {
92
return
93
}
94
95
if ev.ChannelIDs == nil {
96
// The entire guild was synced, so invalidate everything.
97
m.invalidateAll(chID, list)
98
return
99
}
100
101
for _, parentID := range ev.ChannelIDs {
102
if parentID == chID {
103
// This sync event is also for us.
104
m.invalidateAll(chID, list)
105
break
106
}
107
}
108
}),
109
)
110
111
m.invalidateAll(chID, list)
112
return model
113
}
114
115
func (m *modelManager) invalidateAll(parentID discord.ChannelID, list *channelList) {
116
channels := fetchSortedChannels(m.state, m.guildID, parentID)
117
list.ClearAndAppend(channels)
118
}
119
120
// channelList wraps a StringList to maintain a set of channel IDs.
121
// Because this is a set, each channel ID can only appear once.
122
type channelList struct {
123
state *gtkcord.State
124
list *gtk.StringList
125
ids []discord.ChannelID
126
}
127
128
func newChannelList(state *gtkcord.State, list *gtk.StringList) *channelList {
129
return &channelList{
130
state: state,
131
list: list,
132
ids: make([]discord.ChannelID, 0, 4),
133
}
134
}
135
136
// CalculatePosition converts the position of a channel given by Discord to the
137
// position relative to the list. If the channel is not found, then this
138
// function returns the end of the list.
139
func (l *channelList) CalculatePosition(target discord.Channel) uint {
140
for i, id := range l.ids {
141
ch, _ := l.state.Offline().Channel(id)
142
if ch == nil {
143
continue
144
}
145
146
if ch.Position > target.Position {
147
return uint(i)
148
}
149
}
150
151
return uint(len(l.ids))
152
}
153
154
// Append appends a channel to the list. If the channel already exists, then
155
// this function does nothing.
156
func (l *channelList) Append(ch discord.Channel) {
157
pos := l.CalculatePosition(ch)
158
l.insertAt(ch, pos)
159
}
160
161
func (l *channelList) insertAt(ch discord.Channel, pos uint) {
162
i := l.Index(ch.ID)
163
if i != -1 {
164
return
165
}
166
167
list := l.list
168
if list == nil {
169
return
170
}
171
172
list.Splice(pos, 0, []string{ch.ID.String()})
173
l.ids = append(l.ids[:pos], append([]discord.ChannelID{ch.ID}, l.ids[pos:]...)...)
174
}
175
176
// Remove removes the channel ID from the list. If the channel ID is not in the
177
// list, then this function does nothing.
178
func (l *channelList) Remove(chID discord.ChannelID) {
179
i := l.Index(chID)
180
if i != -1 {
181
l.ids = append(l.ids[:i], l.ids[i+1:]...)
182
183
list := l.list
184
if list != nil {
185
list.Remove(uint(i))
186
}
187
}
188
}
189
190
// Contains returns whether the channel ID is in the list.
191
func (l *channelList) Contains(chID discord.ChannelID) bool {
192
return l.Index(chID) != -1
193
}
194
195
// Index returns the index of the channel ID in the list. If the channel ID is
196
// not in the list, then this function returns -1.
197
func (l *channelList) Index(chID discord.ChannelID) int {
198
for i, id := range l.ids {
199
if id == chID {
200
return i
201
}
202
}
203
return -1
204
}
205
206
// Clear clears the list.
207
func (l *channelList) Clear() {
208
l.ids = l.ids[:0]
209
210
list := l.list
211
if list != nil {
212
list.Splice(0, list.NItems(), nil)
213
}
214
}
215
216
// ClearAndAppend clears the list and appends the given channels.
217
func (l *channelList) ClearAndAppend(chs []discord.Channel) {
218
list := l.list
219
if list == nil {
220
return
221
}
222
223
ids := make([]string, len(chs))
224
l.ids = make([]discord.ChannelID, len(chs))
225
226
for i, ch := range chs {
227
ids[i] = ch.ID.String()
228
l.ids = append(l.ids, ch.ID)
229
}
230
231
list.Splice(0, list.NItems(), ids)
232
}
233
234
func (l *channelList) ConnectDestroy(f func()) {
235
list := l.list
236
if list == nil {
237
return
238
}
239
// I think this is the only way to know if a ListModel is no longer
240
// being used? At least from reading the source code, which just calls
241
// g_clear_pointer.
242
glib.WeakRefObject(list, f)
243
}
244
245
func fetchSortedChannels(state *gtkcord.State, guildID discord.GuildID, parentID discord.ChannelID) []discord.Channel {
246
channels, err := state.Offline().Channels(guildID, gtkcord.AllowedChannelTypes)
247
if err != nil {
248
slog.Error(
249
"failed to get guild channels to sort",
250
"guild_id", guildID,
251
"err", err)
252
return nil
253
}
254
255
// Filter out all channels that are not in the same parent channel.
256
filtered := channels[:0]
257
for i, ch := range channels {
258
if ch.ParentID == parentID || (parentID == 0 && !ch.ParentID.IsValid()) {
259
filtered = append(filtered, channels[i])
260
}
261
}
262
263
// Sort so that the channels are in increasing order.
264
sort.Slice(filtered, func(i, j int) bool {
265
a := filtered[i]
266
b := filtered[j]
267
if a.Position == b.Position {
268
return a.ID < b.ID
269
}
270
return a.Position < b.Position
271
})
272
273
return filtered
274
}
275
276