From db1424a06d2274f0321991660312445829df13c9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 20 Feb 2020 21:56:03 +0200 Subject: [PATCH] Add support for displaying reactions --- go.mod | 2 +- go.sum | 2 + interface/ui.go | 1 + matrix/matrix.go | 36 ++++++++++++++++- ui/message-view.go | 54 +++++++++++++++----------- ui/messages/base.go | 90 +++++++++++++++++++++++++++++++++++++++++-- ui/messages/parser.go | 1 + ui/room-view.go | 17 ++++++++ 8 files changed, 176 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index cfe36bd..4c56796 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( golang.org/x/net v0.0.0-20200202094626-16171245cfb2 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 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/tcell v1.1.2-0.20200218183045-87c4a25c5b09 ) diff --git a/go.sum b/go.sum index 3091b0c..c6fb4a5 100644 --- a/go.sum +++ b/go.sum @@ -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.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.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/go.mod h1:jwg3Ow7akzsCX3q38pZAfmEC5gGN8gXwMyyjy/yZVMg= maunium.net/go/mauview v0.0.0-20200218231215-04d01c601d5b h1:Bfov5IkJQpkqDexiFioHIZpx4XL7AILDA1GwLVdqtBw= diff --git a/interface/ui.go b/interface/ui.go index e8274e2..abc108f 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -63,6 +63,7 @@ type RoomView interface { AddEvent(evt *event.Event) Message AddRedaction(evt *event.Event) AddEdit(evt *event.Event) + AddReaction(evt *event.Event, key string) GetEvent(eventID string) Message AddServiceMessage(message string) } diff --git a/matrix/matrix.go b/matrix/matrix.go index c75355a..f0009bc 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -297,9 +297,9 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) 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.EventSticker, c.HandleMessage) + c.syncer.OnEventType(mautrix.EventReaction, c.HandleMessage) c.syncer.OnEventType(mautrix.EventRedaction, c.HandleRedaction) c.syncer.OnEventType(mautrix.StateAliases, 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. func (c *Container) HandleMessage(source EventSource, mxEvent *mautrix.Event) { 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 { c.HandleEdit(room, editID, event.Wrap(mxEvent)) 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}) diff --git a/ui/message-view.go b/ui/message-view.go index 9d67c7b..20ee149 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -284,11 +284,16 @@ func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messag view.msgBufferLock.Lock() if new.Height() != end-start { - metaBuffer := view.msgBuffer[0:start] - for i := 0; i < new.Height(); i++ { - metaBuffer = append(metaBuffer, new) + height := new.Height() + + 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 { for i := start; i < end; i++ { view.msgBuffer[i] = new @@ -594,33 +599,38 @@ func (view *MessageView) Draw(screen mauview.Screen) { var prevMsg *messages.UIMessage 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 msg := view.msgBuffer[index] - if msg != prevMsg { - if len(msg.FormatTime()) > 0 { - widget.WriteLineSimpleColor(screen, msg.FormatTime(), 0, line, msg.TimestampColor()) - } - // TODO hiding senders might not be that nice after all, maybe an option? (disabled for now) - //if !bareMode && (prevMsg == nil || meta.Sender() != prevMsg.Sender()) { - widget.WriteLineColor( - screen, mauview.AlignRight, msg.Sender(), - usernameX, line, view.widestSender(), - msg.SenderColor()) - //} - if msg.Edited { - // TODO add better indicator for edits - screen.SetCell(usernameX + view.widestSender(), line, tcell.StyleDefault.Foreground(tcell.ColorDarkRed), '*') - } - prevMsg = msg + if msg == prevMsg { + debug.Print("Unexpected re-encounter of", msg, msg.Height(), "at", line, index) + line++ + continue + } + + if len(msg.FormatTime()) > 0 { + widget.WriteLineSimpleColor(screen, msg.FormatTime(), 0, line, msg.TimestampColor()) + } + // TODO hiding senders might not be that nice after all, maybe an option? (disabled for now) + //if !bareMode && (prevMsg == nil || meta.Sender() != prevMsg.Sender()) { + widget.WriteLineColor( + screen, mauview.AlignRight, msg.Sender(), + usernameX, line, view.widestSender(), + msg.SenderColor()) + //} + if msg.Edited { + // TODO add better indicator for edits + screen.SetCell(usernameX+view.widestSender(), line, tcell.StyleDefault.Foreground(tcell.ColorDarkRed), '*') } for i := index - 1; i >= 0 && view.msgBuffer[i] == msg; i-- { line-- } msg.Draw(mauview.NewProxyScreen(screen, messageX, line, view.width()-messageX, msg.Height())) - line += msg.Height() - 1 + line += msg.Height() + + prevMsg = msg } view.msgBufferLock.RUnlock() } diff --git a/ui/messages/base.go b/ui/messages/base.go index b54691e..4dba9de 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -18,6 +18,8 @@ package messages import ( "fmt" + "sort" + "strings" "time" "maunium.net/go/gomuks/config" @@ -41,6 +43,29 @@ type MessageRenderer interface { 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 { EventID string TxnID string @@ -56,7 +81,10 @@ type UIMessage struct { Edited bool Event *event.Event ReplyTo *UIMessage + Reactions ReactionSlice Renderer MessageRenderer + + reactionBuffer string } const DateFormat = "January _2, 2006" @@ -68,6 +96,15 @@ func newUIMessage(evt *event.Event, displayname string, renderer MessageRenderer 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{ SenderID: evt.Sender, SenderName: displayname, @@ -81,11 +118,31 @@ func newUIMessage(evt *event.Event, displayname string, renderer MessageRenderer IsHighlight: false, IsService: false, Edited: len(evt.Gomuks.Edits) > 0, + Reactions: reactions, Event: evt, 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 { timestamp := time.Now() if unix != 0 { @@ -195,9 +252,16 @@ func (msg *UIMessage) ReplyHeight() int { 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()). 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 { @@ -235,14 +299,25 @@ func (msg *UIMessage) SetIsHighlight(isHighlight bool) { 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) { screen = msg.DrawReply(screen) msg.Renderer.Draw(screen) + msg.DrawReactions(screen) } func (msg *UIMessage) Clone() *UIMessage { clone := *msg clone.ReplyTo = nil + clone.Reactions = nil clone.Renderer = clone.Renderer.Clone() return &clone } @@ -254,9 +329,19 @@ func (msg *UIMessage) CalculateReplyBuffer(preferences config.UserPreferences, w 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) { msg.Renderer.CalculateBuffer(preferences, width, msg) msg.CalculateReplyBuffer(preferences, width) + msg.CalculateReactionBuffer() } func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen { @@ -286,8 +371,7 @@ func (msg *UIMessage) String() string { msg.EventID, msg.TxnID, msg.Type, msg.Timestamp.String(), 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 { diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 53d30e3..c242f47 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -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 { if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil { msg.ReplyTo = replyToMsg + msg.ReplyTo.Reactions = nil } else { // TODO add unrenderable reply header } diff --git a/ui/room-view.go b/ui/room-view.go index 126f1ff..ab31ca7 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -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 { message, ok := view.content.messageIDs[eventID] if !ok {