Display thumbnail for all files and add commands to download and open files

This commit is contained in:
Tulir Asokan
2020-04-08 15:30:29 +03:00
parent 80564b2887
commit a6f6fb3ef2
16 changed files with 224 additions and 100 deletions

View File

@ -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,

View File

@ -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 <reaction>")

View File

@ -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
}

View File

@ -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

View File

@ -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) {}

View File

@ -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)

View File

@ -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(),

View File

@ -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
}

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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)