Add reply to threads functionality

This commit is contained in:
erroneousboat 2019-02-16 22:45:36 +01:00
parent 56cad01ce0
commit 946a8ca086
6 changed files with 152 additions and 32 deletions

View File

@ -182,7 +182,10 @@ func (c *Chat) AddMessage(message Message) {
c.Messages[message.ID] = message c.Messages[message.ID] = message
} }
// AddReply adds a single reply to a parent thread, it also sets
// the thread separator
func (c *Chat) AddReply(parentID string, message Message) { func (c *Chat) AddReply(parentID string, message Message) {
message.Thread = " "
c.Messages[parentID].Messages[message.ID] = message c.Messages[parentID].Messages[message.ID] = message
} }
@ -267,6 +270,12 @@ func (c *Chat) MessageToCells(msg Message) []termui.Cell {
termui.ColorDefault, termui.ColorDefault)..., termui.ColorDefault, termui.ColorDefault)...,
) )
// Thread
cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetThread(),
termui.ColorDefault, termui.ColorDefault)...,
)
// Name // Name
cells = append(cells, termui.DefaultTxBuilder.Build( cells = append(cells, termui.DefaultTxBuilder.Build(
msg.GetName(), msg.GetName(),

View File

@ -25,12 +25,14 @@ type Message struct {
Messages map[string]Message Messages map[string]Message
Time time.Time Time time.Time
Thread string
Name string Name string
Content string Content string
StyleTime string StyleTime string
StyleName string StyleThread string
StyleText string StyleName string
StyleText string
FormatTime string FormatTime string
} }
@ -43,6 +45,13 @@ func (m Message) GetTime() string {
) )
} }
func (m Message) GetThread() string {
return fmt.Sprintf("[%s](%s)",
m.Thread,
m.StyleThread,
)
}
func (m Message) GetName() string { func (m Message) GetName() string {
return fmt.Sprintf("[<%s>](%s) ", return fmt.Sprintf("[<%s>](%s) ",
m.Name, m.Name,

View File

@ -128,6 +128,7 @@ func getDefaultConfig() Config {
Message: Message{ Message: Message{
Time: "", Time: "",
TimeFormat: "15:04", TimeFormat: "15:04",
Thread: "fg-bold",
Name: "", Name: "",
Text: "", Text: "",
}, },

View File

@ -18,6 +18,7 @@ type View struct {
type Message struct { type Message struct {
Time string `json:"time"` Time string `json:"time"`
Name string `json:"name"` Name string `json:"name"`
Thread string `json:"thread"`
Text string `json:"text"` Text string `json:"text"`
TimeFormat string `json:"time_format"` TimeFormat string `json:"time_format"`
} }

View File

@ -121,6 +121,8 @@ func messageHandler(ctx *context.AppContext) {
// Add message to the selected channel // Add message to the selected channel
if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID { if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID {
// When timestamp isn't set this is a thread reply,
// handle as such
if ev.ThreadTimestamp != "" { if ev.ThreadTimestamp != "" {
ctx.View.Chat.AddReply(ev.ThreadTimestamp, msg) ctx.View.Chat.AddReply(ev.ThreadTimestamp, msg)
} else { } else {
@ -241,8 +243,8 @@ func actionSend(ctx *context.AppContext) {
ctx.View.Input.Clear() ctx.View.Input.Clear()
ctx.View.Refresh() ctx.View.Refresh()
// Send message // Send slash command
err := ctx.Service.SendMessage( isCmd, err := ctx.Service.SendCommand(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID, ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message, message,
) )
@ -252,6 +254,19 @@ 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(),
)
}
}
// Clear notification icon if there is any // Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel] channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
if channelItem.Notification { if channelItem.Notification {

View File

@ -23,6 +23,7 @@ type SlackService struct {
RTM *slack.RTM RTM *slack.RTM
Conversations []slack.Channel Conversations []slack.Channel
UserCache map[string]string UserCache map[string]string
ThreadCache map[string]string
CurrentUserID string CurrentUserID string
CurrentUsername string CurrentUsername string
} }
@ -31,9 +32,10 @@ type SlackService struct {
// the RTM and a Client // the RTM and a Client
func NewSlackService(config *config.Config) (*SlackService, error) { func NewSlackService(config *config.Config) (*SlackService, error) {
svc := &SlackService{ svc := &SlackService{
Config: config, Config: config,
Client: slack.New(config.SlackToken), Client: slack.New(config.SlackToken),
UserCache: make(map[string]string), UserCache: make(map[string]string),
ThreadCache: make(map[string]string),
} }
// Get user associated with token, mainly // Get user associated with token, mainly
@ -276,6 +278,66 @@ func (s *SlackService) SendMessage(channelID string, message string) error {
return nil return nil
} }
// SendReply will send a message to a particular thread, specifying the
// ThreadTimestamp will make it reply to that specific thread. (see:
// https://api.slack.com/docs/message-threading, 'Posting replies')
func (s *SlackService) SendReply(channelID string, threadID string, message string) error {
// https://godoc.org/github.com/nlopes/slack#PostMessageParameters
postParams := slack.PostMessageParameters{
AsUser: true,
Username: s.CurrentUsername,
LinkNames: 1,
ThreadTimestamp: threadID,
}
// https://godoc.org/github.com/nlopes/slack#Client.PostMessage
_, _, err := s.Client.PostMessage(channelID, message, postParams)
if err != nil {
return err
}
return nil
}
// SendCommand will send a specific command to slack. First we check
// wether we are dealing with a command, and if it is one of the supported
// ones.
func (s *SlackService) SendCommand(channelID string, message string) (bool, error) {
// First check if it begins with slash and a command
r, err := regexp.Compile(`^/\w+`)
if err != nil {
return false, err
}
match := r.MatchString(message)
if !match {
return false, nil
}
// Execute the the command when supported
switch r.FindString(message) {
case "/reply":
r := regexp.MustCompile(`(?P<cmd>^/\w+) (?P<id>\w+) (?P<msg>.*)`)
subMatch := r.FindStringSubmatch(message)
if len(subMatch) < 4 {
return false, errors.New("'/reply' command malformed")
}
threadID := s.ThreadCache[subMatch[2]]
msg := subMatch[3]
err := s.SendReply(channelID, threadID, msg)
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// GetMessages will get messages for a channel, group or im channel delimited // GetMessages will get messages for a channel, group or im channel delimited
// by a count. // by a count.
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) { func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) {
@ -355,15 +417,16 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
// Format message // Format message
msg := components.Message{ msg := components.Message{
ID: message.Timestamp, ID: message.Timestamp,
Messages: make(map[string]components.Message), Messages: make(map[string]components.Message),
Time: time.Unix(intTime, 0), Time: time.Unix(intTime, 0),
Name: name, Name: name,
Content: parseMessage(s, message.Text), Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time, StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name, StyleThread: s.Config.Theme.Message.Thread,
StyleText: s.Config.Theme.Message.Text, StyleName: s.Config.Theme.Message.Name,
FormatTime: s.Config.Theme.Message.TimeFormat, StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
} }
// When there are attachments, add them to Messages // When there are attachments, add them to Messages
@ -381,7 +444,23 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
// When the message timestamp and thread timestamp are the same, we // When the message timestamp and thread timestamp are the same, we
// have a parent message. This means it contains a thread with replies. // have a parent message. This means it contains a thread with replies.
//
// Additionally, we set the thread timestamp in the s.ThreadCache with
// the hexadecimal representation of the timestamp. We do this because
// we if we want to reply to a thread, we need to reference this
// timestamp. Which is too long to type, we shorten it and remember the
// reference in the cache.
if message.ThreadTimestamp != "" && message.ThreadTimestamp == message.Timestamp { if message.ThreadTimestamp != "" && message.ThreadTimestamp == message.Timestamp {
// Set the thread identifier for thread cache
f, _ := strconv.ParseFloat(message.ThreadTimestamp, 64)
threadID := fmt.Sprintf("thread_%x", int(f))
s.ThreadCache[threadID] = message.ThreadTimestamp
// Set thread prefix for message
msg.Thread = fmt.Sprintf("%s ", threadID)
// Create the message replies from the thread
replies := s.CreateMessageFromReplies(message, channelID) replies := s.CreateMessageFromReplies(message, channelID)
for _, reply := range replies { for _, reply := range replies {
msg.Messages[reply.ID] = reply msg.Messages[reply.ID] = reply
@ -435,7 +514,6 @@ func (s *SlackService) CreateMessageFromReplies(message slack.Message, channelID
var replies []components.Message var replies []components.Message
for _, reply := range msgs { for _, reply := range msgs {
// Because the conversations api returns an entire thread (a // Because the conversations api returns an entire thread (a
// message plus all the messages in reply), we need to check if // message plus all the messages in reply), we need to check if
// one of the replies isn't the parent that we started with. // one of the replies isn't the parent that we started with.
@ -447,6 +525,10 @@ func (s *SlackService) CreateMessageFromReplies(message slack.Message, channelID
} }
msg := s.CreateMessage(reply, channelID) msg := s.CreateMessage(reply, channelID)
// Set the thread separator
msg.Thread = " "
replies = append(replies, msg) replies = append(replies, msg)
} }
@ -554,10 +636,11 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
field.Title, field.Title,
field.Value, field.Value,
), ),
StyleTime: s.Config.Theme.Message.Time, StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name, StyleThread: s.Config.Theme.Message.Thread,
StyleText: s.Config.Theme.Message.Text, StyleName: s.Config.Theme.Message.Name,
FormatTime: s.Config.Theme.Message.TimeFormat, StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
}, },
) )
} }
@ -566,11 +649,12 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
msgs = append( msgs = append(
msgs, msgs,
components.Message{ components.Message{
Content: fmt.Sprintf("%s", att.Text), Content: fmt.Sprintf("%s", att.Text),
StyleTime: s.Config.Theme.Message.Time, StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name, StyleThread: s.Config.Theme.Message.Thread,
StyleText: s.Config.Theme.Message.Text, StyleName: s.Config.Theme.Message.Name,
FormatTime: s.Config.Theme.Message.TimeFormat, StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
}, },
) )
} }
@ -579,11 +663,12 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
msgs = append( msgs = append(
msgs, msgs,
components.Message{ components.Message{
Content: fmt.Sprintf("%s", att.Title), Content: fmt.Sprintf("%s", att.Title),
StyleTime: s.Config.Theme.Message.Time, StyleTime: s.Config.Theme.Message.Time,
StyleName: s.Config.Theme.Message.Name, StyleThread: s.Config.Theme.Message.Thread,
StyleText: s.Config.Theme.Message.Text, StyleName: s.Config.Theme.Message.Name,
FormatTime: s.Config.Theme.Message.TimeFormat, StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
}, },
) )
} }