diff --git a/README.md b/README.md index d1ae854..f3bcedf 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ func Foo() { * `/notice ` - Send a notice (generally used for bot messages). * `/rainbow ` - Send rainbow text (markdown not supported). * `/rainbowme ` - Send rainbow text in an emote. +* `/reply [text]` - Reply to the selected message. If text is not specified, the next message will be used. +* `/react ` - React to the selected message. +* `/redact` - Redact the selected message. #### Rooms ##### Creating diff --git a/go.mod b/go.mod index b6257e6..a23bf00 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.20200221234248-8a868bce2854 + maunium.net/go/mautrix v0.1.0-alpha.3.0.20200229222711-57beb97fdccb maunium.net/go/mauview v0.0.0-20200220222850-39f1414676d9 maunium.net/go/tcell v1.1.2-0.20200218183045-87c4a25c5b09 ) diff --git a/go.sum b/go.sum index be4ef13..5f0b1e8 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ maunium.net/go/mautrix v0.1.0-alpha.3.0.20200221220303-b441ba9359cf h1:ojEsISqRL maunium.net/go/mautrix v0.1.0-alpha.3.0.20200221220303-b441ba9359cf/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4= maunium.net/go/mautrix v0.1.0-alpha.3.0.20200221234248-8a868bce2854 h1:NKj52IqLkfscNMSbSVepaXHM6M/x3iNe/iWSAcmAilo= maunium.net/go/mautrix v0.1.0-alpha.3.0.20200221234248-8a868bce2854/go.mod h1:g10T1fh2Q2HkJWycVs93eBXdWpqD67f1YVQhNxdIDr4= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20200229222711-57beb97fdccb h1:b9cMiQ85HtNhV/gItnjdEFC7nQTWmvnALiHQiYqrJAU= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20200229222711-57beb97fdccb/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/matrix.go b/interface/matrix.go index 0b1278d..34e8dda 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -23,6 +23,11 @@ import ( "maunium.net/go/gomuks/matrix/rooms" ) +type Relation struct { + Type mautrix.RelationType + Event *event.Event +} + type MatrixContainer interface { Client() *mautrix.Client InitClient() error @@ -35,7 +40,7 @@ type MatrixContainer interface { Logout() SendPreferencesToMatrix() - PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string, edit *event.Event) *event.Event + PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, message string, relation *Relation) *event.Event SendEvent(evt *event.Event) (string, error) SendTyping(roomID string, typing bool) MarkRead(roomID, eventID string) diff --git a/matrix/matrix.go b/matrix/matrix.go index 85c4a46..940a519 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -732,7 +732,7 @@ func (c *Container) MarkRead(roomID, eventID string) { var mentionRegex = regexp.MustCompile("\\[(.+?)]\\(https://matrix.to/#/@.+?:.+?\\)") var roomRegex = regexp.MustCompile("\\[.+?]\\(https://matrix.to/#/(#.+?:[^/]+?)\\)") -func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string, edit *event.Event) *event.Event { +func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string, rel *ifc.Relation) *event.Event { content := format.RenderMarkdown(text) content.MsgType = msgtype @@ -740,7 +740,7 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag content.Body = mentionRegex.ReplaceAllString(content.Body, "$1") content.Body = roomRegex.ReplaceAllString(content.Body, "$1") - if edit != nil { + if rel != nil && rel.Type == mautrix.RelReplace { contentCopy := content content.NewContent = &contentCopy content.Body = "* " + content.Body @@ -749,8 +749,10 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag } content.RelatesTo = &mautrix.RelatesTo{ Type: mautrix.RelReplace, - EventID: edit.ID, + EventID: rel.Event.ID, } + } else if rel != nil && rel.Type == mautrix.RelReference { + content.SetReply(rel.Event.Event) } txnID := c.client.TxnID() @@ -766,8 +768,8 @@ func (c *Container) PrepareMarkdownMessage(roomID string, msgtype mautrix.Messag }, }) localEcho.Gomuks.OutgoingState = event.StateLocalEcho - if edit != nil { - localEcho.ID = edit.ID + if rel != nil && rel.Type == mautrix.RelReplace { + localEcho.ID = rel.Event.ID localEcho.Gomuks.Edits = []*event.Event{localEcho} } return localEcho diff --git a/ui/command-processor.go b/ui/command-processor.go index c4dabbd..3212dae 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -87,6 +87,11 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "createroom": {"create"}, "dm": {"pm"}, "query": {"pm"}, + "r": {"reply"}, + "delete": {"redact"}, + "remove": {"redact"}, + "rm": {"redact"}, + "del": {"redact"}, }, commands: map[string]CommandHandler{ "unknown-command": cmdUnknownCommand, @@ -107,6 +112,9 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "logout": cmdLogout, "accept": cmdAccept, "reject": cmdReject, + "reply": cmdReply, + "redact": cmdRedact, + "react": cmdReact, "sendevent": cmdSendEvent, "msendevent": cmdMSendEvent, "setstate": cmdSetState, diff --git a/ui/commands.go b/ui/commands.go index 0f07e94..c7f0301 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -41,7 +41,6 @@ import ( func cmdMe(cmd *Command) { text := strings.Join(cmd.Args, " ") go cmd.Room.SendMessage(mautrix.MsgEmote, text) - cmd.UI.Render() } // GradientTable from https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go @@ -87,10 +86,9 @@ func makeRainbow(cmd *Command, msgtype mautrix.MessageType) { continue } color := rainbow.GetInterpolatedColorFor(float64(i) / float64(len(text))).Hex() - _, _ = fmt.Fprintf(&html, "%c", color, char) + _, _ = fmt.Fprintf(&html, "%[2]c", color, char) } go cmd.Room.SendMessage(msgtype, html.String()) - cmd.UI.Render() } func cmdRainbow(cmd *Command) { @@ -103,7 +101,6 @@ func cmdRainbowMe(cmd *Command) { func cmdNotice(cmd *Command) { go cmd.Room.SendMessage(mautrix.MsgNotice, strings.Join(cmd.Args, " ")) - cmd.UI.Render() } func cmdAccept(cmd *Command) { @@ -141,6 +138,42 @@ func cmdID(cmd *Command) { cmd.Reply("The internal ID of this room is %s", cmd.Room.MxRoom().ID) } +type SelectReason string + +const ( + SelectReply SelectReason = "reply to" + SelectReact = "react to" + SelectRedact = "redact" +) + +func cmdReply(cmd *Command) { + cmd.Room.selecting = true + cmd.Room.selectReason = SelectReply + cmd.Room.selectContent = strings.Join(cmd.Args, " ") + cmd.Room.OnSelect(cmd.Room.MessageView().selected) +} + +func cmdRedact(cmd *Command) { + cmd.Reply("Not yet implemented 3:") + + // This needs to be implemented in RoomView's OnSelect method + //cmd.Room.selecting = true + //cmd.Room.selectReason = SelectRedact + //cmd.Room.OnSelect(cmd.Room.MessageView().selected) +} + +func cmdReact(cmd *Command) { + if len(cmd.Args) == 0 { + cmd.Reply("Usage: /react ") + return + } + + cmd.Room.selecting = true + cmd.Room.selectReason = SelectReact + cmd.Room.selectContent = strings.Join(cmd.Args, " ") + cmd.Room.OnSelect(cmd.Room.MessageView().selected) +} + func cmdTags(cmd *Command) { tags := cmd.Room.MxRoom().RawTags if len(cmd.Args) > 0 && cmd.Args[0] == "--internal" { @@ -298,6 +331,9 @@ Things: rooms, users, baremessages, images, typingnotif /notice - Send a notice (generally used for bot messages). /rainbow - Send rainbow text (markdown not supported). /rainbowme - Send rainbow text in an emote. +/reply [text] - Reply to the selected message. +/react - React to the selected message. +/redact - Redact the selected message. # Rooms /pm <...> - Create a private chat with the given user(s). diff --git a/ui/message-view.go b/ui/message-view.go index 3635a7b..6fa579a 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -338,7 +338,11 @@ func (view *MessageView) SetSelected(message *messages.UIMessage) { if view.selected != nil { view.selected.IsSelected = false } - view.selected = message + if message != nil && (view.selected == message || message.IsService) { + view.selected = nil + } else { + view.selected = message + } if view.selected != nil { view.selected.IsSelected = true } @@ -349,11 +353,9 @@ func (view *MessageView) handleMessageClick(message *messages.UIMessage, mod tce open.Open(msg.Path()) // No need to re-render return false - } else if message.IsService { - // Can't select service messages - return false } view.SetSelected(message) + view.parent.OnSelect(view.selected) return true } diff --git a/ui/messages/parser.go b/ui/messages/parser.go index e4bbc30..75c564e 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -129,7 +129,6 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event evt.Content.RemoveReplyFallback() } if len(evt.Gomuks.Edits) > 0 { - evt = evt.SomewhatDangerousCopy() evt.Content = *evt.Gomuks.Edits[len(evt.Gomuks.Edits)-1].Content.NewContent } switch evt.Content.MsgType { diff --git a/ui/room-view.go b/ui/room-view.go index ab31ca7..a54a5de 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -67,6 +67,12 @@ type RoomView struct { typing []string + selecting bool + selectReason SelectReason + selectContent string + + replying *event.Event + editing *event.Event editMoveText string @@ -148,14 +154,43 @@ func (view *RoomView) Focus() { } func (view *RoomView) Blur() { + view.MessageView().SetSelected(nil) view.input.Blur() } +func (view *RoomView) OnSelect(message *messages.UIMessage) { + if !view.selecting || message == nil { + return + } + switch view.selectReason { + case SelectReply: + view.replying = message.Event + if len(view.selectContent) > 0 { + go view.SendMessage(mautrix.MsgText, view.selectContent) + } + case SelectReact: + go view.SendReaction(message.EventID, view.selectContent) + case SelectRedact: + // TODO redact + } + view.selecting = false + view.selectContent = "" + view.MessageView().SetSelected(nil) +} + func (view *RoomView) GetStatus() string { var buf strings.Builder if view.editing != nil { buf.WriteString("Editing message - ") + } else if view.replying != nil { + buf.WriteString("Replying to ") + buf.WriteString(view.replying.Sender) + buf.WriteString(" - ") + } else if view.selecting { + buf.WriteString("Selecting message to") + buf.WriteString(string(view.selectReason)) + buf.WriteString(" - ") } if len(view.completions.list) > 0 { @@ -373,6 +408,8 @@ func (view *RoomView) SetEditing(evt *event.Event) { view.editMoveText = view.GetInputText() } view.editing = evt + // replying should never be non-nil when SetEditing, but do this just to be safe + view.replying = nil text := view.editing.Content.Body if view.editing.Content.MsgType == mautrix.MsgEmote { text = "/me " + text @@ -393,7 +430,9 @@ func (view *RoomView) findMessageToEdit(forward bool) *event.Event { index = len(msgs) - i - 1 } evt := msgs[index] - if currentFound { + if evt.EventID == "" || evt.EventID == evt.TxnID { + continue + } else if currentFound { if evt.SenderID == self && evt.Event.Type == mautrix.EventMessage { return evt.Event } @@ -413,6 +452,9 @@ func (view *RoomView) EditNext() { } func (view *RoomView) EditPrevious() { + if view.replying != nil { + return + } foundEvent := view.findMessageToEdit(false) if foundEvent != nil { view.SetEditing(foundEvent) @@ -468,6 +510,35 @@ func (view *RoomView) InputSubmit(text string) { } view.editMoveText = "" view.SetInputText("") + view.MessageView().SetSelected(nil) +} + +func (view *RoomView) SendReaction(eventID string, reaction string) { + defer debug.Recover() + debug.Print("Reacting to", eventID, "in", view.Room.ID, "with", reaction) + eventID, err := view.parent.matrix.SendEvent(&event.Event{ + Event: &mautrix.Event{ + Type: mautrix.EventReaction, + RoomID: view.Room.ID, + Content: mautrix.Content{ + RelatesTo: &mautrix.RelatesTo{ + Type: mautrix.RelAnnotation, + EventID: eventID, + Key: reaction, + }, + }, + }, + }) + if err != nil { + if httpErr, ok := err.(mautrix.HTTPError); ok { + err = httpErr + if respErr := httpErr.RespError; respErr != nil { + err = respErr + } + } + view.AddServiceMessage(fmt.Sprintf("Failed to send reaction: %v", err)) + view.parent.parent.Render() + } } func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { @@ -476,10 +547,23 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { if !view.config.Preferences.DisableEmojis { text = emoji.Sprint(text) } - evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text, view.editing) - msg := view.parseEvent(evt) + var rel *ifc.Relation + if view.editing != nil { + rel = &ifc.Relation{ + Type: mautrix.RelReplace, + Event: view.editing, + } + } else if view.replying != nil { + rel = &ifc.Relation{ + Type: mautrix.RelReference, + Event: view.replying, + } + } + evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text, rel) + msg := view.parseEvent(evt.SomewhatDangerousCopy()) view.content.AddMessage(msg, AppendMessage) view.editing = nil + view.replying = nil view.status.SetText(view.GetStatus()) eventID, err := view.parent.matrix.SendEvent(evt) if err != nil {