diff --git a/interface/ui.go b/interface/ui.go index 12aabbe..781f803 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -61,6 +61,7 @@ type RoomView interface { UpdateUserList() ParseEvent(evt *mautrix.Event) Message + GetEvent(eventID string) Message AddMessage(message Message) AddServiceMessage(message string) } diff --git a/ui/message-view.go b/ui/message-view.go index 924eb7a..2211503 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -257,7 +257,7 @@ func (view *MessageView) handleMessageClick(message messages.UIMessage) bool { case *messages.ImageMessage: open.Open(message.Path()) case messages.UIMessage: - debug.Print("Message clicked:", message.NotificationContent()) + debug.Print("Message clicked:", message) } return false } diff --git a/ui/messages/base.go b/ui/messages/base.go index c377ccb..c3ee1f0 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -44,7 +44,6 @@ type BaseMessage struct { MsgSource json.RawMessage ReplyTo UIMessage buffer []tstring.TString - plainBuffer []tstring.TString } func newBaseMessage(event *mautrix.Event, displayname string) BaseMessage { @@ -259,6 +258,12 @@ func (msg *BaseMessage) Draw(screen mauview.Screen) { } } +func (msg *BaseMessage) clone() BaseMessage { + clone := *msg + clone.buffer = nil + return clone +} + func (msg *BaseMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) { if msg.ReplyTo == nil { return diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index 6ed96fc..cf71ba1 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -55,6 +55,14 @@ func NewDateChangeMessage(text string) UIMessage { } } + +func (msg *ExpandedTextMessage) Clone() UIMessage { + return &ExpandedTextMessage{ + BaseMessage: msg.BaseMessage.clone(), + MsgText: msg.MsgText.Clone(), + } +} + func (msg *ExpandedTextMessage) GenerateText() tstring.TString { return msg.MsgText } diff --git a/ui/messages/html/blockquote.go b/ui/messages/html/blockquote.go index 30ce52e..17799a2 100644 --- a/ui/messages/html/blockquote.go +++ b/ui/messages/html/blockquote.go @@ -18,6 +18,7 @@ package html import ( "fmt" + "strings" "maunium.net/go/mauview" ) @@ -37,6 +38,10 @@ func NewBlockquoteEntity(children []Entity) *BlockquoteEntity { }} } +func (be *BlockquoteEntity) Clone() Entity { + return &BlockquoteEntity{BaseEntity: be.BaseEntity.Clone().(*BaseEntity)} +} + func (be *BlockquoteEntity) Draw(screen mauview.Screen) { be.BaseEntity.Draw(screen) for y := 0; y < be.height; y++ { @@ -44,6 +49,33 @@ func (be *BlockquoteEntity) Draw(screen mauview.Screen) { } } +func (be *BlockquoteEntity) PlainText() string { + if len(be.Children) == 0 { + return "" + } + var buf strings.Builder + newlined := false + for i, child := range be.Children { + if i != 0 && child.IsBlock() && !newlined { + buf.WriteRune('\n') + } + newlined = false + for i, row := range strings.Split(child.PlainText(), "\n") { + if i != 0 { + buf.WriteRune('\n') + } + buf.WriteRune('>') + buf.WriteRune(' ') + buf.WriteString(row) + } + if child.IsBlock() { + buf.WriteRune('\n') + newlined = true + } + } + return strings.TrimSpace(buf.String()) +} + func (be *BlockquoteEntity) String() string { return fmt.Sprintf("&html.BlockquoteEntity{%s},\n", be.BaseEntity) } diff --git a/ui/messages/html/break.go b/ui/messages/html/break.go index d400f00..ea67ead 100644 --- a/ui/messages/html/break.go +++ b/ui/messages/html/break.go @@ -26,3 +26,15 @@ func NewBreakEntity() *BreakEntity { Block: true, }} } + +func (be *BreakEntity) Clone() Entity { + return NewBreakEntity() +} + +func (be *BreakEntity) PlainText() string { + return "\n" +} + +func (be *BreakEntity) String() string { + return "&html.BreakEntity{},\n" +} diff --git a/ui/messages/html/codeblock.go b/ui/messages/html/codeblock.go index ec6181d..4a0766c 100644 --- a/ui/messages/html/codeblock.go +++ b/ui/messages/html/codeblock.go @@ -37,6 +37,13 @@ func NewCodeBlockEntity(children []Entity, background tcell.Style) *CodeBlockEnt } } +func (ce *CodeBlockEntity) Clone() Entity { + return &CodeBlockEntity{ + BaseEntity: ce.BaseEntity.Clone().(*BaseEntity), + Background: ce.Background, + } +} + func (ce *CodeBlockEntity) Draw(screen mauview.Screen) { screen.Fill(' ', ce.Background) ce.BaseEntity.Draw(screen) diff --git a/ui/messages/html/entity.go b/ui/messages/html/entity.go index 2ce37a8..be58d61 100644 --- a/ui/messages/html/entity.go +++ b/ui/messages/html/entity.go @@ -165,17 +165,20 @@ func (he *BaseEntity) PlainText() string { buf.WriteString(he.Text) newlined := false for _, child := range he.Children { - if child.IsBlock() && !newlined { + text := child.PlainText() + if !strings.HasPrefix(text, "\n") && child.IsBlock() && !newlined { buf.WriteRune('\n') } newlined = false - buf.WriteString(child.PlainText()) + buf.WriteString(text) if child.IsBlock() { - buf.WriteRune('\n') + if !strings.HasSuffix(text, "\n") { + buf.WriteRune('\n') + } newlined = true } } - return buf.String() + return strings.TrimSpace(buf.String()) } // Draw draws this entity onto the given mauview Screen. diff --git a/ui/messages/html/horizontalline.go b/ui/messages/html/horizontalline.go new file mode 100644 index 0000000..32761aa --- /dev/null +++ b/ui/messages/html/horizontalline.go @@ -0,0 +1,56 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package html + +import ( + "strings" + + "maunium.net/go/mauview" +) + +type HorizontalLineEntity struct { + *BaseEntity +} + +const HorizontalLineChar = '━' + +func NewHorizontalLineEntity() *HorizontalLineEntity { + return &HorizontalLineEntity{&BaseEntity{ + Tag: "hr", + Block: true, + DefaultHeight: 1, + }} +} + +func (he *HorizontalLineEntity) Clone() Entity { + return NewHorizontalLineEntity() +} + +func (he *HorizontalLineEntity) Draw(screen mauview.Screen) { + width, _ := screen.Size() + for x := 0; x < width; x++ { + screen.SetContent(x, 0, HorizontalLineChar, nil, he.Style) + } +} + +func (he *HorizontalLineEntity) PlainText() string { + return strings.Repeat(string(HorizontalLineChar), 5) +} + +func (he *HorizontalLineEntity) String() string { + return "&html.HorizontalLineEntity{},\n" +} diff --git a/ui/messages/html/list.go b/ui/messages/html/list.go index 9f45b92..c611e66 100644 --- a/ui/messages/html/list.go +++ b/ui/messages/html/list.go @@ -56,6 +56,14 @@ func NewListEntity(ordered bool, start int, children []Entity) *ListEntity { return entity } +func (le *ListEntity) Clone() Entity { + return &ListEntity{ + BaseEntity: le.BaseEntity.Clone().(*BaseEntity), + Ordered: le.Ordered, + Start: le.Start, + } +} + func (le *ListEntity) Draw(screen mauview.Screen) { width, _ := screen.Size() @@ -75,6 +83,31 @@ func (le *ListEntity) Draw(screen mauview.Screen) { } } +func (le *ListEntity) PlainText() string { + if len(le.Children) == 0 { + return "" + } + var buf strings.Builder + for i, child := range le.Children { + indent := strings.Repeat(" ", le.Indent) + if le.Ordered { + number := le.Start + i + _, _ = fmt.Fprintf(&buf, "%d. %s", number, strings.Repeat(" ", le.Indent-2-digits(number))) + } else { + buf.WriteString("● ") + } + for j, row := range strings.Split(child.PlainText(), "\n") { + if j != 0 { + buf.WriteRune('\n') + buf.WriteString(indent) + } + buf.WriteString(row) + } + buf.WriteRune('\n') + } + return strings.TrimSpace(buf.String()) +} + func (le *ListEntity) String() string { return fmt.Sprintf("&html.ListEntity{Ordered=%t, Start=%d, Base=%s},\n", le.Ordered, le.Start, le.BaseEntity) } diff --git a/ui/messages/html/parser.go b/ui/messages/html/parser.go index 0bdf483..9e08ab7 100644 --- a/ui/messages/html/parser.go +++ b/ui/messages/html/parser.go @@ -111,7 +111,7 @@ func (parser *htmlParser) basicFormatToEntity(node *html.Node) Entity { entity.AdjustStyle(AdjustStyleBold) case "i", "em": entity.AdjustStyle(AdjustStyleItalic) - case "s", "del": + case "s", "del", "strike": entity.AdjustStyle(AdjustStyleStrikethrough) case "u", "ins": entity.AdjustStyle(AdjustStyleUnderline) @@ -237,7 +237,7 @@ func (parser *htmlParser) syntaxHighlight(text, language string) Entity { children := make([]Entity, len(tokens)) for i, token := range tokens { if token.Value == "\n" { - children[i] = &BaseEntity{Block: true, Tag: "br"} + children[i] = NewBreakEntity() } else { children[i] = &BaseEntity{ Tag: token.Type.String(), @@ -282,7 +282,7 @@ func (parser *htmlParser) tagNodeToEntity(node *html.Node) Entity { return parser.headerToEntity(node) case "br": return NewBreakEntity() - case "b", "strong", "i", "em", "s", "del", "u", "ins", "font": + case "b", "strong", "i", "em", "s", "strike", "del", "u", "ins", "font": return parser.basicFormatToEntity(node) case "a": return parser.linkToEntity(node) @@ -290,6 +290,10 @@ func (parser *htmlParser) tagNodeToEntity(node *html.Node) Entity { return parser.imageToEntity(node) case "pre": return parser.codeblockToEntity(node) + case "hr": + return NewHorizontalLineEntity() + case "mx-reply": + return nil default: return &BaseEntity{ Tag: node.Data, diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index 577a33d..9ee1eab 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -40,6 +40,14 @@ func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) } } +func (hw *HTMLMessage) Clone() UIMessage { + return &HTMLMessage{ + BaseMessage: hw.BaseMessage.clone(), + Root: hw.Root.Clone(), + FocusedBg: hw.FocusedBg, + } +} + func (hw *HTMLMessage) Draw(screen mauview.Screen) { screen = hw.DrawReply(screen) if hw.focused { diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index 9bebc11..01a6500 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -53,6 +53,19 @@ func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayna } } +func (msg *ImageMessage) Clone() UIMessage { + data := make([]byte, len(msg.data)) + copy(data, msg.data) + return &ImageMessage{ + BaseMessage: msg.BaseMessage.clone(), + Body: msg.Body, + Homeserver: msg.Homeserver, + FileID: msg.FileID, + data: data, + matrix: msg.matrix, + } +} + func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) { msg.matrix = matrix diff --git a/ui/messages/message.go b/ui/messages/message.go index 44aaec1..c990368 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -43,6 +43,8 @@ type UIMessage interface { Height() int PlainText() string + Clone() UIMessage + RealSender() string RegisterMatrix(matrix ifc.MatrixContainer) } diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 097c9d0..75e010a 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -31,7 +31,18 @@ import ( "maunium.net/go/gomuks/ui/widget" ) -func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { +func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage { + if roomView := mainView.GetRoom(roomID); roomView != nil { + if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil { + if replyToMsg, ok := replyToIfcMsg.(UIMessage); ok && replyToMsg != nil { + return replyToMsg + } + } + } + return nil +} + +func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *mautrix.Event) UIMessage { msg := directParseEvent(matrix, room, evt) if msg == nil { return nil @@ -41,10 +52,14 @@ func ParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event if len(evt.Content.RelatesTo.InReplyTo.RoomID) > 0 { replyToRoom = matrix.GetRoom(evt.Content.RelatesTo.InReplyTo.RoomID) } - replyToEvt, _ := matrix.GetEvent(replyToRoom, evt.Content.GetReplyTo()) - if replyToEvt != nil { - replyToMsg := directParseEvent(matrix, replyToRoom, replyToEvt) - if replyToMsg != nil { + + if replyToMsg := getCachedEvent(mainView, replyToRoom.ID, evt.Content.GetReplyTo()); replyToMsg != nil { + debug.Print("Cloning cached UIMessage", replyToMsg) + replyToMsg = replyToMsg.Clone() + replyToMsg.SetReplyTo(nil) + msg.SetReplyTo(replyToMsg) + } else if replyToEvt, _ := matrix.GetEvent(replyToRoom, evt.Content.GetReplyTo()); replyToEvt != nil { + if replyToMsg := directParseEvent(matrix, replyToRoom, replyToEvt); replyToMsg != nil { msg.SetReplyTo(replyToMsg) } else { // TODO add unrenderable reply header @@ -68,6 +83,7 @@ func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix case mautrix.StateMember: return ParseMembershipEvent(room, evt) } + return nil } diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 355a90e..f8c4573 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -43,15 +43,22 @@ func NewTextMessage(event *mautrix.Event, displayname string, text string) UIMes func NewServiceMessage(text string) UIMessage { return &TextMessage{ BaseMessage: BaseMessage{ - MsgSenderID: "*", - MsgSender: "*", - MsgTimestamp: time.Now(), - MsgIsService: true, + MsgSenderID: "*", + MsgSender: "*", + MsgTimestamp: time.Now(), + MsgIsService: true, }, MsgText: text, } } +func (msg *TextMessage) Clone() UIMessage { + return &TextMessage{ + BaseMessage: msg.BaseMessage.clone(), + MsgText: msg.MsgText, + } +} + func (msg *TextMessage) getCache() tstring.TString { if msg.cache == nil { switch msg.MsgType { diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index bd6798d..48a6507 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -21,6 +21,7 @@ import ( "unicode" "github.com/mattn/go-runewidth" + "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -29,11 +30,11 @@ import ( type TString []Cell func NewBlankTString() TString { - return make([]Cell, 0) + return make(TString, 0) } func NewTString(str string) TString { - newStr := make([]Cell, len(str)) + newStr := make(TString, len(str)) for i, char := range str { newStr[i] = NewCell(char) } @@ -41,7 +42,7 @@ func NewTString(str string) TString { } func NewColorTString(str string, color tcell.Color) TString { - newStr := make([]Cell, len(str)) + newStr := make(TString, len(str)) for i, char := range str { newStr[i] = NewColorCell(char, color) } @@ -49,7 +50,7 @@ func NewColorTString(str string, color tcell.Color) TString { } func NewStyleTString(str string, style tcell.Style) TString { - newStr := make([]Cell, len(str)) + newStr := make(TString, len(str)) for i, char := range str { newStr[i] = NewStyleCell(char, style) } @@ -74,6 +75,12 @@ func Join(strings []TString, separator string) TString { return out } +func (str TString) Clone() TString { + newStr := make(TString, len(str)) + copy(newStr, str) + return newStr +} + func (str TString) AppendTString(dataList ...TString) TString { newStr := str for _, data := range dataList { diff --git a/ui/room-view.go b/ui/room-view.go index 4402eaa..2bb3e74 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -445,5 +445,13 @@ func (view *RoomView) AddMessage(message ifc.Message) { } func (view *RoomView) ParseEvent(evt *mautrix.Event) ifc.Message { - return messages.ParseEvent(view.parent.matrix, view.Room, evt) + return messages.ParseEvent(view.parent.matrix, view.parent, view.Room, evt) +} + +func (view *RoomView) GetEvent(eventID string) ifc.Message { + message, ok := view.content.messageIDs[eventID] + if !ok { + return nil + } + return message }