Path: blob/main/internal/sidebar/guilds/guilds.go
366 views
package guilds12import (3"context"4"log/slog"5"sort"67"github.com/diamondburned/arikawa/v3/discord"8"github.com/diamondburned/arikawa/v3/gateway"9"github.com/diamondburned/gotk4/pkg/gtk/v4"10"github.com/diamondburned/gotkit/app"11"github.com/diamondburned/gotkit/gtkutil"12"github.com/diamondburned/gotkit/gtkutil/cssutil"13"github.com/diamondburned/ningen/v3/states/read"14"github.com/pkg/errors"15"libdb.so/dissent/internal/gtkcord"16)1718// ViewChild is a child inside the guilds view. It is either a *Guild or a19// *Folder containing more *Guilds.20type ViewChild interface {21gtk.Widgetter22viewChild()23}2425// View contains a list of guilds and folders.26type View struct {27*gtk.Box28Children []ViewChild2930current currentGuild3132ctx context.Context33}3435var viewCSS = cssutil.Applier("guild-view", `36.guild-view {37margin: 4px 0;38}39.guild-view button:active:not(:hover) {40background: initial;41}42`)4344// NewView creates a new View.45func NewView(ctx context.Context) *View {46v := View{47ctx: ctx,48}4950v.Box = gtk.NewBox(gtk.OrientationVertical, 0)51viewCSS(v)5253cancellable := gtkutil.WithVisibility(ctx, v)5455state := gtkcord.FromContext(ctx)56state.BindHandler(cancellable, func(ev gateway.Event) {57switch ev := ev.(type) {58case *gateway.ReadyEvent, *gateway.ResumedEvent:59// Recreate the whole list in case we have some new info.60v.Invalidate()6162case *read.UpdateEvent:63if guild := v.Guild(ev.GuildID); guild != nil {64guild.InvalidateUnread()65}66case *gateway.ChannelCreateEvent:67if ev.GuildID.IsValid() {68if guild := v.Guild(ev.GuildID); guild != nil {69guild.InvalidateUnread()70}71}72case *gateway.GuildCreateEvent:73if guild := v.Guild(ev.ID); guild != nil {74guild.Update(&ev.Guild)75} else {76v.AddGuild(&ev.Guild)77}78case *gateway.GuildUpdateEvent:79if guild := v.Guild(ev.ID); guild != nil {80guild.Invalidate()81}82case *gateway.GuildDeleteEvent:83if ev.Unavailable {84if guild := v.Guild(ev.ID); guild != nil {85guild.SetUnavailable()8687parent := gtk.BaseWidget(guild.Parent())88parent.ActivateAction("win.reset-view", nil)89return90}91}9293guild := v.RemoveGuild(ev.ID)94if guild != nil && guild.IsSelected() {95parent := gtk.BaseWidget(guild.Parent())96parent.ActivateAction("win.reset-view", nil)97}98}99})100101return &v102}103104// InvalidateUnreads invalidates the unread states of all guilds.105func (v *View) InvalidateUnreads() {106for _, child := range v.Children {107if child, ok := child.(*Guild); ok {108child.InvalidateUnread()109}110}111}112113// Invalidate invalidates the view and recreates everything. Use with care.114func (v *View) Invalidate() {115// TODO: reselect.116117state := gtkcord.FromContext(v.ctx)118ready := state.Ready()119120if ready.UserSettings != nil {121switch {122case ready.UserSettings.GuildFolders != nil:123v.SetFolders(ready.UserSettings.GuildFolders)124case ready.UserSettings.GuildPositions != nil:125v.SetGuildsFromIDs(ready.UserSettings.GuildPositions)126}127}128129guilds, err := state.Cabinet.Guilds()130if err != nil {131app.Error(v.ctx, errors.Wrap(err, "cannot get guilds"))132return133}134135// Sort so that the guilds that we've joined last are at the bottom.136// This means we can prepend guilds as we go, and the latest one will be137// prepended to the top.138sort.Slice(guilds, func(i, j int) bool {139ti, ok := state.GuildState.JoinedAt(guilds[i].ID)140if !ok {141return false // put last142}143tj, ok := state.GuildState.JoinedAt(guilds[j].ID)144if !ok {145return true146}147return ti.Before(tj)148})149150// Construct a map of shownGuilds guilds, so we know to not create a151// guild if it's already shown.152shownGuilds := make(map[discord.GuildID]struct{}, 200)153v.eachGuild(func(g *Guild) bool {154shownGuilds[g.ID()] = struct{}{}155return false156})157158for i, guild := range guilds {159_, shown := shownGuilds[guild.ID]160if shown {161continue162}163164g := NewGuild(v.ctx, guild.ID)165g.Update(&guilds[i])166167// Prepend the guild.168v.prepend(g)169}170}171172// SetFolders sets the guild folders to use.173func (v *View) SetFolders(folders []gateway.GuildFolder) {174restore := v.saveSelection()175defer restore()176177v.clear()178179for i, folder := range folders {180if folder.ID == 0 {181// Contains a single guild, so we just unbox it.182g := NewGuild(v.ctx, folder.GuildIDs[0])183g.Invalidate()184185v.append(g)186continue187}188189f := NewFolder(v.ctx)190f.Set(&folders[i])191192v.append(f)193}194}195196// AddGuild prepends a single guild into the view.197func (v *View) AddGuild(guild *discord.Guild) {198g := NewGuild(v.ctx, guild.ID)199g.Update(guild)200201v.Box.Prepend(g)202v.Children = append([]ViewChild{g}, v.Children...)203}204205// RemoveGuild removes the given guild.206func (v *View) RemoveGuild(id discord.GuildID) *Guild {207guild := v.Guild(id)208if guild == nil {209return nil210}211212if folder := guild.ParentFolder(); folder != nil {213folder.Remove(guild.ID())214if len(folder.Guilds) == 0 {215v.remove(folder)216}217} else {218v.remove(guild)219}220221return guild222}223224// SetGuildsFromIDs calls SetGuilds with guilds fetched from the state by the225// given ID list.226func (v *View) SetGuildsFromIDs(guildIDs []discord.GuildID) {227restore := v.saveSelection()228defer restore()229230v.clear()231232for _, id := range guildIDs {233g := NewGuild(v.ctx, id)234g.Invalidate()235236v.append(g)237}238}239240// SetGuilds sets the guilds shown.241func (v *View) SetGuilds(guilds []discord.Guild) {242restore := v.saveSelection()243defer restore()244245v.clear()246247for i, guild := range guilds {248g := NewGuild(v.ctx, guild.ID)249g.Update(&guilds[i])250251v.append(g)252}253}254255func (v *View) append(this ViewChild) {256v.Children = append(v.Children, this)257v.Box.Append(this)258}259260func (v *View) prepend(this ViewChild) {261v.Children = append(v.Children, nil)262copy(v.Children[1:], v.Children)263v.Children[0] = this264265v.Box.Prepend(this)266}267268func (v *View) remove(this ViewChild) {269for i, child := range v.Children {270if child == this {271v.Children = append(v.Children[:i], v.Children[i+1:]...)272v.Box.Remove(child)273break274}275}276}277278func (v *View) clear() {279for _, child := range v.Children {280v.Box.Remove(child)281}282v.Children = nil283}284285// SelectedGuildID returns the selected guild ID, if any.286func (v *View) SelectedGuildID() discord.GuildID {287if v.current.guild == nil {288return 0289}290return v.current.guild.id291}292293// Guild finds a guild inside View by its ID.294func (v *View) Guild(id discord.GuildID) *Guild {295var guild *Guild296v.eachGuild(func(g *Guild) bool {297if g.ID() == id {298guild = g299return true300}301return false302})303return guild304}305306func (v *View) eachGuild(f func(*Guild) (stop bool)) {307for _, child := range v.Children {308switch child := child.(type) {309case *Guild:310if f(child) {311return312}313case *Folder:314for _, guild := range child.Guilds {315if f(guild) {316return317}318}319}320}321}322323// SetSelectedGuild sets the selected guild. It does not propagate the selection324// to the sidebar. If the ID is invalid, it unselects the current guild325// selection.326func (v *View) SetSelectedGuild(id discord.GuildID) {327if !id.IsValid() {328v.Unselect()329return330}331332guild := v.Guild(id)333if guild == nil {334slog.Error(335"cannot select guild since it's not found in guild view",336"guild_id", id)337v.Unselect()338return339}340341current := currentGuild{342guild: guild,343folder: guild.ParentFolder(),344}345346if current != v.current {347v.Unselect()348v.current = current349v.current.SetSelected(true)350}351}352353// Unselect unselects any guilds inside this guild view. Use this when the354// window is showing a channel that's not from any guild.355func (v *View) Unselect() {356v.current.Unselect()357v.current = currentGuild{}358}359360// saveSelection saves the current guild selection to be restored later using361// the returned callback.362func (v *View) saveSelection() (restore func()) {363if v.current.guild == nil {364// Nothing to restore.365return func() {}366}367368guildID := v.current.guild.id369return func() {370parent := gtk.BaseWidget(v.Parent())371parent.ActivateAction("win.open-guild", gtkcord.NewGuildIDVariant(guildID))372}373}374375type currentGuild struct {376guild *Guild377folder *Folder378}379380func (c currentGuild) Unselect() {381c.SetSelected(false)382}383384func (c currentGuild) SetSelected(selected bool) {385if c.folder != nil {386c.folder.SetSelected(selected)387}388if c.guild != nil {389c.guild.SetSelected(selected)390}391}392393394