Merge branch 'threads-v2'
* threads-v2: Fix cursor position in threads pane Fix actionChangeThread to render Threads Make first channel item in thread selected channel Only redraw grid when threads are present Fix columns Remove debugging statements Add reply to thread functionality Make columns render correctly Test with loading replies in chat window Fix visible cursor Start with different threads setup
This commit is contained in:
commit
40ca615ffc
@ -108,13 +108,13 @@ type Channels struct {
|
||||
}
|
||||
|
||||
// CreateChannels is the constructor for the Channels component
|
||||
func CreateChannelsComponent(inputHeight int) *Channels {
|
||||
func CreateChannelsComponent(height int) *Channels {
|
||||
channels := &Channels{
|
||||
List: termui.NewList(),
|
||||
}
|
||||
|
||||
channels.List.BorderLabel = "Channels"
|
||||
channels.List.Height = termui.TermHeight() - inputHeight
|
||||
channels.List.Height = height
|
||||
|
||||
channels.SelectedChannel = 0
|
||||
channels.Offset = 0
|
||||
@ -148,17 +148,16 @@ func (c *Channels) Buffer() termui.Buffer {
|
||||
// Append ellipsis when overflows
|
||||
cells = termui.DTrimTxCls(cells, c.List.InnerWidth())
|
||||
|
||||
x := 0
|
||||
x := c.List.InnerBounds().Min.X
|
||||
for _, cell := range cells {
|
||||
width := cell.Width()
|
||||
buf.Set(c.List.InnerBounds().Min.X+x, y, cell)
|
||||
x += width
|
||||
buf.Set(x, y, cell)
|
||||
x += cell.Width()
|
||||
}
|
||||
|
||||
// When not at the end of the pane fill it up empty characters
|
||||
for x < c.List.InnerBounds().Max.X-1 {
|
||||
for x < c.List.InnerBounds().Max.X {
|
||||
if y == c.CursorPosition {
|
||||
buf.Set(x+1, y,
|
||||
buf.Set(x, y,
|
||||
termui.Cell{
|
||||
Ch: ' ',
|
||||
Fg: c.List.ItemBgColor,
|
||||
@ -167,7 +166,7 @@ func (c *Channels) Buffer() termui.Buffer {
|
||||
)
|
||||
} else {
|
||||
buf.Set(
|
||||
x+1, y,
|
||||
x, y,
|
||||
termui.Cell{
|
||||
Ch: ' ',
|
||||
Fg: c.List.ItemFgColor,
|
||||
@ -236,6 +235,11 @@ func (c *Channels) SetSelectedChannel(index int) {
|
||||
c.SelectedChannel = index
|
||||
}
|
||||
|
||||
// Get SelectedChannel returns the ChannelItem that is currently selected
|
||||
func (c *Channels) GetSelectedChannel() ChannelItem {
|
||||
return c.ChannelItems[c.SelectedChannel]
|
||||
}
|
||||
|
||||
// MoveCursorUp will decrease the SelectedChannel by 1
|
||||
func (c *Channels) MoveCursorUp() {
|
||||
if c.SelectedChannel > 0 {
|
||||
|
26
components/threads.go
Normal file
26
components/threads.go
Normal file
@ -0,0 +1,26 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"github.com/erroneousboat/termui"
|
||||
)
|
||||
|
||||
type Threads struct {
|
||||
*Channels
|
||||
}
|
||||
|
||||
func CreateThreadsComponent(height int) *Threads {
|
||||
threads := &Threads{
|
||||
Channels: &Channels{
|
||||
List: termui.NewList(),
|
||||
},
|
||||
}
|
||||
|
||||
threads.List.BorderLabel = "Threads"
|
||||
threads.List.Height = height
|
||||
|
||||
threads.SelectedChannel = 0
|
||||
threads.Offset = 0
|
||||
threads.CursorPosition = threads.List.InnerBounds().Min.Y
|
||||
|
||||
return threads
|
||||
}
|
@ -22,6 +22,7 @@ type Config struct {
|
||||
Emoji bool `json:"emoji"`
|
||||
SidebarWidth int `json:"sidebar_width"`
|
||||
MainWidth int `json:"-"`
|
||||
ThreadsWidth int `json:"threads_width"`
|
||||
KeyMap map[string]keyMapping `json:"key_map"`
|
||||
Theme Theme `json:"theme"`
|
||||
}
|
||||
@ -90,6 +91,7 @@ func getDefaultConfig() Config {
|
||||
return Config{
|
||||
SidebarWidth: 1,
|
||||
MainWidth: 11,
|
||||
ThreadsWidth: 1,
|
||||
Notify: "",
|
||||
Emoji: false,
|
||||
KeyMap: map[string]keyMapping{
|
||||
@ -100,6 +102,8 @@ func getDefaultConfig() Config {
|
||||
"j": "channel-down",
|
||||
"g": "channel-top",
|
||||
"G": "channel-bottom",
|
||||
"K": "thread-up",
|
||||
"J": "thread-down",
|
||||
"<previous>": "chat-up",
|
||||
"C-b": "chat-up",
|
||||
"C-u": "chat-up",
|
||||
|
@ -19,6 +19,9 @@ const (
|
||||
CommandMode = "command"
|
||||
InsertMode = "insert"
|
||||
SearchMode = "search"
|
||||
|
||||
ChatFocus = iota
|
||||
ThreadFocus
|
||||
)
|
||||
|
||||
type AppContext struct {
|
||||
@ -31,6 +34,7 @@ type AppContext struct {
|
||||
Config *config.Config
|
||||
Debug bool
|
||||
Mode string
|
||||
Focus int
|
||||
Notify *notificator.Notificator
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,8 @@ var actionMap = map[string]func(*context.AppContext){
|
||||
"channel-search-next": actionSearchNextChannels,
|
||||
"channel-search-prev": actionSearchPrevChannels,
|
||||
"channel-jump": actionJumpChannels,
|
||||
"thread-up": actionMoveCursorUpThreads,
|
||||
"thread-down": actionMoveCursorDownThreads,
|
||||
"chat-up": actionScrollUpChat,
|
||||
"chat-down": actionScrollDownChat,
|
||||
"help": actionHelp,
|
||||
@ -205,6 +207,65 @@ func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
|
||||
termui.Render(termui.Body)
|
||||
}
|
||||
|
||||
func actionRedrawGrid(ctx *context.AppContext, threads bool, debug bool) {
|
||||
termui.Clear()
|
||||
termui.Body = termui.NewGrid()
|
||||
termui.Body.X = 0
|
||||
termui.Body.Y = 0
|
||||
termui.Body.BgColor = termui.ThemeAttr("bg")
|
||||
termui.Body.Width = termui.TermWidth()
|
||||
|
||||
columns := []*termui.Row{
|
||||
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
|
||||
}
|
||||
|
||||
if threads && debug {
|
||||
columns = append(
|
||||
columns,
|
||||
[]*termui.Row{
|
||||
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth-3, 0, ctx.View.Chat),
|
||||
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
|
||||
termui.NewCol(3, 0, ctx.View.Debug),
|
||||
}...,
|
||||
)
|
||||
|
||||
} else if threads {
|
||||
columns = append(
|
||||
columns,
|
||||
[]*termui.Row{
|
||||
termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth, 0, ctx.View.Chat),
|
||||
termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
|
||||
}...,
|
||||
)
|
||||
} else if debug {
|
||||
columns = append(
|
||||
columns,
|
||||
[]*termui.Row{
|
||||
termui.NewCol(ctx.Config.MainWidth-5, 0, ctx.View.Chat),
|
||||
termui.NewCol(ctx.Config.MainWidth-6, 0, ctx.View.Debug),
|
||||
}...,
|
||||
)
|
||||
} else {
|
||||
columns = append(
|
||||
columns,
|
||||
[]*termui.Row{
|
||||
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
|
||||
}...,
|
||||
)
|
||||
}
|
||||
|
||||
termui.Body.AddRows(
|
||||
termui.NewRow(columns...),
|
||||
termui.NewRow(
|
||||
termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Mode),
|
||||
termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Input),
|
||||
),
|
||||
)
|
||||
|
||||
termui.Body.Align()
|
||||
termui.Render(termui.Body)
|
||||
}
|
||||
|
||||
func actionInput(view *views.View, key rune) {
|
||||
view.Input.Insert(key)
|
||||
termui.Render(view.Input)
|
||||
@ -265,14 +326,30 @@ func actionSend(ctx *context.AppContext) {
|
||||
|
||||
// Send message
|
||||
if !isCmd {
|
||||
err := ctx.Service.SendMessage(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
message,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.View.Debug.Println(
|
||||
err.Error(),
|
||||
if ctx.Focus == context.ChatFocus {
|
||||
err := ctx.Service.SendMessage(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
message,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.View.Debug.Println(
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ctx.Focus == context.ThreadFocus {
|
||||
err := ctx.Service.SendReply(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
|
||||
message,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.View.Debug.Println(
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,7 +409,7 @@ func actionSearchMode(ctx *context.AppContext) {
|
||||
}
|
||||
|
||||
func actionGetMessages(ctx *context.AppContext) {
|
||||
msgs, err := ctx.Service.GetMessages(
|
||||
msgs, _, err := ctx.Service.GetMessages(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
ctx.View.Chat.GetMaxItems(),
|
||||
)
|
||||
@ -418,7 +495,7 @@ func actionChangeChannel(ctx *context.AppContext) {
|
||||
|
||||
// Get messages of the SelectedChannel, and get the count of messages
|
||||
// that fit into the Chat component
|
||||
msgs, err := ctx.Service.GetMessages(
|
||||
msgs, threads, err := ctx.Service.GetMessages(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
ctx.View.Chat.GetMaxItems(),
|
||||
)
|
||||
@ -431,6 +508,23 @@ func actionChangeChannel(ctx *context.AppContext) {
|
||||
// Set messages for the channel
|
||||
ctx.View.Chat.SetMessages(msgs)
|
||||
|
||||
// Set the threads identifiers in the threads pane
|
||||
var haveThreads bool
|
||||
if len(threads) > 0 {
|
||||
haveThreads = true
|
||||
|
||||
// Make the first thread the current Channel
|
||||
ctx.View.Threads.SetChannels(
|
||||
append(
|
||||
[]components.ChannelItem{ctx.View.Channels.GetSelectedChannel()},
|
||||
threads...,
|
||||
),
|
||||
)
|
||||
|
||||
// Reset position of SelectedChannel
|
||||
ctx.View.Threads.MoveCursorTop()
|
||||
}
|
||||
|
||||
// Set channel name for the Chat pane
|
||||
ctx.View.Chat.SetBorderLabel(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].GetChannelName(),
|
||||
@ -443,10 +537,102 @@ func actionChangeChannel(ctx *context.AppContext) {
|
||||
ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
|
||||
}
|
||||
|
||||
// Redraw grid, necessary when threads and/or debug is set. We will redraw
|
||||
// the grid when there are threads, or we just came from a thread and went
|
||||
// to a channel without threads. Hence the clearing of ChannelItems of
|
||||
// Threads.
|
||||
if haveThreads {
|
||||
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
|
||||
} else if !haveThreads && len(ctx.View.Threads.ChannelItems) > 0 {
|
||||
ctx.View.Threads.SetChannels([]components.ChannelItem{})
|
||||
actionRedrawGrid(ctx, haveThreads, ctx.Debug)
|
||||
} else {
|
||||
termui.Render(ctx.View.Channels)
|
||||
termui.Render(ctx.View.Chat)
|
||||
}
|
||||
|
||||
// Set focus, necessary to know when replying to thread or chat
|
||||
ctx.Focus = context.ChatFocus
|
||||
}
|
||||
|
||||
func actionChangeThread(ctx *context.AppContext) {
|
||||
// Clear messages from Chat pane
|
||||
ctx.View.Chat.ClearMessages()
|
||||
|
||||
// The first channel in the Thread list is current Channel. Set context
|
||||
// Focus and messages accordingly.
|
||||
var err error
|
||||
msgs := []components.Message{}
|
||||
if ctx.View.Threads.SelectedChannel == 0 {
|
||||
ctx.Focus = context.ChatFocus
|
||||
|
||||
msgs, _, err = ctx.Service.GetMessages(
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
ctx.View.Chat.GetMaxItems(),
|
||||
)
|
||||
if err != nil {
|
||||
termbox.Close()
|
||||
log.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
} else {
|
||||
ctx.Focus = context.ThreadFocus
|
||||
|
||||
msgs, err = ctx.Service.GetMessageByID(
|
||||
ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
|
||||
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
|
||||
)
|
||||
if err != nil {
|
||||
termbox.Close()
|
||||
log.Println(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Set messages for the channel
|
||||
ctx.View.Chat.SetMessages(msgs)
|
||||
|
||||
// Set focus, necessary to know when replying to thread or chat
|
||||
|
||||
termui.Render(ctx.View.Channels)
|
||||
termui.Render(ctx.View.Threads)
|
||||
termui.Render(ctx.View.Chat)
|
||||
}
|
||||
|
||||
func actionMoveCursorUpThreads(ctx *context.AppContext) {
|
||||
go func() {
|
||||
if scrollTimer != nil {
|
||||
scrollTimer.Stop()
|
||||
}
|
||||
|
||||
ctx.View.Threads.MoveCursorUp()
|
||||
termui.Render(ctx.View.Threads)
|
||||
|
||||
scrollTimer = time.NewTimer(time.Second / 4)
|
||||
<-scrollTimer.C
|
||||
|
||||
// Only actually change channel when the timer expires
|
||||
actionChangeThread(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func actionMoveCursorDownThreads(ctx *context.AppContext) {
|
||||
go func() {
|
||||
if scrollTimer != nil {
|
||||
scrollTimer.Stop()
|
||||
}
|
||||
|
||||
ctx.View.Threads.MoveCursorDown()
|
||||
termui.Render(ctx.View.Threads)
|
||||
|
||||
scrollTimer = time.NewTimer(time.Second / 4)
|
||||
<-scrollTimer.C
|
||||
|
||||
// Only actually change thread when the timer expires
|
||||
actionChangeThread(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
// actionNewMessage will set the new message indicator for a channel, and
|
||||
// if configured will also display a desktop notification
|
||||
func actionNewMessage(ctx *context.AppContext, ev *slack.MessageEvent) {
|
||||
|
@ -406,8 +406,9 @@ func (s *SlackService) SendCommand(channelID string, message string) (bool, erro
|
||||
}
|
||||
|
||||
// GetMessages will get messages for a channel, group or im channel delimited
|
||||
// by a count.
|
||||
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) {
|
||||
// by a count. It will return the messages, the thread identifiers
|
||||
// (as ChannelItem), and and error.
|
||||
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, []components.ChannelItem, error) {
|
||||
|
||||
// https://godoc.org/github.com/nlopes/slack#GetConversationHistoryParameters
|
||||
historyParams := slack.GetConversationHistoryParameters{
|
||||
@ -418,14 +419,27 @@ func (s *SlackService) GetMessages(channelID string, count int) ([]components.Me
|
||||
|
||||
history, err := s.Client.GetConversationHistory(&historyParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Construct the messages
|
||||
var messages []components.Message
|
||||
var threads []components.ChannelItem
|
||||
for _, message := range history.Messages {
|
||||
msg := s.CreateMessage(message, channelID)
|
||||
messages = append(messages, msg)
|
||||
|
||||
// FIXME: create boolean isThread
|
||||
if msg.Thread != "" {
|
||||
threads = append(threads, components.ChannelItem{
|
||||
ID: msg.ID,
|
||||
Name: msg.Thread,
|
||||
Type: components.ChannelTypeGroup,
|
||||
StylePrefix: s.Config.Theme.Channel.Prefix,
|
||||
StyleIcon: s.Config.Theme.Channel.Icon,
|
||||
StyleText: s.Config.Theme.Channel.Text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the order of the messages, we want the newest in
|
||||
@ -435,7 +449,38 @@ func (s *SlackService) GetMessages(channelID string, count int) ([]components.Me
|
||||
messagesReversed = append(messagesReversed, messages[i])
|
||||
}
|
||||
|
||||
return messagesReversed, nil
|
||||
return messagesReversed, threads, nil
|
||||
}
|
||||
|
||||
// CreateMessageByID will construct an array of components.Message with only
|
||||
// 1 message, using the message ID (Timestamp).
|
||||
//
|
||||
// For the choice of history parameters see:
|
||||
// https://api.slack.com/messaging/retrieving
|
||||
func (s *SlackService) GetMessageByID(messageID string, channelID string) ([]components.Message, error) {
|
||||
|
||||
var msgs []components.Message
|
||||
|
||||
// https://godoc.org/github.com/nlopes/slack#GetConversationHistoryParameters
|
||||
historyParams := slack.GetConversationHistoryParameters{
|
||||
ChannelID: channelID,
|
||||
Limit: 1,
|
||||
Inclusive: true,
|
||||
Latest: messageID,
|
||||
}
|
||||
|
||||
history, err := s.Client.GetConversationHistory(&historyParams)
|
||||
if err != nil {
|
||||
return msgs, err
|
||||
}
|
||||
|
||||
// We break because we're only asking for 1 message
|
||||
for _, message := range history.Messages {
|
||||
msgs = append(msgs, s.CreateMessage(message, channelID))
|
||||
break
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// CreateMessage will create a string formatted message that can be rendered
|
||||
@ -545,7 +590,7 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
|
||||
msg.Thread = fmt.Sprintf("%s ", threadID)
|
||||
|
||||
// Create the message replies from the thread
|
||||
replies := s.CreateMessageFromReplies(message, channelID)
|
||||
replies := s.CreateMessageFromReplies(message.ThreadTimestamp, channelID)
|
||||
for _, reply := range replies {
|
||||
msg.Messages[reply.ID] = reply
|
||||
}
|
||||
@ -563,13 +608,13 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
|
||||
// https://api.slack.com/methods/conversations.replies
|
||||
// https://godoc.org/github.com/nlopes/slack#Client.GetConversationReplies
|
||||
// https://godoc.org/github.com/nlopes/slack#GetConversationRepliesParameters
|
||||
func (s *SlackService) CreateMessageFromReplies(message slack.Message, channelID string) []components.Message {
|
||||
func (s *SlackService) CreateMessageFromReplies(messageID string, channelID string) []components.Message {
|
||||
msgs := make([]slack.Message, 0)
|
||||
|
||||
initReplies, _, initCur, err := s.Client.GetConversationReplies(
|
||||
&slack.GetConversationRepliesParameters{
|
||||
ChannelID: channelID,
|
||||
Timestamp: message.ThreadTimestamp,
|
||||
Timestamp: messageID,
|
||||
Limit: 200,
|
||||
},
|
||||
)
|
||||
@ -583,7 +628,7 @@ func (s *SlackService) CreateMessageFromReplies(message slack.Message, channelID
|
||||
for nextCur != "" {
|
||||
conversationReplies, _, cursor, err := s.Client.GetConversationReplies(&slack.GetConversationRepliesParameters{
|
||||
ChannelID: channelID,
|
||||
Timestamp: message.ThreadTimestamp,
|
||||
Timestamp: messageID,
|
||||
Cursor: nextCur,
|
||||
Limit: 200,
|
||||
})
|
||||
|
@ -13,6 +13,7 @@ type View struct {
|
||||
Input *components.Input
|
||||
Chat *components.Chat
|
||||
Channels *components.Channels
|
||||
Threads *components.Threads
|
||||
Mode *components.Mode
|
||||
Debug *components.Debug
|
||||
}
|
||||
@ -22,7 +23,8 @@ func CreateView(config *config.Config, svc *service.SlackService) (*View, error)
|
||||
input := components.CreateInputComponent()
|
||||
|
||||
// Channels: create the component
|
||||
channels := components.CreateChannelsComponent(input.Par.Height)
|
||||
sideBarHeight := termui.TermHeight() - input.Par.Height
|
||||
channels := components.CreateChannelsComponent(sideBarHeight)
|
||||
|
||||
// Channels: fill the component
|
||||
slackChans, err := svc.GetChannels()
|
||||
@ -33,11 +35,14 @@ func CreateView(config *config.Config, svc *service.SlackService) (*View, error)
|
||||
// Channels: set channels in component
|
||||
channels.SetChannels(slackChans)
|
||||
|
||||
// Threads: create component
|
||||
threads := components.CreateThreadsComponent(sideBarHeight)
|
||||
|
||||
// Chat: create the component
|
||||
chat := components.CreateChatComponent(input.Par.Height)
|
||||
|
||||
// Chat: fill the component
|
||||
msgs, err := svc.GetMessages(
|
||||
msgs, thr, err := svc.GetMessages(
|
||||
channels.ChannelItems[channels.SelectedChannel].ID,
|
||||
chat.GetMaxItems(),
|
||||
)
|
||||
@ -52,6 +57,9 @@ func CreateView(config *config.Config, svc *service.SlackService) (*View, error)
|
||||
channels.ChannelItems[channels.SelectedChannel].GetChannelName(),
|
||||
)
|
||||
|
||||
// Threads: set threads in component
|
||||
threads.SetChannels(thr)
|
||||
|
||||
// Debug: create the component
|
||||
debug := components.CreateDebugComponent(input.Par.Height)
|
||||
|
||||
@ -62,6 +70,7 @@ func CreateView(config *config.Config, svc *service.SlackService) (*View, error)
|
||||
Config: config,
|
||||
Input: input,
|
||||
Channels: channels,
|
||||
Threads: threads,
|
||||
Chat: chat,
|
||||
Mode: mode,
|
||||
Debug: debug,
|
||||
@ -75,6 +84,7 @@ func (v *View) Refresh() {
|
||||
v.Input,
|
||||
v.Chat,
|
||||
v.Channels,
|
||||
v.Threads,
|
||||
v.Mode,
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user