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

View File

@ -25,10 +25,12 @@ type Message struct {
Messages map[string]Message
Time time.Time
Thread string
Name string
Content string
StyleTime string
StyleThread string
StyleName string
StyleText 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 {
return fmt.Sprintf("[<%s>](%s) ",
m.Name,

View File

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

View File

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

View File

@ -121,6 +121,8 @@ func messageHandler(ctx *context.AppContext) {
// Add message to the selected channel
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 != "" {
ctx.View.Chat.AddReply(ev.ThreadTimestamp, msg)
} else {
@ -241,7 +243,19 @@ func actionSend(ctx *context.AppContext) {
ctx.View.Input.Clear()
ctx.View.Refresh()
// Send slash command
isCmd, err := ctx.Service.SendCommand(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
)
if err != nil {
ctx.View.Debug.Println(
err.Error(),
)
}
// Send message
if !isCmd {
err := ctx.Service.SendMessage(
ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
message,
@ -251,6 +265,7 @@ func actionSend(ctx *context.AppContext) {
err.Error(),
)
}
}
// Clear notification icon if there is any
channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]

View File

@ -23,6 +23,7 @@ type SlackService struct {
RTM *slack.RTM
Conversations []slack.Channel
UserCache map[string]string
ThreadCache map[string]string
CurrentUserID string
CurrentUsername string
}
@ -34,6 +35,7 @@ func NewSlackService(config *config.Config) (*SlackService, error) {
Config: config,
Client: slack.New(config.SlackToken),
UserCache: make(map[string]string),
ThreadCache: make(map[string]string),
}
// Get user associated with token, mainly
@ -276,6 +278,66 @@ func (s *SlackService) SendMessage(channelID string, message string) error {
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
// by a count.
func (s *SlackService) GetMessages(channelID string, count int) ([]components.Message, error) {
@ -361,6 +423,7 @@ func (s *SlackService) CreateMessage(message slack.Message, channelID string) co
Name: name,
Content: parseMessage(s, message.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
@ -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
// 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 {
// 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)
for _, reply := range replies {
msg.Messages[reply.ID] = reply
@ -435,7 +514,6 @@ func (s *SlackService) CreateMessageFromReplies(message slack.Message, channelID
var replies []components.Message
for _, reply := range msgs {
// Because the conversations api returns an entire thread (a
// message plus all the messages in reply), we need to check if
// 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)
// Set the thread separator
msg.Thread = " "
replies = append(replies, msg)
}
@ -555,6 +637,7 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
field.Value,
),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
@ -568,6 +651,7 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
components.Message{
Content: fmt.Sprintf("%s", att.Text),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,
@ -581,6 +665,7 @@ func (s *SlackService) CreateMessageFromAttachments(atts []slack.Attachment) []c
components.Message{
Content: fmt.Sprintf("%s", att.Title),
StyleTime: s.Config.Theme.Message.Time,
StyleThread: s.Config.Theme.Message.Thread,
StyleName: s.Config.Theme.Message.Name,
StyleText: s.Config.Theme.Message.Text,
FormatTime: s.Config.Theme.Message.TimeFormat,