Add support for displaying reactions

This commit is contained in:
Tulir Asokan 2020-02-20 21:56:03 +02:00
parent db7d2da50c
commit db1424a06d
8 changed files with 176 additions and 27 deletions

2
go.mod
View File

@ -19,7 +19,7 @@ require (
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 golang.org/x/net v0.0.0-20200202094626-16171245cfb2
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219230859-de66c34ea5bc maunium.net/go/mautrix v0.1.0-alpha.3.0.20200220001222-8dc3dd5d538d
maunium.net/go/mauview v0.0.0-20200219222453-b984e20438e6 maunium.net/go/mauview v0.0.0-20200219222453-b984e20438e6
maunium.net/go/tcell v1.1.2-0.20200218183045-87c4a25c5b09 maunium.net/go/tcell v1.1.2-0.20200218183045-87c4a25c5b09
) )

2
go.sum
View File

@ -80,6 +80,8 @@ maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219223957-21a588b7e623 h1:StXwGEEdQ
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219223957-21a588b7e623/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4= maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219223957-21a588b7e623/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219230859-de66c34ea5bc h1:1iMzqdMF4z9Rp3CDfDa+gjuRHGfb9cZjZZR9Y68nPuo= maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219230859-de66c34ea5bc h1:1iMzqdMF4z9Rp3CDfDa+gjuRHGfb9cZjZZR9Y68nPuo=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219230859-de66c34ea5bc/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4= maunium.net/go/mautrix v0.1.0-alpha.3.0.20200219230859-de66c34ea5bc/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200220001222-8dc3dd5d538d h1:doAHXnYCIgNy4qAgReZRRb3EaWR4D0w+Zs2y959Z8Uk=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20200220001222-8dc3dd5d538d/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4=
maunium.net/go/mauview v0.0.0-20200218183549-88ecb1321176 h1:KoTm7ASEzFIZ1SvPWuWYzpkeA+wiR1fuUu4l7TCHcE0= maunium.net/go/mauview v0.0.0-20200218183549-88ecb1321176 h1:KoTm7ASEzFIZ1SvPWuWYzpkeA+wiR1fuUu4l7TCHcE0=
maunium.net/go/mauview v0.0.0-20200218183549-88ecb1321176/go.mod h1:jwg3Ow7akzsCX3q38pZAfmEC5gGN8gXwMyyjy/yZVMg= maunium.net/go/mauview v0.0.0-20200218183549-88ecb1321176/go.mod h1:jwg3Ow7akzsCX3q38pZAfmEC5gGN8gXwMyyjy/yZVMg=
maunium.net/go/mauview v0.0.0-20200218231215-04d01c601d5b h1:Bfov5IkJQpkqDexiFioHIZpx4XL7AILDA1GwLVdqtBw= maunium.net/go/mauview v0.0.0-20200218231215-04d01c601d5b h1:Bfov5IkJQpkqDexiFioHIZpx4XL7AILDA1GwLVdqtBw=

View File

@ -63,6 +63,7 @@ type RoomView interface {
AddEvent(evt *event.Event) Message AddEvent(evt *event.Event) Message
AddRedaction(evt *event.Event) AddRedaction(evt *event.Event)
AddEdit(evt *event.Event) AddEdit(evt *event.Event)
AddReaction(evt *event.Event, key string)
GetEvent(eventID string) Message GetEvent(eventID string) Message
AddServiceMessage(message string) AddServiceMessage(message string)
} }

View File

@ -297,9 +297,9 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer") debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config) c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage)
// Just pass encrypted events as messages, they'll show up with an encryption unsupported message.
c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage) c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage) c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage)
c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction) c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction)
c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage) c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage)
@ -453,6 +453,37 @@ func (c *Container) HandleEdit(room *rooms.Room, editsID string, editEvent *even
} }
} }
func (c *Container) HandleReaction(room *rooms.Room, reactsTo string, reactEvent *event.Event) {
rel := reactEvent.Content.GetRelatesTo()
var origEvt *event.Event
err := c.history.Update(room, reactsTo, func(evt *event.Event) error {
if evt.Unsigned.Relations.Annotations.Map == nil {
evt.Unsigned.Relations.Annotations.Map = make(map[string]int)
}
val, _ := evt.Unsigned.Relations.Annotations.Map[rel.Key]
evt.Unsigned.Relations.Annotations.Map[rel.Key] = val + 1
origEvt = evt
return nil
})
if err != nil {
debug.Print("Failed to store reaction in history db:", err)
return
} else if !c.config.AuthCache.InitialSyncDone || !room.Loaded() {
return
}
roomView := c.ui.MainView().GetRoom(reactEvent.RoomID)
if roomView == nil {
debug.Printf("Failed to handle edit event %v: No room view found.", reactEvent)
return
}
roomView.AddReaction(origEvt, rel.Key)
if c.syncer.FirstSyncDone {
c.ui.Render()
}
}
// HandleMessage is the event handler for the m.room.message timeline event. // HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) {
room := c.GetOrCreateRoom(mxEvent.RoomID) room := c.GetOrCreateRoom(mxEvent.RoomID)
@ -466,6 +497,9 @@ func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) {
if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 { if editID := mxEvent.Content.GetRelatesTo().GetReplaceID(); len(editID) > 0 {
c.HandleEdit(room, editID, event.Wrap(mxEvent)) c.HandleEdit(room, editID, event.Wrap(mxEvent))
return return
} else if reactionID := mxEvent.Content.GetRelatesTo().GetAnnotationID(); mxEvent.Type == mautrix.EventReaction && len(reactionID) > 0 {
c.HandleReaction(room, reactionID, event.Wrap(mxEvent))
return
} }
events, err := c.history.Append(room, []*mautrix.Event{mxEvent}) events, err := c.history.Append(room, []*mautrix.Event{mxEvent})

View File

@ -284,11 +284,16 @@ func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messag
view.msgBufferLock.Lock() view.msgBufferLock.Lock()
if new.Height() != end-start { if new.Height() != end-start {
metaBuffer := view.msgBuffer[0:start] height := new.Height()
for i := 0; i < new.Height(); i++ {
metaBuffer = append(metaBuffer, new) newBuffer := make([]*messages.UIMessage, height + len(view.msgBuffer) - end)
for i := 0; i < height; i++ {
newBuffer[i] = new
} }
view.msgBuffer = append(metaBuffer, view.msgBuffer[end:]...) for i := height; i < len(newBuffer); i++ {
newBuffer[i] = view.msgBuffer[end + (i - height)]
}
view.msgBuffer = append(view.msgBuffer[0:start], newBuffer...)
} else { } else {
for i := start; i < end; i++ { for i := start; i < end; i++ {
view.msgBuffer[i] = new view.msgBuffer[i] = new
@ -594,11 +599,16 @@ func (view *MessageView) Draw(screen mauview.Screen) {
var prevMsg *messages.UIMessage var prevMsg *messages.UIMessage
view.msgBufferLock.RLock() view.msgBufferLock.RLock()
for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); line++ { for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); {
index := indexOffset + line index := indexOffset + line
msg := view.msgBuffer[index] msg := view.msgBuffer[index]
if msg != prevMsg { if msg == prevMsg {
debug.Print("Unexpected re-encounter of", msg, msg.Height(), "at", line, index)
line++
continue
}
if len(msg.FormatTime()) > 0 { if len(msg.FormatTime()) > 0 {
widget.WriteLineSimpleColor(screen, msg.FormatTime(), 0, line, msg.TimestampColor()) widget.WriteLineSimpleColor(screen, msg.FormatTime(), 0, line, msg.TimestampColor())
} }
@ -613,14 +623,14 @@ func (view *MessageView) Draw(screen mauview.Screen) {
// TODO add better indicator for edits // TODO add better indicator for edits
screen.SetCell(usernameX+view.widestSender(), line, tcell.StyleDefault.Foreground(tcell.ColorDarkRed), '*') screen.SetCell(usernameX+view.widestSender(), line, tcell.StyleDefault.Foreground(tcell.ColorDarkRed), '*')
} }
prevMsg = msg
}
for i := index - 1; i >= 0 && view.msgBuffer[i] == msg; i-- { for i := index - 1; i >= 0 && view.msgBuffer[i] == msg; i-- {
line-- line--
} }
msg.Draw(mauview.NewProxyScreen(screen, messageX, line, view.width()-messageX, msg.Height())) msg.Draw(mauview.NewProxyScreen(screen, messageX, line, view.width()-messageX, msg.Height()))
line += msg.Height() - 1 line += msg.Height()
prevMsg = msg
} }
view.msgBufferLock.RUnlock() view.msgBufferLock.RUnlock()
} }

View File

@ -18,6 +18,8 @@ package messages
import ( import (
"fmt" "fmt"
"sort"
"strings"
"time" "time"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -41,6 +43,29 @@ type MessageRenderer interface {
String() string String() string
} }
type ReactionItem struct {
Key string
Count int
}
func (ri ReactionItem) String() string {
return fmt.Sprintf("%d %s", ri.Count, ri.Key)
}
type ReactionSlice []ReactionItem
func (rs ReactionSlice) Len() int {
return len(rs)
}
func (rs ReactionSlice) Less(i, j int) bool {
return rs[i].Key < rs[j].Key
}
func (rs ReactionSlice) Swap(i, j int) {
rs[i], rs[j] = rs[j], rs[i]
}
type UIMessage struct { type UIMessage struct {
EventID string EventID string
TxnID string TxnID string
@ -56,7 +81,10 @@ type UIMessage struct {
Edited bool Edited bool
Event *event.Event Event *event.Event
ReplyTo *UIMessage ReplyTo *UIMessage
Reactions ReactionSlice
Renderer MessageRenderer Renderer MessageRenderer
reactionBuffer string
} }
const DateFormat = "January _2, 2006" const DateFormat = "January _2, 2006"
@ -68,6 +96,15 @@ func newUIMessage(evt *event.Event, displayname string, renderer MessageRenderer
msgtype = mautrix.MessageType(evt.Type.String()) msgtype = mautrix.MessageType(evt.Type.String())
} }
reactions := make(ReactionSlice, 0, len(evt.Unsigned.Relations.Annotations.Map))
for key, count := range evt.Unsigned.Relations.Annotations.Map {
reactions = append(reactions, ReactionItem{
Key: key,
Count: count,
})
}
sort.Sort(reactions)
return &UIMessage{ return &UIMessage{
SenderID: evt.Sender, SenderID: evt.Sender,
SenderName: displayname, SenderName: displayname,
@ -81,11 +118,31 @@ func newUIMessage(evt *event.Event, displayname string, renderer MessageRenderer
IsHighlight: false, IsHighlight: false,
IsService: false, IsService: false,
Edited: len(evt.Gomuks.Edits) > 0, Edited: len(evt.Gomuks.Edits) > 0,
Reactions: reactions,
Event: evt, Event: evt,
Renderer: renderer, Renderer: renderer,
} }
} }
func (msg *UIMessage) AddReaction(key string) {
found := false
for _, rs := range msg.Reactions {
if rs.Key == key {
rs.Count++
found = true
break
}
}
if !found {
msg.Reactions = append(msg.Reactions, ReactionItem{
Key: key,
Count: 1,
})
}
sort.Sort(msg.Reactions)
msg.CalculateReactionBuffer()
}
func unixToTime(unix int64) time.Time { func unixToTime(unix int64) time.Time {
timestamp := time.Now() timestamp := time.Now()
if unix != 0 { if unix != 0 {
@ -195,9 +252,16 @@ func (msg *UIMessage) ReplyHeight() int {
return 0 return 0
} }
func (msg *UIMessage) ReactionHeight() int {
if len(msg.Reactions) > 0 {
return 1
}
return 0
}
// Height returns the number of rows in the computed buffer (see Buffer()). // Height returns the number of rows in the computed buffer (see Buffer()).
func (msg *UIMessage) Height() int { func (msg *UIMessage) Height() int {
return msg.ReplyHeight() + msg.Renderer.Height() return msg.ReplyHeight() + msg.Renderer.Height() + msg.ReactionHeight()
} }
func (msg *UIMessage) Time() time.Time { func (msg *UIMessage) Time() time.Time {
@ -235,14 +299,25 @@ func (msg *UIMessage) SetIsHighlight(isHighlight bool) {
msg.IsHighlight = isHighlight msg.IsHighlight = isHighlight
} }
func (msg *UIMessage) DrawReactions(screen mauview.Screen) {
if len(msg.Reactions) == 0 {
return
}
width, height := screen.Size()
screen = mauview.NewProxyScreen(screen, 0, height-1, width, 1)
mauview.Print(screen, msg.reactionBuffer, 0, 0, width, mauview.AlignLeft, mauview.Styles.PrimaryTextColor)
}
func (msg *UIMessage) Draw(screen mauview.Screen) { func (msg *UIMessage) Draw(screen mauview.Screen) {
screen = msg.DrawReply(screen) screen = msg.DrawReply(screen)
msg.Renderer.Draw(screen) msg.Renderer.Draw(screen)
msg.DrawReactions(screen)
} }
func (msg *UIMessage) Clone() *UIMessage { func (msg *UIMessage) Clone() *UIMessage {
clone := *msg clone := *msg
clone.ReplyTo = nil clone.ReplyTo = nil
clone.Reactions = nil
clone.Renderer = clone.Renderer.Clone() clone.Renderer = clone.Renderer.Clone()
return &clone return &clone
} }
@ -254,9 +329,19 @@ func (msg *UIMessage) CalculateReplyBuffer(preferences config.UserPreferences, w
msg.ReplyTo.CalculateBuffer(preferences, width-1) msg.ReplyTo.CalculateBuffer(preferences, width-1)
} }
func (msg *UIMessage) CalculateReactionBuffer() {
var text strings.Builder
for _, reaction := range msg.Reactions {
text.WriteString(reaction.String())
text.WriteRune(' ')
}
msg.reactionBuffer = text.String()
}
func (msg *UIMessage) CalculateBuffer(preferences config.UserPreferences, width int) { func (msg *UIMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
msg.Renderer.CalculateBuffer(preferences, width, msg) msg.Renderer.CalculateBuffer(preferences, width, msg)
msg.CalculateReplyBuffer(preferences, width) msg.CalculateReplyBuffer(preferences, width)
msg.CalculateReactionBuffer()
} }
func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen { func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen {
@ -286,8 +371,7 @@ func (msg *UIMessage) String() string {
msg.EventID, msg.TxnID, msg.EventID, msg.TxnID,
msg.Type, msg.Timestamp.String(), msg.Type, msg.Timestamp.String(),
msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(), msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(),
msg.IsService, msg.IsHighlight, msg.Renderer.String(), msg.IsService, msg.IsHighlight, msg.Renderer.String())
)
} }
func (msg *UIMessage) PlainText() string { func (msg *UIMessage) PlainText() string {

View File

@ -54,6 +54,7 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R
} else if replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()); replyToEvt != nil { } else if replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()); replyToEvt != nil {
if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil { if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil {
msg.ReplyTo = replyToMsg msg.ReplyTo = replyToMsg
msg.ReplyTo.Reactions = nil
} else { } else {
// TODO add unrenderable reply header // TODO add unrenderable reply header
} }

View File

@ -558,6 +558,23 @@ func (view *RoomView) AddEdit(evt *event.Event) {
} }
} }
func (view *RoomView) AddReaction(evt *event.Event, key string) {
msgView := view.MessageView()
msg := msgView.getMessageByID(evt.ID)
if msg == nil {
// Message not in view, nothing to do
return
}
recalculate := len(msg.Reactions) == 0
msg.AddReaction(key)
if recalculate {
debug.Print(msg.ReactionHeight(), msg.Height())
// Recalculate height for message
msg.CalculateBuffer(msgView.prevPrefs, msgView.prevWidth())
msgView.replaceBuffer(msg, msg)
}
}
func (view *RoomView) GetEvent(eventID string) ifc.Message { func (view *RoomView) GetEvent(eventID string) ifc.Message {
message, ok := view.content.messageIDs[eventID] message, ok := view.content.messageIDs[eventID]
if !ok { if !ok {