cordless/ui/usertree.go

346 lines
8.7 KiB
Go

package ui
import (
"sort"
"sync"
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/discordutil"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
"github.com/Bios-Marcel/cordless/tview"
"github.com/Bios-Marcel/discordgo"
tcell "github.com/gdamore/tcell/v2"
)
// UserTree represents the visual list of users in a guild.
type UserTree struct {
*sync.Mutex
internalTreeView *tview.TreeView
state *discordgo.State
userNodes map[string]*tview.TreeNode
roleNodes map[string]*tview.TreeNode
roles []*discordgo.Role
loadedFor interface{}
}
// NewUserTree creates a new pre-configured UserTree that is empty.
func NewUserTree(state *discordgo.State) *UserTree {
userTree := &UserTree{
state: state,
internalTreeView: tview.NewTreeView(),
Mutex: &sync.Mutex{},
}
userTree.internalTreeView.
SetVimBindingsEnabled(config.Current.OnTypeInListBehaviour == config.DoNothingOnTypeInList).
SetRoot(tview.NewTreeNode("")).
SetTopLevel(1).
SetCycleSelection(true).
SetBorder(true)
return userTree
}
// Clear removes all nodes and data out of the view.
func (userTree *UserTree) Clear() {
userTree.Lock()
defer userTree.Unlock()
userTree.clear()
}
func (userTree *UserTree) rootNode() *tview.TreeNode {
return userTree.internalTreeView.GetRoot()
}
func (userTree *UserTree) clear() {
for _, roleNode := range userTree.roleNodes {
roleNode.ClearChildren()
}
userTree.rootNode().ClearChildren()
userTree.loadedFor = nil
// After clearing, we don't reallocate anything, since we don't know
// whether we actually want to repopulate the tree.
userTree.userNodes = nil
userTree.roleNodes = nil
userTree.roles = nil
}
// LoadGroup loads all users for a group-channel.
func (userTree *UserTree) LoadGroup(channelID string) error {
userTree.Lock()
defer userTree.Unlock()
channel, stateError := userTree.state.PrivateChannel(channelID)
if stateError != nil {
return stateError
}
//Already loaded
if channel == userTree.loadedFor {
return nil
}
userTree.clear()
userTree.userNodes = make(map[string]*tview.TreeNode)
userTree.roleNodes = make(map[string]*tview.TreeNode)
userTree.addOrUpdateUsers(channel.Recipients)
userTree.loadedFor = channel
userTree.selectFirstNode()
return nil
}
// LoadGuild will load all available roles of the guild and then load all
// available members. Afterwards the first available node will be selected.
func (userTree *UserTree) LoadGuild(guildID string) error {
userTree.Lock()
defer userTree.Unlock()
guild, stateError := userTree.state.Guild(guildID)
if stateError != nil {
return stateError
}
//Already loaded
if guild == userTree.loadedFor {
return nil
}
userTree.clear()
userTree.userNodes = make(map[string]*tview.TreeNode)
userTree.roleNodes = make(map[string]*tview.TreeNode)
guildRoles, roleLoadError := userTree.loadGuildRoles(guild)
if roleLoadError != nil {
return roleLoadError
}
userTree.roles = guildRoles
userLoadError := userTree.loadGuildMembers(guild)
if userLoadError != nil {
return userLoadError
}
userTree.loadedFor = guild
userTree.selectFirstNode()
return nil
}
func (userTree *UserTree) selectFirstNode() {
if userTree.internalTreeView.GetCurrentNode() == nil {
userNodes := userTree.rootNode().GetChildren()
if len(userNodes) > 0 {
userTree.internalTreeView.SetCurrentNode(userTree.rootNode().GetChildren()[0])
}
}
}
func (userTree *UserTree) loadGuildMembers(guild *discordgo.Guild) error {
members, stateError := userTree.state.Members(guild.ID)
if stateError != nil {
return stateError
}
userTree.addOrUpdateMembers(members)
return nil
}
func (userTree *UserTree) loadGuildRoles(guild *discordgo.Guild) ([]*discordgo.Role, error) {
guildRoles := guild.Roles
sort.Slice(guildRoles, func(a, b int) bool {
return guildRoles[a].Position > guildRoles[b].Position
})
for _, role := range guildRoles {
if role.Hoist {
roleColor := discordutil.GetRoleColor(role)
roleName := tviewutil.Escape(role.Name)
var roleNode *tview.TreeNode
if roleColor != "" {
roleNode = tview.NewTreeNode("[" + roleColor + "]" + roleName)
} else {
roleNode = tview.NewTreeNode(roleName)
}
roleNode.SetSelectable(false)
userTree.roleNodes[role.ID] = roleNode
userTree.rootNode().AddChild(roleNode)
}
}
return guildRoles, nil
}
// AddOrUpdateMember adds the passed member to the tree, unless it is
// already part of the tree, in that case the nodes name is updated.
func (userTree *UserTree) AddOrUpdateMember(member *discordgo.Member) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
userTree.addOrUpdateMember(member)
}
func (userTree *UserTree) addOrUpdateMember(member *discordgo.Member) {
nameToUse := "[" + discordutil.GetMemberColor(userTree.state, member) +
"]" + discordutil.GetMemberName(member)
userNode, contains := userTree.userNodes[member.User.ID]
if contains && userNode != nil {
userNode.SetText(nameToUse)
return
}
userNode = tview.NewTreeNode(nameToUse)
userTree.userNodes[member.User.ID] = userNode
discordutil.SortUserRoles(member.Roles, userTree.roles)
for _, userRole := range member.Roles {
roleNode, exists := userTree.roleNodes[userRole]
if exists && roleNode != nil {
roleNode.AddChild(userNode)
return
}
}
userTree.rootNode().AddChild(userNode)
}
// AddOrUpdateUser adds a user to the tree, unless the user already exists,
// in that case the users node gets updated.
func (userTree *UserTree) AddOrUpdateUser(user *discordgo.User) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
userTree.addOrUpdateUser(user)
}
func (userTree *UserTree) addOrUpdateUser(user *discordgo.User) {
nameToUse := "[" + discordutil.GetUserColor(user) +
"]" + discordutil.GetUserName(user)
userNode, contains := userTree.userNodes[user.ID]
if contains && userNode != nil {
userNode.SetText(nameToUse)
return
}
userNode = tview.NewTreeNode(nameToUse)
userTree.userNodes[user.ID] = userNode
userTree.rootNode().AddChild(userNode)
}
// AddOrUpdateUsers adds users to the tree, unless they already exists, in that
// case the users nodes gets updated.
func (userTree *UserTree) AddOrUpdateUsers(users []*discordgo.User) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
userTree.addOrUpdateUsers(users)
}
func (userTree *UserTree) addOrUpdateUsers(users []*discordgo.User) {
for _, user := range users {
userTree.addOrUpdateUser(user)
}
}
// AddOrUpdateMembers adds the all passed members to the tree, unless a node is
// already part of the tree, in that case the nodes name is updated.
func (userTree *UserTree) AddOrUpdateMembers(members []*discordgo.Member) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
userTree.addOrUpdateMembers(members)
}
func (userTree *UserTree) addOrUpdateMembers(members []*discordgo.Member) {
for _, member := range members {
userTree.addOrUpdateMember(member)
}
}
// RemoveMember finds and removes a node from the tree.
func (userTree *UserTree) RemoveMember(member *discordgo.Member) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
userTree.removeMember(member)
}
func (userTree *UserTree) removeMember(member *discordgo.Member) {
userNode, contains := userTree.userNodes[member.User.ID]
if contains {
userTree.rootNode().Walk(func(node, parent *tview.TreeNode) bool {
if node == userNode {
if len(parent.GetChildren()) == 1 {
parent.SetChildren(make([]*tview.TreeNode, 0))
} else {
indexToDelete := -1
for index, child := range parent.GetChildren() {
if child == node {
indexToDelete = index
break
}
}
if indexToDelete == 0 {
parent.SetChildren(parent.GetChildren()[1:])
} else if indexToDelete == len(parent.GetChildren())-1 {
parent.SetChildren(parent.GetChildren()[:len(parent.GetChildren())-1])
} else {
parent.SetChildren(append(parent.GetChildren()[0:indexToDelete],
parent.GetChildren()[indexToDelete+1:]...))
}
}
return false
}
return true
})
}
}
// RemoveMembers finds and removes all passed members from the tree.
func (userTree *UserTree) RemoveMembers(members []*discordgo.Member) {
userTree.Lock()
defer userTree.Unlock()
if !userTree.isLoaded() {
return
}
for _, member := range members {
userTree.removeMember(member)
}
}
//SetInputCapture delegates to tviews SetInputCapture
func (userTree *UserTree) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) {
userTree.internalTreeView.SetInputCapture(capture)
}
// isLoaded indicates whether the UserList is currently displaying any data.
func (userTree *UserTree) isLoaded() bool {
return userTree.loadedFor != nil
}