mirror of https://github.com/Bios-Marcel/cordless
346 lines
8.7 KiB
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
|
|
}
|