mirror of https://github.com/Bios-Marcel/cordless
386 lines
11 KiB
Go
386 lines
11 KiB
Go
package ui
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/Bios-Marcel/cordless/ui/tviewutil"
|
|
|
|
"github.com/Bios-Marcel/cordless/tview"
|
|
"github.com/Bios-Marcel/discordgo"
|
|
|
|
"github.com/Bios-Marcel/cordless/config"
|
|
"github.com/Bios-Marcel/cordless/discordutil"
|
|
"github.com/Bios-Marcel/cordless/readstate"
|
|
)
|
|
|
|
type channelState int
|
|
|
|
const (
|
|
channelLoaded channelState = iota
|
|
channelUnread
|
|
channelMentioned
|
|
channelRead
|
|
)
|
|
|
|
var (
|
|
mentionedIndicator = "(@)"
|
|
nsfwIndicator = tviewutil.Escape("🔞")
|
|
lockedIndicator = tviewutil.Escape("\U0001F512")
|
|
)
|
|
|
|
// ChannelTree is the component that displays the channel hierarchy of the
|
|
// currently loaded guild and allows interactions with those channels.
|
|
type ChannelTree struct {
|
|
*tview.TreeView
|
|
*sync.Mutex
|
|
|
|
state *discordgo.State
|
|
|
|
onChannelSelect func(channelID string)
|
|
channelStates map[*tview.TreeNode]channelState
|
|
channelPosition map[string]int
|
|
prefixes map[string][]string
|
|
}
|
|
|
|
// NewChannelTree creates a new ready-to-be-used ChannelTree
|
|
func NewChannelTree(state *discordgo.State) *ChannelTree {
|
|
channelTree := &ChannelTree{
|
|
state: state,
|
|
TreeView: tview.NewTreeView(),
|
|
channelStates: make(map[*tview.TreeNode]channelState),
|
|
channelPosition: make(map[string]int),
|
|
prefixes: make(map[string][]string),
|
|
Mutex: &sync.Mutex{},
|
|
}
|
|
|
|
channelTree.
|
|
SetVimBindingsEnabled(config.Current.OnTypeInListBehaviour == config.DoNothingOnTypeInList).
|
|
SetCycleSelection(true).
|
|
SetTopLevel(1).
|
|
SetBorder(true).
|
|
SetIndicateOverflow(true)
|
|
|
|
channelTree.SetRoot(tview.NewTreeNode(""))
|
|
channelTree.SetSelectedFunc(func(node *tview.TreeNode) {
|
|
channelID, ok := node.GetReference().(string)
|
|
if ok && channelTree.onChannelSelect != nil {
|
|
channelTree.onChannelSelect(channelID)
|
|
}
|
|
})
|
|
|
|
return channelTree
|
|
}
|
|
|
|
// Clear resets all current state.
|
|
func (channelTree *ChannelTree) Clear() {
|
|
channelTree.channelStates = make(map[*tview.TreeNode]channelState)
|
|
channelTree.channelPosition = make(map[string]int)
|
|
channelTree.GetRoot().ClearChildren()
|
|
}
|
|
|
|
// LoadGuild accesses the state in order to load all locally present channels
|
|
// for the passed guild.
|
|
func (channelTree *ChannelTree) LoadGuild(guildID string) error {
|
|
state := channelTree.state
|
|
guild, cacheError := state.Guild(guildID)
|
|
if cacheError != nil {
|
|
return cacheError
|
|
}
|
|
|
|
channelTree.Clear()
|
|
|
|
channels := guild.Channels
|
|
sort.Slice(channels, func(a, b int) bool {
|
|
return channels[a].Position < channels[b].Position
|
|
})
|
|
|
|
// Top level channel
|
|
for _, channel := range channels {
|
|
if (channel.Type != discordgo.ChannelTypeGuildText && channel.Type != discordgo.ChannelTypeGuildNews) ||
|
|
channel.ParentID != "" || !discordutil.HasReadMessagesPermission(channel.ID, state) {
|
|
continue
|
|
}
|
|
channelTree.createTopLevelChannelNodes(channel)
|
|
}
|
|
// Categories; Must be handled before second level channels, as the
|
|
// categories serve as parents.
|
|
CATEGORY_LOOP:
|
|
for _, channel := range channels {
|
|
if channel.Type != discordgo.ChannelTypeGuildCategory || channel.ParentID != "" {
|
|
continue
|
|
}
|
|
|
|
childless := true
|
|
for _, potentialChild := range channels {
|
|
if potentialChild.ParentID == channel.ID {
|
|
if discordutil.HasReadMessagesPermission(potentialChild.ID, state) {
|
|
//We have at least one child with read-permissions,
|
|
//therefore we add the category as the channel will need
|
|
//a parent.
|
|
channelTree.createChannelCategoryNode(channel)
|
|
continue CATEGORY_LOOP
|
|
}
|
|
|
|
//Has at least once child-channel, so we don't need to add a
|
|
//category later on, if none of the child-channels is
|
|
//accessible by the currently logged on user.
|
|
childless = false
|
|
}
|
|
}
|
|
|
|
//If the category is childless, we want to add it anyway.
|
|
if childless {
|
|
channelTree.createChannelCategoryNode(channel)
|
|
}
|
|
}
|
|
// Second level channel
|
|
for _, channel := range channels {
|
|
//Only Text and News are supported. If new channel types are
|
|
//added, support first needs to be confirmed or implemented. This is
|
|
//in order to avoid faulty runtime behaviour.
|
|
if (channel.Type != discordgo.ChannelTypeGuildText && channel.Type != discordgo.ChannelTypeGuildNews) ||
|
|
channel.ParentID == "" || !discordutil.HasReadMessagesPermission(channel.ID, state) {
|
|
continue
|
|
}
|
|
channelTree.createSecondLevelChannelNodes(channel)
|
|
}
|
|
channelTree.SetCurrentNode(channelTree.GetRoot())
|
|
return nil
|
|
}
|
|
|
|
func (channelTree *ChannelTree) createTopLevelChannelNodes(channel *discordgo.Channel) {
|
|
channelNode := channelTree.createTextChannelNode(channel)
|
|
channelTree.GetRoot().AddChild(channelNode)
|
|
}
|
|
|
|
func (channelTree *ChannelTree) createChannelCategoryNode(channel *discordgo.Channel) {
|
|
channelNode := channelTree.createChannelNode(channel)
|
|
channelTree.GetRoot().AddChild(channelNode)
|
|
}
|
|
|
|
func (channelTree *ChannelTree) createSecondLevelChannelNodes(channel *discordgo.Channel) {
|
|
parentNode := tviewutil.GetNodeByReference(channel.ParentID, channelTree.TreeView)
|
|
if parentNode != nil {
|
|
channelNode := channelTree.createTextChannelNode(channel)
|
|
parentNode.AddChild(channelNode)
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) createChannelNode(channel *discordgo.Channel) *tview.TreeNode {
|
|
channelNode := tview.NewTreeNode(tviewutil.Escape(channel.Name))
|
|
if channel.NSFW {
|
|
channelNode.AddPrefix(nsfwIndicator)
|
|
}
|
|
|
|
// Adds a padlock prefix if the channel if not readable by the everyone group
|
|
if config.Current.IndicateChannelAccessRestriction {
|
|
for _, permission := range channel.PermissionOverwrites {
|
|
if permission.Type == "role" && permission.ID == channel.GuildID && permission.Deny&discordgo.PermissionViewChannel == discordgo.PermissionViewChannel {
|
|
channelNode.AddPrefix(lockedIndicator)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
channelNode.SetReference(channel.ID)
|
|
return channelNode
|
|
}
|
|
|
|
func (channelTree *ChannelTree) createTextChannelNode(channel *discordgo.Channel) *tview.TreeNode {
|
|
channelNode := channelTree.createChannelNode(channel)
|
|
|
|
if !readstate.HasBeenRead(channel, channel.LastMessageID) {
|
|
channelTree.channelStates[channelNode] = channelUnread
|
|
channelTree.markNodeAsUnread(channelNode)
|
|
}
|
|
|
|
if readstate.HasBeenMentioned(channel.ID) {
|
|
channelTree.markNodeAsMentioned(channelNode, channel.ID)
|
|
}
|
|
|
|
return channelNode
|
|
}
|
|
|
|
// AddOrUpdateChannel either adds a new node for the given channel or updates
|
|
// its current node.
|
|
func (channelTree *ChannelTree) AddOrUpdateChannel(channel *discordgo.Channel) {
|
|
var updated bool
|
|
channelTree.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {
|
|
nodeChannelID, ok := node.GetReference().(string)
|
|
if ok && nodeChannelID == channel.ID {
|
|
//TODO Support Re-Parenting
|
|
/*oldPosition := channelTree.channelPosition[channel.ID]
|
|
oldParentID, parentOk := parent.GetReference().(string)
|
|
if (!parentOk && channel.ParentID != "") || (oldPosition != channel.Position) ||
|
|
(parentOk && channel.ParentID != oldParentID) {
|
|
|
|
}*/
|
|
|
|
updated = true
|
|
node.SetText(tviewutil.Escape(channel.Name))
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if !updated {
|
|
channelNode := channelTree.createChannelNode(channel)
|
|
if channel.ParentID == "" {
|
|
channelTree.GetRoot().AddChild(channelNode)
|
|
} else {
|
|
parentNode := tviewutil.GetNodeByReference(channel.ParentID, channelTree.TreeView)
|
|
if parentNode != parentNode {
|
|
parentNode.AddChild(channelNode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RemoveChannel removes a channels node from the tree.
|
|
func (channelTree *ChannelTree) RemoveChannel(channel *discordgo.Channel) {
|
|
channelID := channel.ID
|
|
|
|
if channel.Type == discordgo.ChannelTypeGuildText {
|
|
channelTree.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {
|
|
nodeChannelID, ok := node.GetReference().(string)
|
|
if ok && nodeChannelID == channelID {
|
|
channelTree.removeNode(node, parent, channelID)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
} else if channel.Type == discordgo.ChannelTypeGuildCategory {
|
|
node := tviewutil.GetNodeByReference(channelID, channelTree.TreeView)
|
|
if node != nil {
|
|
oldChildren := node.GetChildren()
|
|
node.SetChildren(make([]*tview.TreeNode, 0))
|
|
channelTree.removeNode(node, channelTree.GetRoot(), channelID)
|
|
channelTree.GetRoot().SetChildren(append(channelTree.GetRoot().GetChildren(), oldChildren...))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) removeNode(node, parent *tview.TreeNode, channelID string) {
|
|
delete(channelTree.channelStates, node)
|
|
delete(channelTree.channelPosition, channelID)
|
|
children := parent.GetChildren()
|
|
if len(children) == 1 {
|
|
parent.SetChildren(make([]*tview.TreeNode, 0))
|
|
} else {
|
|
var childIndex int
|
|
for index, child := range children {
|
|
if child == node {
|
|
childIndex = index
|
|
}
|
|
}
|
|
|
|
parent.SetChildren(append(children[:childIndex], children[childIndex+1:]...))
|
|
}
|
|
}
|
|
|
|
// MarkAsUnread marks a channel as unread.
|
|
func (channelTree *ChannelTree) MarkAsUnread(channelID string) {
|
|
node := tviewutil.GetNodeByReference(channelID, channelTree.TreeView)
|
|
if node != nil {
|
|
channelTree.channelStates[node] = channelUnread
|
|
channelTree.markNodeAsUnread(node)
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) markNodeAsUnread(node *tview.TreeNode) {
|
|
if tview.IsVtxxx {
|
|
node.SetBlinking(true)
|
|
node.SetUnderline(false)
|
|
} else {
|
|
node.SetColor(config.GetTheme().AttentionColor)
|
|
}
|
|
}
|
|
|
|
// MarkAsRead marks a channel as read.
|
|
func (channelTree *ChannelTree) MarkAsRead(channelID string) {
|
|
node := tviewutil.GetNodeByReference(channelID, channelTree.TreeView)
|
|
if node != nil {
|
|
channelTree.channelStates[node] = channelRead
|
|
channelTree.markNodeAsRead(node)
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) markNodeAsRead(node *tview.TreeNode) {
|
|
if tview.IsVtxxx {
|
|
node.SetBlinking(false)
|
|
node.SetUnderline(false)
|
|
} else {
|
|
node.SetColor(config.GetTheme().PrimaryTextColor)
|
|
}
|
|
node.RemovePrefix(mentionedIndicator)
|
|
}
|
|
|
|
// MarkAsMentioned marks a channel as mentioned.
|
|
func (channelTree *ChannelTree) MarkAsMentioned(channelID string) {
|
|
node := tviewutil.GetNodeByReference(channelID, channelTree.TreeView)
|
|
if node != nil {
|
|
channelTree.channelStates[node] = channelMentioned
|
|
channelTree.markNodeAsMentioned(node, channelID)
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) markNodeAsMentioned(node *tview.TreeNode, channelID string) {
|
|
channelTree.markNodeAsUnread(node)
|
|
node.AddPrefix(mentionedIndicator)
|
|
node.SortPrefixes(channelTree.prefixSorter)
|
|
}
|
|
|
|
func (channelTree *ChannelTree) prefixSorter(a, b string) bool {
|
|
if a == mentionedIndicator {
|
|
return true
|
|
} else if b == mentionedIndicator {
|
|
return false
|
|
} else if a == nsfwIndicator {
|
|
return true
|
|
} else if b == nsfwIndicator {
|
|
return false
|
|
} else if a == lockedIndicator {
|
|
return true
|
|
} else if b == lockedIndicator {
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MarkAsLoaded marks a channel as loaded and therefore marks all other
|
|
// channels as either unread, read or mentioned.
|
|
func (channelTree *ChannelTree) MarkAsLoaded(channelID string) {
|
|
for node, state := range channelTree.channelStates {
|
|
if state == channelLoaded {
|
|
channelTree.channelStates[node] = channelRead
|
|
channelTree.markNodeAsRead(node)
|
|
break
|
|
}
|
|
}
|
|
|
|
node := tviewutil.GetNodeByReference(channelID, channelTree.TreeView)
|
|
if node != nil {
|
|
channelTree.channelStates[node] = channelLoaded
|
|
channelTree.markNodeAsLoaded(node)
|
|
}
|
|
}
|
|
|
|
func (channelTree *ChannelTree) markNodeAsLoaded(node *tview.TreeNode) {
|
|
if tview.IsVtxxx {
|
|
node.SetUnderline(true)
|
|
node.SetBlinking(false)
|
|
} else {
|
|
node.SetColor(tview.Styles.ContrastBackgroundColor)
|
|
}
|
|
node.RemovePrefix(mentionedIndicator)
|
|
}
|
|
|
|
// SetOnChannelSelect sets the handler that reacts to channel selection events.
|
|
func (channelTree *ChannelTree) SetOnChannelSelect(handler func(channelID string)) {
|
|
channelTree.onChannelSelect = handler
|
|
}
|