Add reply to threads functionality
This commit is contained in:
parent
56cad01ce0
commit
946a8ca086
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
@ -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: "",
|
||||||
},
|
},
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
139
service/slack.go
139
service/slack.go
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user