refactor(chat): extract channels picker package
This commit is contained in:
parent
c6ca02d8ae
commit
7abc3e4003
9 changed files with 201 additions and 185 deletions
|
|
@ -2,6 +2,7 @@ package chat
|
|||
|
||||
import (
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/help"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
|
|
@ -24,7 +25,7 @@ var _ help.KeyMap = (*attachmentsPicker)(nil)
|
|||
|
||||
func newAttachmentsPicker(cfg *config.Config, chat *Model) *attachmentsPicker {
|
||||
ap := &attachmentsPicker{Model: picker.NewModel(), cfg: cfg, chat: chat}
|
||||
ConfigurePicker(ap.Model, cfg, "Attachments")
|
||||
ui.ConfigurePicker(ap.Model, cfg, "Attachments")
|
||||
return ap
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/help"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
"github.com/ayn2op/tview/picker"
|
||||
)
|
||||
|
||||
type channelsPicker struct {
|
||||
*picker.Model
|
||||
chat *Model
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
var _ help.KeyMap = (*channelsPicker)(nil)
|
||||
|
||||
func newChannelsPicker(cfg *config.Config, chat *Model) *channelsPicker {
|
||||
cp := &channelsPicker{Model: picker.NewModel(), chat: chat, cfg: cfg}
|
||||
ConfigurePicker(cp.Model, cfg, "Channels")
|
||||
return cp
|
||||
}
|
||||
|
||||
func (cp *channelsPicker) Update(msg tview.Msg) tview.Cmd {
|
||||
switch msg := msg.(type) {
|
||||
case picker.SelectedMsg:
|
||||
channelID, ok := msg.Reference.(discord.ChannelID)
|
||||
if !ok || !channelID.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel, err := cp.chat.state.Cabinet.Channel(channelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channel from state", "err", err, "channel_id", channelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
node := cp.chat.guildsTree.findNodeByChannelID(channel.ID)
|
||||
if node == nil {
|
||||
slog.Error("failed to locate channel in tree", "channel_id", channel.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
cp.chat.guildsTree.expandPathToNode(node)
|
||||
cp.chat.guildsTree.SetCurrentNode(node)
|
||||
var selectCmd tview.Cmd
|
||||
if channel.Type != discord.GuildCategory {
|
||||
selectCmd = cp.chat.guildsTree.onSelected(node)
|
||||
}
|
||||
cp.chat.closePicker()
|
||||
return selectCmd
|
||||
case picker.CancelMsg:
|
||||
cp.chat.closePicker()
|
||||
return nil
|
||||
}
|
||||
return cp.Model.Update(msg)
|
||||
}
|
||||
|
||||
func (cp *channelsPicker) update() {
|
||||
var items picker.Items
|
||||
state := cp.chat.state
|
||||
|
||||
privateChannels, err := state.Cabinet.PrivateChannels()
|
||||
if err != nil {
|
||||
slog.Error("failed to get private channels from state", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.SortPrivateChannels(privateChannels)
|
||||
for _, channel := range privateChannels {
|
||||
items = append(items, cp.channelItem(nil, channel))
|
||||
}
|
||||
|
||||
guilds, err := state.Cabinet.Guilds()
|
||||
if err != nil {
|
||||
slog.Error("failed to get guilds from state", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, guild := range guilds {
|
||||
channels, err := state.Cabinet.Channels(guild.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels from state", "err", err, "guild_id", guild.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
items = append(items, cp.channelItem(&guild, channel))
|
||||
}
|
||||
}
|
||||
|
||||
cp.SetItems(items)
|
||||
}
|
||||
|
||||
func (cp *channelsPicker) channelItem(guild *discord.Guild, channel discord.Channel) picker.Item {
|
||||
var b strings.Builder
|
||||
b.WriteString(ui.ChannelToString(channel, cp.cfg.Icons, cp.chat.state))
|
||||
|
||||
if guild != nil {
|
||||
b.WriteString(" - ")
|
||||
b.WriteString(guild.Name)
|
||||
}
|
||||
|
||||
name := b.String()
|
||||
return picker.Item{Text: name, FilterText: name, Reference: channel.ID}
|
||||
}
|
||||
|
||||
func (cp *channelsPicker) ShortHelp() []keybind.Keybind {
|
||||
cfg := cp.cfg.Keybinds.Picker
|
||||
return []keybind.Keybind{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
|
||||
}
|
||||
|
||||
func (cp *channelsPicker) FullHelp() [][]keybind.Keybind {
|
||||
cfg := cp.cfg.Keybinds.Picker
|
||||
return [][]keybind.Keybind{
|
||||
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
|
||||
{cfg.Select.Keybind, cfg.Cancel.Keybind},
|
||||
}
|
||||
}
|
||||
21
internal/ui/chat/channelspicker/keymap.go
Normal file
21
internal/ui/chat/channelspicker/keymap.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package channelspicker
|
||||
|
||||
import (
|
||||
"github.com/ayn2op/tview/help"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
)
|
||||
|
||||
var _ help.KeyMap = (*Model)(nil)
|
||||
|
||||
func (m *Model) ShortHelp() []keybind.Keybind {
|
||||
cfg := m.cfg.Keybinds.Picker
|
||||
return []keybind.Keybind{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
|
||||
}
|
||||
|
||||
func (m *Model) FullHelp() [][]keybind.Keybind {
|
||||
cfg := m.cfg.Keybinds.Picker
|
||||
return [][]keybind.Keybind{
|
||||
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
|
||||
{cfg.Select.Keybind, cfg.Cancel.Keybind},
|
||||
}
|
||||
}
|
||||
91
internal/ui/chat/channelspicker/model.go
Normal file
91
internal/ui/chat/channelspicker/model.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package channelspicker
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/picker"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
*picker.Model
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewModel(cfg *config.Config) *Model {
|
||||
p := picker.NewModel()
|
||||
ui.ConfigurePicker(p, cfg, "Channels")
|
||||
return &Model{
|
||||
Model: p,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
var _ tview.Model = (*Model)(nil)
|
||||
|
||||
func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||
switch msg := msg.(type) {
|
||||
case picker.SelectedMsg:
|
||||
channelID, ok := msg.Reference.(discord.ChannelID)
|
||||
if !ok || !channelID.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return func() tview.Msg { return SelectedMsg{ChannelID: channelID} }
|
||||
case picker.CancelMsg:
|
||||
return func() tview.Msg { return CancelMsg{} }
|
||||
}
|
||||
return m.Model.Update(msg)
|
||||
}
|
||||
|
||||
func (m *Model) RefreshChannels(state *ningen.State) {
|
||||
var items picker.Items
|
||||
|
||||
privateChannels, err := state.Cabinet.PrivateChannels()
|
||||
if err != nil {
|
||||
slog.Error("failed to get private channels from state", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.SortPrivateChannels(privateChannels)
|
||||
for _, channel := range privateChannels {
|
||||
items = append(items, m.channelItem(state, nil, channel))
|
||||
}
|
||||
|
||||
guilds, err := state.Cabinet.Guilds()
|
||||
if err != nil {
|
||||
slog.Error("failed to get guilds from state", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, guild := range guilds {
|
||||
channels, err := state.Cabinet.Channels(guild.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels from state", "err", err, "guild_id", guild.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
items = append(items, m.channelItem(state, &guild, channel))
|
||||
}
|
||||
}
|
||||
|
||||
m.SetItems(items)
|
||||
}
|
||||
|
||||
func (m *Model) channelItem(state *ningen.State, guild *discord.Guild, channel discord.Channel) picker.Item {
|
||||
var b strings.Builder
|
||||
b.WriteString(ui.ChannelToString(channel, m.cfg.Icons, state))
|
||||
|
||||
if guild != nil {
|
||||
b.WriteString(" - ")
|
||||
b.WriteString(guild.Name)
|
||||
}
|
||||
|
||||
name := b.String()
|
||||
return picker.Item{Text: name, FilterText: name, Reference: channel.ID}
|
||||
}
|
||||
9
internal/ui/chat/channelspicker/msg.go
Normal file
9
internal/ui/chat/channelspicker/msg.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package channelspicker
|
||||
|
||||
import "github.com/ayn2op/arikawa/v3/discord"
|
||||
|
||||
type SelectedMsg struct {
|
||||
ChannelID discord.ChannelID
|
||||
}
|
||||
|
||||
type CancelMsg struct{}
|
||||
|
|
@ -872,3 +872,17 @@ func (c *composer) FullHelp() [][]keybind.Keybind {
|
|||
openEditor,
|
||||
}
|
||||
}
|
||||
|
||||
func humanJoin(items []string) string {
|
||||
count := len(items)
|
||||
switch count {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return items[0]
|
||||
case 2:
|
||||
return items[0] + " and " + items[1]
|
||||
default:
|
||||
return strings.Join(items[:count-1], ", ") + ", and " + items[count-1]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
clientgateway "github.com/ayn2op/discordo/internal/gateway"
|
||||
"github.com/ayn2op/discordo/internal/http"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/discordo/internal/ui/chat/channelspicker"
|
||||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/ningen/v3/states/read"
|
||||
"github.com/ayn2op/tview"
|
||||
|
|
@ -48,7 +49,7 @@ type Model struct {
|
|||
guildsTree *guildsTree
|
||||
messagesList *messagesList
|
||||
composer *composer
|
||||
channelsPicker *channelsPicker
|
||||
channelsPicker *channelspicker.Model
|
||||
|
||||
selectedChannel *discord.Channel
|
||||
selectedChannelMu sync.RWMutex
|
||||
|
|
@ -79,7 +80,7 @@ func NewModel(app *tview.Application, cfg *config.Config, token string) *Model {
|
|||
m.guildsTree = newGuildsTree(cfg, m)
|
||||
m.messagesList = newMessagesList(cfg, m)
|
||||
m.composer = newComposer(cfg, m)
|
||||
m.channelsPicker = newChannelsPicker(cfg, m)
|
||||
m.channelsPicker = channelspicker.NewModel(cfg)
|
||||
|
||||
id := gateway.NewIdentifier(gateway.IdentifyCommand{
|
||||
Token: token,
|
||||
|
|
@ -161,7 +162,7 @@ func (m *Model) openPicker() {
|
|||
layers.WithVisible(true),
|
||||
layers.WithOverlay(),
|
||||
).SendToFront(channelsPickerLayerName)
|
||||
m.channelsPicker.update()
|
||||
m.channelsPicker.RefreshChannels(m.state)
|
||||
}
|
||||
|
||||
func (m *Model) closePicker() {
|
||||
|
|
@ -169,6 +170,28 @@ func (m *Model) closePicker() {
|
|||
m.channelsPicker.Refresh()
|
||||
}
|
||||
|
||||
func (m *Model) navigateToChannel(channelID discord.ChannelID) tview.Cmd {
|
||||
channel, err := m.state.Cabinet.Channel(channelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channel from state", "err", err, "channel_id", channelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
node := m.guildsTree.findNodeByChannelID(channel.ID)
|
||||
if node == nil {
|
||||
slog.Error("failed to locate channel in tree", "channel_id", channel.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
m.guildsTree.expandPathToNode(node)
|
||||
m.guildsTree.SetCurrentNode(node)
|
||||
m.closePicker()
|
||||
if channel.Type != discord.GuildCategory {
|
||||
return m.guildsTree.onSelected(node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) toggleGuildsTree() tview.Cmd {
|
||||
// The guilds tree is visible if the number of items is two.
|
||||
if m.mainFlex.GetItemCount() == 2 {
|
||||
|
|
@ -326,6 +349,11 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
return focusCmd
|
||||
case deleteMessageMsg:
|
||||
return m.messagesList.deleteMessageRequest(discord.Message(msg))
|
||||
case channelspicker.SelectedMsg:
|
||||
return m.navigateToChannel(msg.ChannelID)
|
||||
case channelspicker.CancelMsg:
|
||||
m.closePicker()
|
||||
return nil
|
||||
case QuitMsg:
|
||||
return m.closeState()
|
||||
case tview.KeyMsg:
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/list"
|
||||
"github.com/ayn2op/tview/picker"
|
||||
)
|
||||
|
||||
func ConfigurePicker(model *picker.Model, cfg *config.Config, title string) {
|
||||
model.Box = ui.ConfigureBox(tview.NewBox(), &cfg.Theme)
|
||||
// When a child of the parent flex is focused, the parent layout itself is not reported as focused.
|
||||
// Instead, the focused child (picker) is considered focused.
|
||||
// Therefore, we manually set the active border style on the picker to ensure it displays the correct focused appearance.
|
||||
model.
|
||||
SetBlurFunc(nil).
|
||||
SetFocusFunc(nil).
|
||||
SetBorderSet(cfg.Theme.Border.ActiveSet.BorderSet).
|
||||
SetBorderStyle(cfg.Theme.Border.ActiveStyle.Style).
|
||||
SetTitleStyle(cfg.Theme.Title.ActiveStyle.Style).
|
||||
SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style)
|
||||
|
||||
model.SetTitle(title)
|
||||
model.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)
|
||||
model.SetScrollBar(tview.NewScrollBar().
|
||||
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
|
||||
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
|
||||
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
|
||||
model.SetKeybinds(picker.Keybinds{
|
||||
Cancel: cfg.Keybinds.Picker.Cancel.Keybind,
|
||||
Keybinds: list.Keybinds{
|
||||
SelectUp: cfg.Keybinds.Picker.SelectUp.Keybind,
|
||||
SelectDown: cfg.Keybinds.Picker.SelectDown.Keybind,
|
||||
SelectTop: cfg.Keybinds.Picker.SelectTop.Keybind,
|
||||
SelectBottom: cfg.Keybinds.Picker.SelectBottom.Keybind,
|
||||
},
|
||||
Select: cfg.Keybinds.Picker.Select.Keybind,
|
||||
})
|
||||
}
|
||||
|
||||
func humanJoin(items []string) string {
|
||||
count := len(items)
|
||||
switch count {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return items[0]
|
||||
case 2:
|
||||
return items[0] + " and " + items[1]
|
||||
default:
|
||||
return strings.Join(items[:count-1], ", ") + ", and " + items[count-1]
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/grid"
|
||||
"github.com/ayn2op/tview/list"
|
||||
"github.com/ayn2op/tview/picker"
|
||||
)
|
||||
|
||||
// ConfigureBox configures the provided box according to the provided theme.
|
||||
|
|
@ -54,6 +56,37 @@ func ConfigureBox(box *tview.Box, cfg *config.Theme) *tview.Box {
|
|||
return box
|
||||
}
|
||||
|
||||
func ConfigurePicker(model *picker.Model, cfg *config.Config, title string) {
|
||||
model.Box = ConfigureBox(tview.NewBox(), &cfg.Theme)
|
||||
// When a child of the parent flex is focused, the parent layout itself is not reported as focused.
|
||||
// Instead, the focused child (picker) is considered focused.
|
||||
// Therefore, we manually set the active border style on the picker to ensure it displays the correct focused appearance.
|
||||
model.
|
||||
SetBlurFunc(nil).
|
||||
SetFocusFunc(nil).
|
||||
SetBorderSet(cfg.Theme.Border.ActiveSet.BorderSet).
|
||||
SetBorderStyle(cfg.Theme.Border.ActiveStyle.Style).
|
||||
SetTitleStyle(cfg.Theme.Title.ActiveStyle.Style).
|
||||
SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style)
|
||||
|
||||
model.SetTitle(title)
|
||||
model.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)
|
||||
model.SetScrollBar(tview.NewScrollBar().
|
||||
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
|
||||
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
|
||||
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
|
||||
model.SetKeybinds(picker.Keybinds{
|
||||
Cancel: cfg.Keybinds.Picker.Cancel.Keybind,
|
||||
Keybinds: list.Keybinds{
|
||||
SelectUp: cfg.Keybinds.Picker.SelectUp.Keybind,
|
||||
SelectDown: cfg.Keybinds.Picker.SelectDown.Keybind,
|
||||
SelectTop: cfg.Keybinds.Picker.SelectTop.Keybind,
|
||||
SelectBottom: cfg.Keybinds.Picker.SelectBottom.Keybind,
|
||||
},
|
||||
Select: cfg.Keybinds.Picker.Select.Keybind,
|
||||
})
|
||||
}
|
||||
|
||||
// Centered creates a new grid with provided primitive aligned in the center.
|
||||
func Centered(m tview.Model, width, height int) tview.Model {
|
||||
return grid.NewModel().
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue