feat(guildstree): select the previous or next unread guild or channel
Closes #665
This commit is contained in:
parent
0b68476086
commit
f7180f8767
5 changed files with 92 additions and 3 deletions
|
|
@ -175,6 +175,9 @@ select_top = "g"
|
|||
select_bottom = "G"
|
||||
# Select the currently highlighted text-based channel or expand a guild or channel.
|
||||
select_current = "enter"
|
||||
# Highlight the previous or next unread guild or channel, wrapping around.
|
||||
select_previous_unread = "K"
|
||||
select_next_unread = "J"
|
||||
yank_id = "i"
|
||||
collapse_all = "_"
|
||||
collapse_parent_node = "-"
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ type TreeKeybinds struct {
|
|||
MoveToParentNode Keybind `toml:"move_to_parent_node"`
|
||||
}
|
||||
|
||||
type GuildsTreeKeybinds struct {
|
||||
TreeKeybinds
|
||||
SelectPreviousUnread Keybind `toml:"select_previous_unread"`
|
||||
SelectNextUnread Keybind `toml:"select_next_unread"`
|
||||
}
|
||||
|
||||
type MessagesListKeybinds struct {
|
||||
SelectionKeybinds
|
||||
ScrollKeybinds
|
||||
|
|
@ -130,7 +136,7 @@ type Keybinds struct {
|
|||
FocusNext Keybind `toml:"focus_next"`
|
||||
|
||||
Picker PickerKeybinds `toml:"picker"`
|
||||
GuildsTree TreeKeybinds `toml:"guilds_tree"`
|
||||
GuildsTree GuildsTreeKeybinds `toml:"guilds_tree"`
|
||||
MembersTree TreeKeybinds `toml:"members_tree"`
|
||||
MessagesList MessagesListKeybinds `toml:"messages_list"`
|
||||
Composer ComposerKeybinds `toml:"composer"`
|
||||
|
|
@ -234,7 +240,7 @@ func defaultKeybinds() Keybinds {
|
|||
Quit: desc("quit"),
|
||||
|
||||
Picker: defaultPickerKeybinds(),
|
||||
GuildsTree: defaultTreeKeybinds(),
|
||||
GuildsTree: GuildsTreeKeybinds{TreeKeybinds: defaultTreeKeybinds(), SelectPreviousUnread: desc("prev unread"), SelectNextUnread: desc("next unread")},
|
||||
MembersTree: defaultTreeKeybinds(),
|
||||
MessagesList: defaultMessagesListKeybinds(),
|
||||
Composer: defaultComposerKeybinds(),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func (m Model) FullHelp() [][]keybind.Keybind {
|
|||
return [][]keybind.Keybind{
|
||||
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
|
||||
selectGroup,
|
||||
{cfg.SelectPreviousUnread.Keybind, cfg.SelectNextUnread.Keybind},
|
||||
{cfg.YankID.Keybind},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,16 @@ func (m *Model) CurrentNode() *tree.Node {
|
|||
func (m Model) View(focused bool) tview.Element {
|
||||
onChange := func(c tree.Change) tview.Msg { return Msg(c) }
|
||||
onSelect := func(n *tree.Node) tview.Msg { return SelectedMsg{Node: n} }
|
||||
return uitree.New(m.root, m.selectionState, m.cfg, m.cfg.Theme.GuildsTree.CommonTreeTheme, m.cfg.Keybinds.GuildsTree, focused, onChange, onSelect).Title("Guilds")
|
||||
return uitree.New(
|
||||
m.root,
|
||||
m.selectionState,
|
||||
m.cfg,
|
||||
m.cfg.Theme.GuildsTree.CommonTreeTheme,
|
||||
m.cfg.Keybinds.GuildsTree.TreeKeybinds,
|
||||
focused,
|
||||
onChange,
|
||||
onSelect,
|
||||
).Title("Guilds")
|
||||
}
|
||||
|
||||
func (m *Model) reset() *tree.Node {
|
||||
|
|
@ -332,6 +341,20 @@ func (m *Model) update(msg tview.Msg) tview.Cmd {
|
|||
case keybind.Matches(msg, m.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
|
||||
m.collapseParentNode(m.selectionState.CurrentNode())
|
||||
return nil
|
||||
case keybind.Matches(msg, m.cfg.Keybinds.GuildsTree.SelectPreviousUnread.Keybind, m.cfg.Keybinds.GuildsTree.SelectNextUnread.Keybind):
|
||||
previous := keybind.Matches(msg, m.cfg.Keybinds.GuildsTree.SelectPreviousUnread.Keybind)
|
||||
if node := adjacentNode(m.root, m.selectionState.CurrentNode(), m.isUnread, previous); node != nil {
|
||||
// A guild or the direct messages without channels yet: load them and move on to the unread one.
|
||||
if _, ok := node.Reference().(discord.ChannelID); !ok {
|
||||
m.selectNode(node)
|
||||
if channel := adjacentNode(node, node, m.isUnread, previous); channel != nil {
|
||||
node = channel
|
||||
}
|
||||
}
|
||||
m.expandPathToNode(node)
|
||||
m.selectionState.SetCurrentNode(node)
|
||||
}
|
||||
return nil
|
||||
case keybind.Matches(msg, m.cfg.Keybinds.GuildsTree.YankID.Keybind):
|
||||
return uitree.YankID(m.selectionState.CurrentNode())
|
||||
}
|
||||
|
|
@ -339,6 +362,45 @@ func (m *Model) update(msg tview.Msg) tview.Cmd {
|
|||
return nil
|
||||
}
|
||||
|
||||
// isUnread reports whether node is an unread channel, or a guild or the direct messages whose channels are not shown yet and has one.
|
||||
func (m *Model) isUnread(node *tree.Node) bool {
|
||||
if len(node.Children()) > 0 {
|
||||
return false
|
||||
}
|
||||
opts := ningen.UnreadOpts{IncludeMutedCategories: true}
|
||||
switch ref := node.Reference().(type) {
|
||||
case discord.GuildID:
|
||||
return m.state.GuildIsUnread(ref, ningen.GuildUnreadOpts{UnreadOpts: opts}) != ningen.ChannelRead
|
||||
case discord.ChannelID:
|
||||
return m.state.ChannelIsUnread(ref, opts) != ningen.ChannelRead
|
||||
case dmNode:
|
||||
channels, _ := m.state.PrivateChannels()
|
||||
return slices.ContainsFunc(channels, func(c discord.Channel) bool { return m.state.ChannelIsUnread(c.ID, opts) != ningen.ChannelRead })
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// adjacentNode returns the nearest node under root matching match after current, or before it if previous, wrapping around, or nil if no other node matches.
|
||||
func adjacentNode(root, current *tree.Node, match func(*tree.Node) bool, previous bool) *tree.Node {
|
||||
var nodes []*tree.Node
|
||||
after := 0
|
||||
root.Walk(func(node, _ *tree.Node) bool {
|
||||
if node == current {
|
||||
after = len(nodes)
|
||||
} else if match(node) {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
if previous {
|
||||
after += len(nodes) - 1
|
||||
}
|
||||
return nodes[after%len(nodes)]
|
||||
}
|
||||
|
||||
func (m *Model) findNodeByReference(reference any) *tree.Node {
|
||||
switch ref := reference.(type) {
|
||||
case discord.GuildID:
|
||||
|
|
|
|||
|
|
@ -52,3 +52,20 @@ func TestModelMoveDMToFront(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdjacentNode(t *testing.T) {
|
||||
a, b, c := tree.NewNode("a"), tree.NewNode("b"), tree.NewNode("c")
|
||||
root := tree.NewNode("").SetChildren([]*tree.Node{a, tree.NewNode("guild").AddChild(b), c})
|
||||
match := func(n *tree.Node) bool { return n == a || n == b }
|
||||
for current, want := range map[*tree.Node][2]*tree.Node{nil: {a, b}, a: {b, b}, b: {a, a}, c: {a, b}} {
|
||||
if got := adjacentNode(root, current, match, false); got != want[0] {
|
||||
t.Errorf("after %v = %v, want %v", current, got, want[0])
|
||||
}
|
||||
if got := adjacentNode(root, current, match, true); got != want[1] {
|
||||
t.Errorf("before %v = %v, want %v", current, got, want[1])
|
||||
}
|
||||
}
|
||||
if got := adjacentNode(root, a, func(*tree.Node) bool { return false }, false); got != nil {
|
||||
t.Errorf("got %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue