Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/sidebar/direct/view.go
366 views
1
package direct
2
3
import (
4
"context"
5
"log/slog"
6
"strings"
7
8
"github.com/diamondburned/adaptive"
9
"github.com/diamondburned/arikawa/v3/discord"
10
"github.com/diamondburned/arikawa/v3/gateway"
11
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
12
"github.com/diamondburned/gotk4/pkg/gtk/v4"
13
"github.com/diamondburned/gotkit/app/locale"
14
"github.com/diamondburned/gotkit/gtkutil"
15
"github.com/diamondburned/gotkit/gtkutil/cssutil"
16
"github.com/diamondburned/ningen/v3/states/read"
17
"libdb.so/dissent/internal/gtkcord"
18
)
19
20
// ChannelView displays a list of direct messaging channels.
21
type ChannelView struct {
22
*adaptive.LoadablePage
23
box *adw.ToolbarView
24
25
scroll *gtk.ScrolledWindow
26
list *gtk.ListBox
27
28
searchBar *gtk.SearchBar
29
searchEntry *gtk.SearchEntry
30
searchString string
31
32
ctx context.Context
33
channels map[discord.ChannelID]*Channel
34
selectID discord.ChannelID // delegate to be selected later
35
}
36
37
var _ = cssutil.WriteCSS(`
38
.direct-searchbar > revealer > box {
39
border-bottom: 0;
40
background: none;
41
box-shadow: none;
42
}
43
.direct-searchbar > revealer > box > entry {
44
min-height: 28px;
45
}
46
`)
47
48
// NewChannelView creates a new view.
49
func NewChannelView(ctx context.Context) *ChannelView {
50
v := ChannelView{
51
ctx: ctx,
52
channels: make(map[discord.ChannelID]*Channel, 50),
53
}
54
55
v.list = gtk.NewListBox()
56
v.list.SetCSSClasses([]string{"direct-list", "navigation-sidebar"})
57
v.list.SetHExpand(true)
58
v.list.SetSortFunc(v.sort)
59
v.list.SetFilterFunc(v.filter)
60
v.list.SetSelectionMode(gtk.SelectionBrowse)
61
v.list.SetActivateOnSingleClick(true)
62
63
var currentCh discord.ChannelID
64
65
v.list.ConnectRowSelected(func(r *gtk.ListBoxRow) {
66
if r == nil {
67
// This should not happen.
68
return
69
}
70
71
// Invalidate our selection state.
72
v.selectID = 0
73
74
ch := v.rowChannel(r)
75
if ch == nil || ch.id == currentCh {
76
return
77
}
78
79
currentCh = ch.id
80
parent := gtk.BaseWidget(v.list.Parent())
81
parent.ActivateAction("win.open-channel", gtkcord.NewChannelIDVariant(ch.id))
82
})
83
84
v.scroll = gtk.NewScrolledWindow()
85
v.scroll.SetPropagateNaturalHeight(true)
86
v.scroll.SetHExpand(true)
87
v.scroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)
88
v.scroll.SetChild(v.list)
89
90
v.searchEntry = gtk.NewSearchEntry()
91
v.searchEntry.SetHExpand(true)
92
v.searchEntry.SetVAlign(gtk.AlignCenter)
93
v.searchEntry.SetObjectProperty("placeholder-text", locale.Get("Search Users"))
94
v.searchEntry.ConnectSearchChanged(func() {
95
v.searchString = strings.ToLower(v.searchEntry.Text())
96
v.list.InvalidateFilter()
97
})
98
99
v.searchBar = gtk.NewSearchBar()
100
v.searchBar.AddCSSClass("titlebar")
101
v.searchBar.AddCSSClass("direct-searchbar")
102
v.searchBar.ConnectEntry(&v.searchEntry.EditableTextWidget)
103
v.searchBar.SetSearchMode(true)
104
v.searchBar.SetShowCloseButton(false)
105
v.searchBar.SetChild(v.searchEntry)
106
107
v.box = adw.NewToolbarView()
108
v.box.SetTopBarStyle(adw.ToolbarFlat)
109
v.box.SetContent(v.scroll)
110
v.box.AddTopBar(v.searchBar)
111
112
v.LoadablePage = adaptive.NewLoadablePage()
113
v.LoadablePage.SetLoading()
114
115
vis := gtkutil.WithVisibility(ctx, v)
116
117
state := gtkcord.FromContext(ctx)
118
state.BindHandler(vis, func(ev gateway.Event) {
119
// TODO: Channel events
120
121
switch ev := ev.(type) {
122
case *gateway.ChannelCreateEvent:
123
if !ev.GuildID.IsValid() {
124
v.Invalidate() // recreate everything
125
}
126
case *gateway.ChannelDeleteEvent:
127
v.deleteCh(ev.ID)
128
129
case *gateway.MessageCreateEvent:
130
if ch, ok := v.channels[ev.ChannelID]; ok {
131
ch.Invalidate()
132
}
133
case *read.UpdateEvent:
134
if ch, ok := v.channels[ev.ChannelID]; ok {
135
ch.Invalidate()
136
}
137
}
138
},
139
(*gateway.ChannelCreateEvent)(nil),
140
(*gateway.ChannelDeleteEvent)(nil),
141
(*gateway.MessageCreateEvent)(nil),
142
(*read.UpdateEvent)(nil),
143
)
144
145
return &v
146
}
147
148
// SelectChannel selects a known channel. If none is known, then it is selected
149
// later when the list is changed or never selected if the user selects
150
// something else.
151
func (v *ChannelView) SelectChannel(chID discord.ChannelID) {
152
ch, ok := v.channels[chID]
153
if !ok {
154
v.selectID = chID
155
return
156
}
157
158
v.selectID = 0
159
v.list.SelectRow(ch.ListBoxRow)
160
161
slog.Debug(
162
"selected DM channel immediately",
163
"channel_id", chID)
164
}
165
166
// Invalidate invalidates the whole channel view.
167
func (v *ChannelView) Invalidate() {
168
state := gtkcord.FromContext(v.ctx)
169
170
// Freeze list signals and re-emit it after.
171
v.list.FreezeNotify()
172
defer v.list.ThawNotify()
173
174
// Temporarily disable the sort function. We'll re-enable it once we're
175
// done and force a full re-sort.
176
v.list.SetSortFunc(nil)
177
defer func() {
178
v.list.SetSortFunc(v.sort)
179
v.list.InvalidateSort()
180
}()
181
182
chs, err := state.Cabinet.PrivateChannels()
183
if err != nil {
184
v.SetError(err)
185
return
186
}
187
188
v.LoadablePage.SetChild(v.box)
189
190
// Keep track of channels that aren't in the list anymore.
191
keep := make(map[discord.ChannelID]bool, len(v.channels))
192
for id := range v.channels {
193
keep[id] = false
194
}
195
196
for i, channel := range chs {
197
ch, ok := v.channels[channel.ID]
198
if !ok {
199
ch = NewChannel(v.ctx, channel.ID)
200
v.channels[channel.ID] = ch
201
}
202
203
ch.Update(&chs[i])
204
205
if _, ok := keep[channel.ID]; ok {
206
keep[channel.ID] = true
207
} else {
208
v.list.Append(ch)
209
}
210
}
211
212
// Remove channels that didn't appear in the tracking map.
213
for id, new := range keep {
214
if !new {
215
v.deleteCh(id)
216
}
217
}
218
219
// If we have a channel to be selected, then select it.
220
if v.selectID.IsValid() {
221
if ch, ok := v.channels[v.selectID]; ok {
222
v.list.SelectRow(ch.ListBoxRow)
223
224
slog.Debug(
225
"finally found DM channel to select",
226
"channel_id", v.selectID)
227
}
228
}
229
}
230
231
func (v *ChannelView) deleteCh(id discord.ChannelID) {
232
ch, ok := v.channels[id]
233
if !ok {
234
return
235
}
236
237
v.list.Remove(ch)
238
delete(v.channels, id)
239
}
240
241
func (v *ChannelView) sort(r1, r2 *gtk.ListBoxRow) int { // -1 == less == r1 first
242
ch1 := v.rowChannel(r1)
243
ch2 := v.rowChannel(r2)
244
if ch1 == nil {
245
return 1
246
}
247
if ch2 == nil {
248
return -1
249
}
250
251
last1 := ch1.LastMessageID()
252
last2 := ch2.LastMessageID()
253
254
if !last1.IsValid() {
255
return 1
256
}
257
if !last2.IsValid() {
258
return -1
259
}
260
if last1 > last2 {
261
// ch1 is older, put first.
262
return -1
263
}
264
if last1 == last2 {
265
return 0
266
}
267
return 1 // newer
268
}
269
270
func (v *ChannelView) filter(r *gtk.ListBoxRow) bool {
271
if v.searchString == "" {
272
return true
273
}
274
275
ch := v.rowChannel(r)
276
if ch == nil {
277
return false
278
}
279
280
name := strings.ToLower(ch.Name())
281
return strings.Contains(name, v.searchString)
282
}
283
284
func (v *ChannelView) rowChannel(r *gtk.ListBoxRow) *Channel {
285
id, err := discord.ParseSnowflake(r.Name())
286
if err != nil {
287
slog.Error(
288
"failed to parse channel row name as snowflake",
289
"row_name", r.Name(),
290
"err", err)
291
return nil
292
}
293
294
ch, ok := v.channels[discord.ChannelID(id)]
295
if !ok {
296
slog.Warn(
297
"ChannelView contains channel with unknown ID",
298
"channel_id", id)
299
return nil
300
}
301
302
return ch
303
}
304
305