diff --git a/config/config.go b/config/config.go index e8c7b1a..3507833 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,7 @@ type Config struct { HistoryPath string `yaml:"history_path"` RoomListPath string `yaml:"room_list_path"` MediaDir string `yaml:"media_dir"` + DownloadDir string `yaml:"download_dir"` StateDir string `yaml:"state_dir"` Preferences UserPreferences `yaml:"-"` @@ -78,6 +79,7 @@ type Config struct { // NewConfig creates a config that loads data from the given directory. func NewConfig(configDir, cacheDir string) *Config { + home, _ := os.UserHomeDir() return &Config{ Dir: configDir, CacheDir: cacheDir, @@ -85,6 +87,7 @@ func NewConfig(configDir, cacheDir string) *Config { RoomListPath: filepath.Join(cacheDir, "rooms.gob.gz"), StateDir: filepath.Join(cacheDir, "state"), MediaDir: filepath.Join(cacheDir, "media"), + DownloadDir: home, RoomCacheSize: 32, RoomCacheAge: 1 * 60, diff --git a/go.mod b/go.mod index 137267a..6daa947 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( golang.org/x/net v0.0.0-20200301022130-244492dfa37a gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/yaml.v2 v2.2.8 - maunium.net/go/mautrix v0.1.0-beta.2.0.20200403220206-c81fdd9fe68f + maunium.net/go/mautrix v0.1.0-beta.2.0.20200408115320-100bcaabf80b maunium.net/go/mauview v0.1.0-beta.1 maunium.net/go/tcell v0.1.0 ) diff --git a/go.sum b/go.sum index 371b37c..60af87a 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ maunium.net/go/mautrix v0.1.0-beta.1.0.20200320135656-32dd5f172592 h1:WybA4BBUyo maunium.net/go/mautrix v0.1.0-beta.1.0.20200320135656-32dd5f172592/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= maunium.net/go/mautrix v0.1.0-beta.2.0.20200403220206-c81fdd9fe68f h1:Fg7t2i+AtUX4NDN5Ue9HsHO/Vog0JN9CZsOH3u+hDOc= maunium.net/go/mautrix v0.1.0-beta.2.0.20200403220206-c81fdd9fe68f/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= +maunium.net/go/mautrix v0.1.0-beta.2.0.20200408115320-100bcaabf80b h1:ifTLB2qPUcZmeCeh2/CvlBZz/ddrdXcszdH7H9qFHzE= +maunium.net/go/mautrix v0.1.0-beta.2.0.20200408115320-100bcaabf80b/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= maunium.net/go/mauview v0.1.0-beta.1 h1:hRprD6NTi5Mw7i97DKmgs/TzFQeNpGPytPoswNlU/Ww= maunium.net/go/mauview v0.1.0-beta.1/go.mod h1:og9WbzmWe9SNYNyOFlCv8qa9zMcOvG2nzRJ5vYyud9U= maunium.net/go/tcell v0.1.0 h1:XzsEoGCvOw5nac+tlkSLzQcliLYTN4PrtA7ar2ptjSM= diff --git a/interface/matrix.go b/interface/matrix.go index d4351d7..34d8f7f 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -17,6 +17,7 @@ package ifc import ( + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mautrix" @@ -24,12 +25,13 @@ import ( ) type Relation struct { - Type mautrix.RelationType + Type mautrix.RelationType Event *event.Event } type MatrixContainer interface { Client() *mautrix.Client + Preferences() *config.UserPreferences InitClient() error Initialized() bool @@ -55,7 +57,8 @@ type MatrixContainer interface { GetRoom(roomID string) *rooms.Room GetOrCreateRoom(roomID string) *rooms.Room - Download(mxcURL string) ([]byte, string, string, error) - GetDownloadURL(homeserver, fileID string) string - GetCachePath(homeserver, fileID string) string + Download(uri mautrix.ContentURI) ([]byte, error) + DownloadToDisk(uri mautrix.ContentURI, target string) (string, error) + GetDownloadURL(uri mautrix.ContentURI) string + GetCachePath(uri mautrix.ContentURI) string } diff --git a/matrix/matrix.go b/matrix/matrix.go index 852a410..651f6bb 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -29,7 +29,6 @@ import ( "os" "path" "path/filepath" - "regexp" "runtime" dbg "runtime/debug" "time" @@ -389,6 +388,10 @@ func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) { } } +func (c *Container) Preferences() *config.UserPreferences { + return &c.config.Preferences +} + func (c *Container) SendPreferencesToMatrix() { defer debug.Recover() debug.Print("Sending updated preferences:", c.config.Preferences) @@ -926,22 +929,73 @@ func (c *Container) GetRoom(roomID string) *rooms.Room { return c.config.Rooms.Get(roomID) } -var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") +func cp(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} + +func (c *Container) DownloadToDisk(uri mautrix.ContentURI, target string) (fullPath string, err error) { + cachePath := c.GetCachePath(uri) + if target == "" { + fullPath = cachePath + } else if !path.IsAbs(target) { + fullPath = path.Join(c.config.DownloadDir, target) + } else { + fullPath = target + } + + if _, statErr := os.Stat(cachePath); os.IsNotExist(statErr) { + var file *os.File + file, err = os.OpenFile(cachePath, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return + } + defer file.Close() + + var resp *http.Response + resp, err = c.client.Client.Get(c.GetDownloadURL(uri)) + if err != nil { + return + } + defer resp.Body.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return + } + } + + if fullPath != cachePath { + err = os.MkdirAll(path.Dir(fullPath), 0700) + if err != nil { + return + } + err = cp(cachePath, fullPath) + } + + return +} // Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors. // // The file will be either read from the media cache (if found) or downloaded from the server. -func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err error) { - parts := mxcRegex.FindStringSubmatch(mxcURL) - if parts == nil || len(parts) != 3 { - err = fmt.Errorf("invalid matrix content URL") - return - } - - hs = parts[1] - id = parts[2] - - cacheFile := c.GetCachePath(hs, id) +func (c *Container) Download(uri mautrix.ContentURI) (data []byte, err error) { + cacheFile := c.GetCachePath(uri) var info os.FileInfo if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() { data, err = ioutil.ReadFile(cacheFile) @@ -950,22 +1004,22 @@ func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err err } } - data, err = c.download(hs, id, cacheFile) + data, err = c.download(uri, cacheFile) return } -func (c *Container) GetDownloadURL(hs, id string) string { +func (c *Container) GetDownloadURL(uri mautrix.ContentURI) string { dlURL, _ := url.Parse(c.client.HomeserverURL.String()) if dlURL.Scheme == "" { dlURL.Scheme = "https" } - dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id) + dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", uri.Homeserver, uri.FileID) return dlURL.String() } -func (c *Container) download(hs, id, cacheFile string) (data []byte, err error) { +func (c *Container) download(uri mautrix.ContentURI, cacheFile string) (data []byte, err error) { var resp *http.Response - resp, err = c.client.Client.Get(c.GetDownloadURL(hs, id)) + resp, err = c.client.Client.Get(c.GetDownloadURL(uri)) if err != nil { return } @@ -985,13 +1039,13 @@ func (c *Container) download(hs, id, cacheFile string) (data []byte, err error) // GetCachePath gets the path to the cached version of the given homeserver:fileID combination. // The file may or may not exist, use Download() to ensure it has been cached. -func (c *Container) GetCachePath(homeserver, fileID string) string { - dir := filepath.Join(c.config.MediaDir, homeserver) +func (c *Container) GetCachePath(uri mautrix.ContentURI) string { + dir := filepath.Join(c.config.MediaDir, uri.Homeserver) err := os.MkdirAll(dir, 0700) if err != nil { return "" } - return filepath.Join(dir, fileID) + return filepath.Join(dir, uri.FileID) } diff --git a/ui/command-processor.go b/ui/command-processor.go index 3212dae..082d157 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -92,6 +92,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "remove": {"redact"}, "rm": {"redact"}, "del": {"redact"}, + "dl": {"download"}, + "o": {"open"}, }, commands: map[string]CommandHandler{ "unknown-command": cmdUnknownCommand, @@ -115,6 +117,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "reply": cmdReply, "redact": cmdRedact, "react": cmdReact, + "download": cmdDownload, + "open": cmdOpen, "sendevent": cmdSendEvent, "msendevent": cmdMSendEvent, "setstate": cmdSetState, diff --git a/ui/commands.go b/ui/commands.go index a57a78d..a74a164 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -150,9 +150,11 @@ func cmdID(cmd *Command) { type SelectReason string const ( - SelectReply SelectReason = "reply to" - SelectReact = "react to" - SelectRedact = "redact" + SelectReply SelectReason = "reply to" + SelectReact = "react to" + SelectRedact = "redact" + SelectDownload = "download" + SelectOpen = "open" ) func cmdReply(cmd *Command) { @@ -163,6 +165,14 @@ func cmdRedact(cmd *Command) { cmd.Room.StartSelecting(SelectRedact, strings.Join(cmd.Args, " ")) } +func cmdDownload(cmd *Command) { + cmd.Room.StartSelecting(SelectDownload, strings.Join(cmd.Args, " ")) +} + +func cmdOpen(cmd *Command) { + cmd.Room.StartSelecting(SelectOpen, strings.Join(cmd.Args, " ")) +} + func cmdReact(cmd *Command) { if len(cmd.Args) == 0 { cmd.Reply("Usage: /react ") diff --git a/ui/message-view.go b/ui/message-view.go index eab7bf6..4f406e1 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -349,8 +349,8 @@ func (view *MessageView) SetSelected(message *messages.UIMessage) { } func (view *MessageView) handleMessageClick(message *messages.UIMessage, mod tcell.ModMask) bool { - if msg, ok := message.Renderer.(*messages.FileMessage); ok && mod > 0 { - open.Open(msg.Path()) + if msg, ok := message.Renderer.(*messages.FileMessage); ok && mod > 0 && !msg.Thumbnail.IsEmpty() { + open.Open(msg.ThumbnailPath()) // No need to re-render return false } diff --git a/ui/messages/base.go b/ui/messages/base.go index 58d9b6e..745bfcd 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -27,7 +27,6 @@ import ( "maunium.net/go/mauview" "maunium.net/go/tcell" - "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/ui/widget" ) @@ -36,7 +35,6 @@ type MessageRenderer interface { NotificationContent() string PlainText() string CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage) - RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) Height() int Clone() MessageRenderer String() string diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index c666613..bd5aba9 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -20,7 +20,6 @@ import ( "fmt" "time" - ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -88,5 +87,3 @@ func (msg *ExpandedTextMessage) Draw(screen mauview.Screen) { line.Draw(screen, 0, y) } } - -func (msg *ExpandedTextMessage) RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) {} diff --git a/ui/messages/filemessage.go b/ui/messages/filemessage.go index 9fb3499..daf6c00 100644 --- a/ui/messages/filemessage.go +++ b/ui/messages/filemessage.go @@ -23,6 +23,7 @@ import ( "image/color" "maunium.net/go/gomuks/matrix/event" + "maunium.net/go/mautrix" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -34,72 +35,86 @@ import ( ) type FileMessage struct { - Body string - Homeserver string - FileID string - data []byte - buffer []tstring.TString + Type mautrix.MessageType + Body string + URL mautrix.ContentURI + Thumbnail mautrix.ContentURI + imageData []byte + buffer []tstring.TString matrix ifc.MatrixContainer } // NewFileMessage creates a new FileMessage object with the provided values and the default state. -func NewFileMessage(matrix ifc.MatrixContainer, evt *event.Event, displayname string, body, homeserver, fileID string, data []byte) *UIMessage { +func NewFileMessage(matrix ifc.MatrixContainer, evt *event.Event, displayname string) *UIMessage { + url, _ := mautrix.ParseContentURI(evt.Content.URL) + thumbnail, _ := mautrix.ParseContentURI(evt.Content.GetInfo().ThumbnailURL) return newUIMessage(evt, displayname, &FileMessage{ - Body: body, - Homeserver: homeserver, - FileID: fileID, - data: data, - matrix: matrix, + Type: evt.Content.MsgType, + Body: evt.Content.Body, + URL: url, + Thumbnail: thumbnail, + matrix: matrix, }) } func (msg *FileMessage) Clone() MessageRenderer { - data := make([]byte, len(msg.data)) - copy(data, msg.data) + data := make([]byte, len(msg.imageData)) + copy(data, msg.imageData) return &FileMessage{ - Body: msg.Body, - Homeserver: msg.Homeserver, - FileID: msg.FileID, - data: data, - matrix: msg.matrix, - } -} - -func (msg *FileMessage) RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) { - msg.matrix = matrix - - if len(msg.data) == 0 && !prefs.DisableDownloads { - go msg.updateData() + Body: msg.Body, + URL: msg.URL, + Thumbnail: msg.Thumbnail, + imageData: data, + matrix: msg.matrix, } } func (msg *FileMessage) NotificationContent() string { - return "Sent a file" + switch msg.Type { + case mautrix.MsgImage: + return "Sent an image" + case mautrix.MsgAudio: + return "Sent an audio file" + case mautrix.MsgVideo: + return "Sent a video" + case mautrix.MsgFile: + fallthrough + default: + return "Sent a file" + } } func (msg *FileMessage) PlainText() string { - return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.Homeserver, msg.FileID)) + return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.URL)) } func (msg *FileMessage) String() string { - return fmt.Sprintf(`&messages.FileMessage{Body="%s", Homeserver="%s", FileID="%s"}`, msg.Body, msg.Homeserver, msg.FileID) + return fmt.Sprintf(`&messages.FileMessage{Body="%s", URL="%s", Thumbnail="%s"}`, msg.Body, msg.URL, msg.Thumbnail) } -func (msg *FileMessage) updateData() { - defer debug.Recover() - debug.Print("Loading file:", msg.Homeserver, msg.FileID) - data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID)) +func (msg *FileMessage) DownloadPreview() { + url := msg.Thumbnail + if url.IsEmpty() { + if msg.Type == mautrix.MsgImage && !msg.URL.IsEmpty() { + msg.Thumbnail = msg.URL + url = msg.Thumbnail + } else { + return + } + } + debug.Print("Loading file:", url) + data, err := msg.matrix.Download(url) if err != nil { - debug.Printf("Failed to download file %s/%s: %v", msg.Homeserver, msg.FileID, err) + debug.Printf("Failed to download file %s: %v", url, err) return } - debug.Print("File", msg.Homeserver, msg.FileID, "loaded.") - msg.data = data + debug.Print("File", url, "loaded.") + msg.imageData = data } -func (msg *FileMessage) Path() string { - return msg.matrix.GetCachePath(msg.Homeserver, msg.FileID) +func (msg *FileMessage) ThumbnailPath() string { + return msg.matrix.GetCachePath(msg.Thumbnail) } func (msg *FileMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) { @@ -107,12 +122,12 @@ func (msg *FileMessage) CalculateBuffer(prefs config.UserPreferences, width int, return } - if prefs.BareMessageView || prefs.DisableImages || uiMsg.Type != "m.image" { + if prefs.BareMessageView || prefs.DisableImages || len(msg.imageData) == 0 { msg.buffer = calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width, uiMsg) return } - img, _, err := image.DecodeConfig(bytes.NewReader(msg.data)) + img, _, err := image.DecodeConfig(bytes.NewReader(msg.imageData)) if err != nil { debug.Print("File could not be decoded:", err) } @@ -121,7 +136,7 @@ func (msg *FileMessage) CalculateBuffer(prefs config.UserPreferences, width int, imgWidth = width / 3 } - ansFile, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), 0, imgWidth, color.Black) + ansFile, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.imageData), 0, imgWidth, color.Black) if err != nil { msg.buffer = []tstring.TString{tstring.NewColorTString("Failed to display image", tcell.ColorRed)} debug.Print("Failed to display image:", err) diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index ff49e57..af3ce41 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -17,7 +17,6 @@ package messages import ( - ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -39,8 +38,6 @@ func NewHTMLMessage(evt *event.Event, displayname string, root html.Entity) *UIM }) } -func (hw *HTMLMessage) RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) {} - func (hw *HTMLMessage) Clone() MessageRenderer { return &HTMLMessage{ Root: hw.Root.Clone(), diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 1c145e5..d0bc6e0 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -24,7 +24,6 @@ import ( "maunium.net/go/mautrix" "maunium.net/go/tcell" - "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages/html" @@ -132,18 +131,19 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *event.Event evt.Content = *evt.Gomuks.Edits[len(evt.Gomuks.Edits)-1].Content.NewContent } switch evt.Content.MsgType { - case "m.text", "m.notice", "m.emote": + case mautrix.MsgText, mautrix.MsgNotice, mautrix.MsgEmote: if evt.Content.Format == mautrix.FormatHTML { return NewHTMLMessage(evt, displayname, html.Parse(room, evt, displayname)) } evt.Content.Body = strings.Replace(evt.Content.Body, "\t", " ", -1) return NewTextMessage(evt, displayname, evt.Content.Body) - case "m.file", "m.video", "m.audio", "m.image": - data, hs, id, err := matrix.Download(evt.Content.URL) - if err != nil { - debug.Printf("Failed to download %s: %v", evt.Content.URL, err) + case mautrix.MsgImage, mautrix.MsgVideo, mautrix.MsgAudio, mautrix.MsgFile: + msg := NewFileMessage(matrix, evt, displayname) + if !matrix.Preferences().DisableDownloads { + renderer := msg.Renderer.(*FileMessage) + renderer.DownloadPreview() } - return NewFileMessage(matrix, evt, displayname, evt.Content.Body, hs, id, data) + return msg } return nil } diff --git a/ui/messages/redactedmessage.go b/ui/messages/redactedmessage.go index 56b4822..34d880b 100644 --- a/ui/messages/redactedmessage.go +++ b/ui/messages/redactedmessage.go @@ -17,7 +17,6 @@ package messages import ( - ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -64,5 +63,3 @@ func (msg *RedactedMessage) Draw(screen mauview.Screen) { screen.SetContent(x, 0, RedactionChar, nil, RedactionStyle) } } - -func (msg *RedactedMessage) RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) {} diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 0bfa27b..2e59c8f 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -20,7 +20,6 @@ import ( "fmt" "time" - ifc "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mauview" @@ -101,5 +100,3 @@ func (msg *TextMessage) Draw(screen mauview.Screen) { line.Draw(screen, 0, y) } } - -func (msg *TextMessage) RegisterMatrix(matrix ifc.MatrixContainer, prefs config.UserPreferences) {} diff --git a/ui/room-view.go b/ui/room-view.go index ef19c9d..29c455d 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -27,6 +27,7 @@ import ( "github.com/mattn/go-runewidth" "maunium.net/go/gomuks/debug" + "maunium.net/go/gomuks/lib/open" "maunium.net/go/gomuks/matrix/event" "maunium.net/go/mauview" @@ -191,6 +192,17 @@ func (view *RoomView) OnSelect(message *messages.UIMessage) { go view.SendReaction(message.EventID, view.selectContent) case SelectRedact: go view.Redact(message.EventID, view.selectContent) + case SelectDownload, SelectOpen: + msg, ok := message.Renderer.(*messages.FileMessage) + if ok { + path := "" + if len(view.selectContent) > 0 { + path = view.selectContent + } else if view.selectReason == SelectDownload { + path = msg.Body + } + go view.Download(msg.URL, path, view.selectReason == SelectOpen) + } } view.selecting = false view.selectContent = "" @@ -465,9 +477,22 @@ func (view *RoomView) SetEditing(evt *event.Event) { view.input.SetCursorOffset(-1) } -func (view *RoomView) findMessage(current *event.Event, ownMessage, forward bool) *messages.UIMessage { +type findFilter func(evt *event.Event) bool + +func (view *RoomView) filterOwnOnly(evt *event.Event) bool { + return evt.Sender == view.parent.matrix.Client().UserID && evt.Type == mautrix.EventMessage +} + +func (view *RoomView) filterMediaOnly(evt *event.Event) bool { + return evt.Type == mautrix.EventMessage && ( + evt.Content.MsgType == mautrix.MsgFile || + evt.Content.MsgType == mautrix.MsgImage || + evt.Content.MsgType == mautrix.MsgAudio || + evt.Content.MsgType == mautrix.MsgVideo) +} + +func (view *RoomView) findMessage(current *event.Event, forward bool, allow findFilter) *messages.UIMessage { currentFound := current == nil - self := view.parent.matrix.Client().UserID msgs := view.MessageView().messages for i := 0; i < len(msgs); i++ { index := i @@ -478,7 +503,7 @@ func (view *RoomView) findMessage(current *event.Event, ownMessage, forward bool if evt.EventID == "" || evt.EventID == evt.TxnID || evt.IsService { continue } else if currentFound { - if !ownMessage || (evt.SenderID == self && evt.Event.Type == mautrix.EventMessage) { + if allow == nil || allow(evt.Event) { return evt } } else if evt.EventID == current.ID { @@ -492,7 +517,7 @@ func (view *RoomView) EditNext() { if view.editing == nil { return } - foundMsg := view.findMessage(view.editing, true, true) + foundMsg := view.findMessage(view.editing, true, view.filterOwnOnly) view.SetEditing(foundMsg.GetEvent()) } @@ -500,7 +525,7 @@ func (view *RoomView) EditPrevious() { if view.replying != nil { return } - foundMsg := view.findMessage(view.editing, true, false) + foundMsg := view.findMessage(view.editing, false, view.filterOwnOnly) if foundMsg != nil { view.SetEditing(foundMsg.GetEvent()) } @@ -511,7 +536,11 @@ func (view *RoomView) SelectNext() { if msgView.selected == nil { return } - foundMsg := view.findMessage(msgView.selected.GetEvent(), false, true) + var filter findFilter + if view.selectReason == SelectDownload || view.selectReason == SelectOpen { + filter = view.filterMediaOnly + } + foundMsg := view.findMessage(msgView.selected.GetEvent(), true, filter) if foundMsg != nil { msgView.SetSelected(foundMsg) // TODO scroll selected message into view @@ -520,7 +549,11 @@ func (view *RoomView) SelectNext() { func (view *RoomView) SelectPrevious() { msgView := view.MessageView() - foundMsg := view.findMessage(msgView.selected.GetEvent(), false, false) + var filter findFilter + if view.selectReason == SelectDownload || view.selectReason == SelectOpen { + filter = view.filterMediaOnly + } + foundMsg := view.findMessage(msgView.selected.GetEvent(), false, filter) if foundMsg != nil { msgView.SetSelected(foundMsg) // TODO scroll selected message into view @@ -578,6 +611,20 @@ func (view *RoomView) InputSubmit(text string) { view.SetInputText("") } +func (view *RoomView) Download(url mautrix.ContentURI, filename string, openFile bool) { + path, err := view.parent.matrix.DownloadToDisk(url, filename) + if err != nil { + view.AddServiceMessage(fmt.Sprintf("Failed to download media: %v", err)) + view.parent.parent.Render() + return + } + view.AddServiceMessage(fmt.Sprintf("File downloaded to %s", path)) + view.parent.parent.Render() + if openFile { + open.Open(path) + } +} + func (view *RoomView) Redact(eventID, reason string) { defer debug.Recover() err := view.parent.matrix.Redact(view.Room.ID, eventID, reason)