Path: blob/main/internal/sidebar/channels/channels_model.go
366 views
package channels12import (3"log/slog"4"sort"56"github.com/diamondburned/arikawa/v3/discord"7"github.com/diamondburned/arikawa/v3/gateway"8"github.com/diamondburned/gotk4/pkg/core/glib"9"github.com/diamondburned/gotk4/pkg/gio/v2"10"github.com/diamondburned/gotk4/pkg/gtk/v4"11"libdb.so/dissent/internal/gtkcord"12"libdb.so/dissent/internal/signaling"13)1415type modelManager struct {16*gtk.TreeListModel17state *gtkcord.State18guildID discord.GuildID19}2021func newModelManager(state *gtkcord.State, guildID discord.GuildID) *modelManager {22m := &modelManager{23state: state,24guildID: guildID,25}26m.TreeListModel = gtk.NewTreeListModel(27m.Model(0), true, true,28func(item *glib.Object) *gio.ListModel {29chID := channelIDFromItem(item)3031model := m.Model(chID)32if model == nil {33return nil34}3536return &model.ListModel37})38return m39}4041// Model returns the list model containing all channels within the given channel42// ID. If chID is 0, then the guild's root channels will be returned. This43// function may return nil, indicating that the channel will never have any44// children.45func (m *modelManager) Model(chID discord.ChannelID) *gtk.StringList {46model := gtk.NewStringList(nil)4748list := newChannelList(m.state, model)4950var unbind signaling.DisconnectStack51list.ConnectDestroy(func() { unbind.Disconnect() })5253unbind.Push(54m.state.AddHandler(func(ev *gateway.ChannelCreateEvent) {55if ev.GuildID != m.guildID {56return57}58if ev.Channel.ParentID == chID {59list.Append(ev.Channel)60}61}),62m.state.AddHandler(func(ev *gateway.ChannelUpdateEvent) {63if ev.GuildID != m.guildID {64return65}66// Handle channel position moves.67if ev.Channel.ParentID == chID {68list.Append(ev.Channel)69} else {70list.Remove(ev.Channel.ID)71}72}),73m.state.AddHandler(func(ev *gateway.ThreadCreateEvent) {74if ev.GuildID != m.guildID {75return76}77if ev.Channel.ParentID == chID {78list.Append(ev.Channel)79}80}),81m.state.AddHandler(func(ev *gateway.ThreadDeleteEvent) {82if ev.GuildID != m.guildID {83return84}85if ev.ParentID == chID {86list.Remove(ev.ID)87}88}),89m.state.AddHandler(func(ev *gateway.ThreadListSyncEvent) {90if ev.GuildID != m.guildID {91return92}9394if ev.ChannelIDs == nil {95// The entire guild was synced, so invalidate everything.96m.invalidateAll(chID, list)97return98}99100for _, parentID := range ev.ChannelIDs {101if parentID == chID {102// This sync event is also for us.103m.invalidateAll(chID, list)104break105}106}107}),108)109110m.invalidateAll(chID, list)111return model112}113114func (m *modelManager) invalidateAll(parentID discord.ChannelID, list *channelList) {115channels := fetchSortedChannels(m.state, m.guildID, parentID)116list.ClearAndAppend(channels)117}118119// channelList wraps a StringList to maintain a set of channel IDs.120// Because this is a set, each channel ID can only appear once.121type channelList struct {122state *gtkcord.State123list *gtk.StringList124ids []discord.ChannelID125}126127func newChannelList(state *gtkcord.State, list *gtk.StringList) *channelList {128return &channelList{129state: state,130list: list,131ids: make([]discord.ChannelID, 0, 4),132}133}134135// CalculatePosition converts the position of a channel given by Discord to the136// position relative to the list. If the channel is not found, then this137// function returns the end of the list.138func (l *channelList) CalculatePosition(target discord.Channel) uint {139for i, id := range l.ids {140ch, _ := l.state.Offline().Channel(id)141if ch == nil {142continue143}144145if ch.Position > target.Position {146return uint(i)147}148}149150return uint(len(l.ids))151}152153// Append appends a channel to the list. If the channel already exists, then154// this function does nothing.155func (l *channelList) Append(ch discord.Channel) {156pos := l.CalculatePosition(ch)157l.insertAt(ch, pos)158}159160func (l *channelList) insertAt(ch discord.Channel, pos uint) {161i := l.Index(ch.ID)162if i != -1 {163return164}165166list := l.list167if list == nil {168return169}170171list.Splice(pos, 0, []string{ch.ID.String()})172l.ids = append(l.ids[:pos], append([]discord.ChannelID{ch.ID}, l.ids[pos:]...)...)173}174175// Remove removes the channel ID from the list. If the channel ID is not in the176// list, then this function does nothing.177func (l *channelList) Remove(chID discord.ChannelID) {178i := l.Index(chID)179if i != -1 {180l.ids = append(l.ids[:i], l.ids[i+1:]...)181182list := l.list183if list != nil {184list.Remove(uint(i))185}186}187}188189// Contains returns whether the channel ID is in the list.190func (l *channelList) Contains(chID discord.ChannelID) bool {191return l.Index(chID) != -1192}193194// Index returns the index of the channel ID in the list. If the channel ID is195// not in the list, then this function returns -1.196func (l *channelList) Index(chID discord.ChannelID) int {197for i, id := range l.ids {198if id == chID {199return i200}201}202return -1203}204205// Clear clears the list.206func (l *channelList) Clear() {207l.ids = l.ids[:0]208209list := l.list210if list != nil {211list.Splice(0, list.NItems(), nil)212}213}214215// ClearAndAppend clears the list and appends the given channels.216func (l *channelList) ClearAndAppend(chs []discord.Channel) {217list := l.list218if list == nil {219return220}221222ids := make([]string, len(chs))223l.ids = make([]discord.ChannelID, len(chs))224225for i, ch := range chs {226ids[i] = ch.ID.String()227l.ids = append(l.ids, ch.ID)228}229230list.Splice(0, list.NItems(), ids)231}232233func (l *channelList) ConnectDestroy(f func()) {234list := l.list235if list == nil {236return237}238// I think this is the only way to know if a ListModel is no longer239// being used? At least from reading the source code, which just calls240// g_clear_pointer.241glib.WeakRefObject(list, f)242}243244func fetchSortedChannels(state *gtkcord.State, guildID discord.GuildID, parentID discord.ChannelID) []discord.Channel {245channels, err := state.Offline().Channels(guildID, gtkcord.AllowedChannelTypes)246if err != nil {247slog.Error(248"failed to get guild channels to sort",249"guild_id", guildID,250"err", err)251return nil252}253254// Filter out all channels that are not in the same parent channel.255filtered := channels[:0]256for i, ch := range channels {257if ch.ParentID == parentID || (parentID == 0 && !ch.ParentID.IsValid()) {258filtered = append(filtered, channels[i])259}260}261262// Sort so that the channels are in increasing order.263sort.Slice(filtered, func(i, j int) bool {264a := filtered[i]265b := filtered[j]266if a.Position == b.Position {267return a.ID < b.ID268}269return a.Position < b.Position270})271272return filtered273}274275276