mirror of https://github.com/Bios-Marcel/cordless
382 lines
11 KiB
Go
382 lines
11 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
tcell "github.com/gdamore/tcell/v2"
|
|
|
|
"github.com/Bios-Marcel/cordless/discordutil"
|
|
"github.com/Bios-Marcel/cordless/readstate"
|
|
"github.com/Bios-Marcel/cordless/ui/tviewutil"
|
|
|
|
"github.com/Bios-Marcel/cordless/tview"
|
|
"github.com/Bios-Marcel/discordgo"
|
|
|
|
"github.com/Bios-Marcel/cordless/config"
|
|
)
|
|
|
|
type privateChannelState int
|
|
|
|
const (
|
|
loaded privateChannelState = iota
|
|
unread
|
|
read
|
|
)
|
|
|
|
// PrivateChatList holds the nodes and handlers for the private view. That
|
|
// is the one responsible for managing private chats and friends.
|
|
type PrivateChatList struct {
|
|
internalTreeView *tview.TreeView
|
|
|
|
state *discordgo.State
|
|
|
|
chatsNode *tview.TreeNode
|
|
friendsNode *tview.TreeNode
|
|
|
|
onChannelSelect func(channelID string)
|
|
onFriendSelect func(userID string)
|
|
privateChannelStates map[*tview.TreeNode]privateChannelState
|
|
}
|
|
|
|
// NewPrivateChatList creates a new ready to use private chat list.
|
|
func NewPrivateChatList(state *discordgo.State) *PrivateChatList {
|
|
privateList := &PrivateChatList{
|
|
state: state,
|
|
|
|
internalTreeView: tview.NewTreeView(),
|
|
chatsNode: tview.NewTreeNode("Chats"),
|
|
friendsNode: tview.NewTreeNode("Friends"),
|
|
|
|
privateChannelStates: make(map[*tview.TreeNode]privateChannelState),
|
|
}
|
|
|
|
privateList.internalTreeView.
|
|
SetVimBindingsEnabled(config.Current.OnTypeInListBehaviour == config.DoNothingOnTypeInList).
|
|
SetRoot(tview.NewTreeNode("")).
|
|
SetTopLevel(1).
|
|
SetCycleSelection(true).
|
|
SetSelectedFunc(privateList.onNodeSelected).
|
|
SetBorder(true).
|
|
SetIndicateOverflow(true).
|
|
SetTitle("DMs").
|
|
SetTitleAlign(tview.AlignLeft)
|
|
|
|
privateList.internalTreeView.GetRoot().AddChild(privateList.chatsNode)
|
|
|
|
if !state.User.Bot {
|
|
privateList.internalTreeView.GetRoot().AddChild(privateList.friendsNode)
|
|
}
|
|
|
|
return privateList
|
|
}
|
|
|
|
func (privateList *PrivateChatList) setNotificationCount(count int) {
|
|
if count == 0 {
|
|
privateList.internalTreeView.SetTitle("DMs")
|
|
} else {
|
|
privateList.internalTreeView.SetTitle(fmt.Sprintf("DMs[%s](%d)", tviewutil.ColorToHex(config.GetTheme().AttentionColor), count))
|
|
}
|
|
}
|
|
|
|
func (privateList *PrivateChatList) onNodeSelected(node *tview.TreeNode) {
|
|
if node.GetParent() == privateList.chatsNode {
|
|
if privateList.onChannelSelect != nil {
|
|
channelID, ok := node.GetReference().(string)
|
|
if ok {
|
|
privateList.onChannelSelect(channelID)
|
|
}
|
|
}
|
|
} else if node.GetParent() == privateList.friendsNode {
|
|
if privateList.onFriendSelect != nil {
|
|
userID, ok := node.GetReference().(string)
|
|
if ok {
|
|
privateList.onFriendSelect(userID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddOrUpdateChannel either adds a channel or updates the node if it is
|
|
// already present
|
|
func (privateList *PrivateChatList) AddOrUpdateChannel(channel *discordgo.Channel) {
|
|
for _, node := range privateList.chatsNode.GetChildren() {
|
|
referenceChannelID, ok := node.GetReference().(string)
|
|
if ok && referenceChannelID == channel.ID {
|
|
node.SetText(discordutil.GetPrivateChannelName(channel))
|
|
return
|
|
}
|
|
}
|
|
|
|
if channel.Type == discordgo.ChannelTypeDM {
|
|
user := channel.Recipients[0]
|
|
for _, friendNode := range privateList.friendsNode.GetChildren() {
|
|
userID, ok := friendNode.GetReference().(string)
|
|
if ok && userID == user.ID {
|
|
privateList.RemoveFriend(userID)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
privateList.prependChannel(channel)
|
|
}
|
|
|
|
func (privateList *PrivateChatList) prependChannel(channel *discordgo.Channel) {
|
|
newChildren := append([]*tview.TreeNode{createPrivateChannelNode(channel)}, privateList.chatsNode.GetChildren()...)
|
|
privateList.chatsNode.SetChildren(newChildren)
|
|
}
|
|
|
|
func (privateList *PrivateChatList) addChannel(channel *discordgo.Channel) {
|
|
newNode := createPrivateChannelNode(channel)
|
|
if !readstate.HasBeenRead(channel, channel.LastMessageID) {
|
|
privateList.privateChannelStates[newNode] = unread
|
|
if tview.IsVtxxx {
|
|
newNode.SetBlinking(true)
|
|
} else {
|
|
newNode.SetColor(config.GetTheme().AttentionColor)
|
|
}
|
|
}
|
|
privateList.chatsNode.AddChild(newNode)
|
|
}
|
|
|
|
func createPrivateChannelNode(channel *discordgo.Channel) *tview.TreeNode {
|
|
channelNode := tview.NewTreeNode(discordutil.GetPrivateChannelName(channel))
|
|
channelNode.SetReference(channel.ID)
|
|
return channelNode
|
|
}
|
|
|
|
// AddOrUpdateFriend either adds a friend or updates the node if it is
|
|
// already present.
|
|
func (privateList *PrivateChatList) AddOrUpdateFriend(user *discordgo.User) {
|
|
for _, node := range privateList.chatsNode.GetChildren() {
|
|
referenceChannelID, ok := node.GetReference().(string)
|
|
if ok {
|
|
channel, stateError := privateList.state.Channel(referenceChannelID)
|
|
if stateError == nil && channel.Type == discordgo.ChannelTypeDM {
|
|
if channel.Recipients[0].ID == user.ID {
|
|
node.SetText(discordutil.GetUserName(user))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, node := range privateList.friendsNode.GetChildren() {
|
|
referenceUserID, ok := node.GetReference().(string)
|
|
if ok && referenceUserID == user.ID {
|
|
node.SetText(discordutil.GetUserName(user))
|
|
return
|
|
}
|
|
}
|
|
|
|
privateList.addFriend(user)
|
|
}
|
|
|
|
func (privateList *PrivateChatList) addFriend(user *discordgo.User) {
|
|
friendNode := tview.NewTreeNode(user.Username)
|
|
friendNode.SetReference(user.ID)
|
|
privateList.friendsNode.AddChild(friendNode)
|
|
}
|
|
|
|
// RemoveFriend removes a friend node if present. This will not trigger any
|
|
// action on the channel list.
|
|
func (privateList *PrivateChatList) RemoveFriend(userID string) {
|
|
newChildren := make([]*tview.TreeNode, 0)
|
|
|
|
for _, node := range privateList.friendsNode.GetChildren() {
|
|
referenceUserID, ok := node.GetReference().(string)
|
|
if !ok || ok && userID != referenceUserID {
|
|
newChildren = append(newChildren, node)
|
|
}
|
|
}
|
|
|
|
privateList.friendsNode.SetChildren(newChildren)
|
|
}
|
|
|
|
// RemoveChannel removes a channel node if present.
|
|
func (privateList *PrivateChatList) RemoveChannel(channel *discordgo.Channel) {
|
|
newChildren := make([]*tview.TreeNode, 0)
|
|
channelID := channel.ID
|
|
|
|
for _, node := range privateList.chatsNode.GetChildren() {
|
|
referenceChannelID, ok := node.GetReference().(string)
|
|
if !ok || ok && channelID != referenceChannelID {
|
|
newChildren = append(newChildren, node)
|
|
} else {
|
|
delete(privateList.privateChannelStates, node)
|
|
}
|
|
}
|
|
|
|
userID := channel.Recipients[0].ID
|
|
|
|
for _, relationship := range privateList.state.Relationships {
|
|
if relationship.Type == discordgo.RelationTypeFriend &&
|
|
relationship.User.ID == userID {
|
|
privateList.AddOrUpdateFriend(relationship.User)
|
|
break
|
|
}
|
|
}
|
|
|
|
privateList.chatsNode.SetChildren(newChildren)
|
|
}
|
|
|
|
// MarkAsUnread marks the channel as unread, coloring it red.
|
|
func (privateList *PrivateChatList) MarkAsUnread(channelID string) {
|
|
node := tviewutil.GetNodeByReference(channelID, privateList.internalTreeView)
|
|
if node != nil {
|
|
privateList.privateChannelStates[node] = unread
|
|
privateList.setNotificationCount(privateList.amountOfUnreadChannels())
|
|
if tview.IsVtxxx {
|
|
node.SetBlinking(true)
|
|
} else {
|
|
node.SetColor(config.GetTheme().AttentionColor)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (privateList *PrivateChatList) amountOfUnreadChannels() int {
|
|
var amount int
|
|
for _, node := range privateList.chatsNode.GetChildren() {
|
|
if privateList.privateChannelStates[node] == unread {
|
|
amount++
|
|
}
|
|
}
|
|
|
|
return amount
|
|
}
|
|
|
|
// MarkAsRead marks a channel as read.
|
|
func (privateList *PrivateChatList) MarkAsRead(channelID string) {
|
|
node := tviewutil.GetNodeByReference(channelID, privateList.internalTreeView)
|
|
if node != nil {
|
|
privateList.setNotificationCount(privateList.amountOfUnreadChannels())
|
|
privateList.privateChannelStates[node] = read
|
|
if tview.IsVtxxx {
|
|
node.SetBlinking(false)
|
|
node.SetUnderline(false)
|
|
} else {
|
|
node.SetColor(config.GetTheme().PrimaryTextColor)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reorder resorts the list of private chats according to their last
|
|
// message times.
|
|
func (privateList *PrivateChatList) Reorder() {
|
|
children := privateList.chatsNode.GetChildren()
|
|
sort.Slice(children, func(a, b int) bool {
|
|
nodeA := children[a]
|
|
nodeB := children[b]
|
|
|
|
referenceA, ok := nodeA.GetReference().(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
referenceB, ok := nodeB.GetReference().(string)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
channelA, stateError := privateList.state.Channel(referenceA)
|
|
if stateError != nil {
|
|
return false
|
|
}
|
|
channelB, stateError := privateList.state.Channel(referenceB)
|
|
if stateError != nil {
|
|
return true
|
|
}
|
|
|
|
return discordutil.CompareChannels(channelA, channelB)
|
|
})
|
|
}
|
|
|
|
// MarkAsLoaded marks a channel as loaded, coloring it blue. If
|
|
// a different channel had loaded before, it's set to read.
|
|
func (privateList *PrivateChatList) MarkAsLoaded(channelID string) {
|
|
for node, state := range privateList.privateChannelStates {
|
|
if state == loaded {
|
|
privateList.privateChannelStates[node] = read
|
|
if tview.IsVtxxx {
|
|
node.SetBlinking(false)
|
|
node.SetUnderline(false)
|
|
} else {
|
|
node.SetColor(config.GetTheme().PrimaryTextColor)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, node := range privateList.chatsNode.GetChildren() {
|
|
referenceChannelID, ok := node.GetReference().(string)
|
|
if ok && referenceChannelID == channelID {
|
|
privateList.privateChannelStates[node] = loaded
|
|
if tview.IsVtxxx {
|
|
node.SetUnderline(true)
|
|
} else {
|
|
node.SetColor(tview.Styles.ContrastBackgroundColor)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
privateList.setNotificationCount(privateList.amountOfUnreadChannels())
|
|
}
|
|
|
|
// SetOnFriendSelect sets the handler that decides what happens when a friend
|
|
// node gets selected.
|
|
func (privateList *PrivateChatList) SetOnFriendSelect(handler func(userID string)) {
|
|
privateList.onFriendSelect = handler
|
|
}
|
|
|
|
// SetOnChannelSelect sets the handler that decides what happens when a
|
|
// channel node gets selected.
|
|
func (privateList *PrivateChatList) SetOnChannelSelect(handler func(channelID string)) {
|
|
privateList.onChannelSelect = handler
|
|
}
|
|
|
|
// Load loads all present data (chats, groups and friends).
|
|
func (privateList *PrivateChatList) Load() {
|
|
privateChannels := make([]*discordgo.Channel, len(privateList.state.PrivateChannels))
|
|
copy(privateChannels, privateList.state.PrivateChannels)
|
|
discordutil.SortPrivateChannels(privateChannels)
|
|
|
|
for _, channel := range privateChannels {
|
|
privateList.addChannel(channel)
|
|
}
|
|
|
|
FRIEND_LOOP:
|
|
for _, friend := range privateList.state.Relationships {
|
|
if friend.Type != discordgo.RelationTypeFriend {
|
|
continue
|
|
}
|
|
|
|
for _, channel := range privateChannels {
|
|
if channel.Type != discordgo.ChannelTypeDM {
|
|
continue
|
|
}
|
|
|
|
if channel.Recipients[0].ID == friend.ID ||
|
|
(len(channel.Recipients) > 1 && channel.Recipients[1].ID == friend.ID) {
|
|
continue FRIEND_LOOP
|
|
}
|
|
}
|
|
|
|
privateList.addFriend(friend.User)
|
|
}
|
|
|
|
privateList.internalTreeView.SetCurrentNode(privateList.chatsNode)
|
|
privateList.setNotificationCount(privateList.amountOfUnreadChannels())
|
|
}
|
|
|
|
// GetComponent returns the TreeView component that is used.
|
|
// This component is the top-level container of this struct.
|
|
func (privateList *PrivateChatList) GetComponent() *tview.TreeView {
|
|
return privateList.internalTreeView
|
|
}
|
|
|
|
//SetInputCapture delegates to tviews SetInputCapture
|
|
func (privateList *PrivateChatList) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) {
|
|
privateList.internalTreeView.SetInputCapture(capture)
|
|
}
|