Allow clicking images and load images from cache

This commit is contained in:
Tulir Asokan 2018-04-11 19:20:40 +03:00
parent ff7ee333a1
commit 92a2428865
11 changed files with 181 additions and 89 deletions

View File

@ -34,5 +34,6 @@ type MatrixContainer interface {
LeaveRoom(roomID string) error LeaveRoom(roomID string) error
GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error)
GetRoom(roomID string) *rooms.Room GetRoom(roomID string) *rooms.Room
Download(mxcURL string) ([]byte, string, error) Download(mxcURL string) ([]byte, string, string, error)
GetCachePath(homeserver, fileID string) string
} }

View File

@ -71,7 +71,7 @@ const (
type RoomView interface { type RoomView interface {
MxRoom() *rooms.Room MxRoom() *rooms.Room
SaveHistory(dir string) error SaveHistory(dir string) error
LoadHistory(dir string) (int, error) LoadHistory(gmx Gomuks, dir string) (int, error)
SetStatus(status string) SetStatus(status string)
SetTyping(users []string) SetTyping(users []string)

View File

@ -413,17 +413,16 @@ func (c *Container) GetRoom(roomID string) *rooms.Room {
var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)")
func (c *Container) Download(mxcURL string) (data []byte, fullID string, err error) { func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err error) {
parts := mxcRegex.FindStringSubmatch(mxcURL) parts := mxcRegex.FindStringSubmatch(mxcURL)
if parts == nil || len(parts) != 3 { if parts == nil || len(parts) != 3 {
err = fmt.Errorf("invalid matrix content URL") err = fmt.Errorf("invalid matrix content URL")
return return
} }
hs := parts[1] hs = parts[1]
id := parts[2] id = parts[2]
fullID = fmt.Sprintf("%s/%s", hs, id)
cacheFile := c.getCachePath(hs, id) cacheFile := c.GetCachePath(hs, id)
if _, err = os.Stat(cacheFile); err != nil { if _, err = os.Stat(cacheFile); err != nil {
data, err = ioutil.ReadFile(cacheFile) data, err = ioutil.ReadFile(cacheFile)
if err == nil { if err == nil {
@ -452,7 +451,8 @@ func (c *Container) Download(mxcURL string) (data []byte, fullID string, err err
err = ioutil.WriteFile(cacheFile, data, 0600) err = ioutil.WriteFile(cacheFile, data, 0600)
return return
} }
func (c *Container) getCachePath(homeserver, fileID string) string {
func (c *Container) GetCachePath(homeserver, fileID string) string {
dir := filepath.Join(c.config.MediaDir, homeserver) dir := filepath.Join(c.config.MediaDir, homeserver)
err := os.MkdirAll(dir, 0700) err := os.MkdirAll(dir, 0700)

View File

@ -22,6 +22,7 @@ import (
"math" "math"
"os" "os"
"maunium.net/go/gomuks/lib/open"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
@ -88,7 +89,7 @@ func (view *MessageView) SaveHistory(path string) error {
return nil return nil
} }
func (view *MessageView) LoadHistory(path string) (int, error) { func (view *MessageView) LoadHistory(gmx ifc.Gomuks, path string) (int, error) {
file, err := os.OpenFile(path, os.O_RDONLY, 0600) file, err := os.OpenFile(path, os.O_RDONLY, 0600)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -112,6 +113,7 @@ func (view *MessageView) LoadHistory(path string) (int, error) {
if message != nil { if message != nil {
view.messages[index-indexOffset] = message view.messages[index-indexOffset] = message
view.updateWidestSender(message.Sender()) view.updateWidestSender(message.Sender())
message.RegisterGomuks(gmx)
} else { } else {
indexOffset++ indexOffset++
} }
@ -251,6 +253,30 @@ func (view *MessageView) recalculateBuffers() {
} }
} }
func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) {
if button != tcell.Button1 {
return
}
_, _, _, height := view.GetRect()
line := view.TotalHeight() - view.ScrollOffset - height + y
if line < 0 || line >= view.TotalHeight() {
return
}
message := view.metaBuffer[line]
imageMessage, ok := message.(*messages.UIImageMessage)
if !ok {
uiMessage, ok := message.(messages.UIMessage)
if ok {
debug.Print("Message clicked:", uiMessage.Text())
}
return
}
open.Open(imageMessage.Path())
}
const PaddingAtTop = 5 const PaddingAtTop = 5
func (view *MessageView) AddScrollOffset(diff int) { func (view *MessageView) AddScrollOffset(diff int) {

View File

@ -0,0 +1,86 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2018 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package messages
import (
"encoding/gob"
"time"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget"
)
func init() {
gob.Register(&UITextMessage{})
gob.Register(&UIExpandedTextMessage{})
}
type UIExpandedTextMessage struct {
UITextMessage
MsgTStringText tstring.TString
}
// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state.
func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage {
return &UIExpandedTextMessage{
UITextMessage{
MsgSender: sender,
MsgTimestamp: timestamp,
MsgSenderColor: widget.GetHashColor(sender),
MsgType: msgtype,
MsgText: text.String(),
MsgID: id,
prevBufferWidth: 0,
MsgState: ifc.MessageStateDefault,
MsgIsHighlight: false,
MsgIsService: false,
},
text,
}
}
func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString {
return msg.MsgTStringText
}
// CopyFrom replaces the content of this message object with the content of the given object.
func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) {
msg.MsgSender = from.Sender()
msg.MsgSenderColor = from.SenderColor()
fromMsg, ok := from.(UIMessage)
if ok {
msg.MsgSender = fromMsg.RealSender()
msg.MsgID = fromMsg.ID()
msg.MsgType = fromMsg.Type()
msg.MsgTimestamp = fromMsg.Timestamp()
msg.MsgState = fromMsg.State()
msg.MsgIsService = fromMsg.IsService()
msg.MsgIsHighlight = fromMsg.IsHighlight()
msg.buffer = nil
fromExpandedMsg, ok := from.(*UIExpandedTextMessage)
if ok {
msg.MsgTStringText = fromExpandedMsg.MsgTStringText
} else {
msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor())
}
msg.RecalculateBuffer()
}
}

View File

@ -19,15 +19,16 @@ package messages
import ( import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"fmt"
"time" "time"
"image/color" "image/color"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/ansimage"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/gomuks/lib/ansimage"
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
@ -37,12 +38,15 @@ func init() {
type UIImageMessage struct { type UIImageMessage struct {
UITextMessage UITextMessage
Path string Homeserver string
FileID string
data []byte data []byte
gmx ifc.Gomuks
} }
// NewImageMessage creates a new UIImageMessage object with the provided values and the default state. // NewImageMessage creates a new UIImageMessage object with the provided values and the default state.
func NewImageMessage(id, sender, msgtype string, path string, data []byte, timestamp time.Time) UIMessage { func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage {
return &UIImageMessage{ return &UIImageMessage{
UITextMessage{ UITextMessage{
MsgSender: sender, MsgSender: sender,
@ -55,11 +59,39 @@ func NewImageMessage(id, sender, msgtype string, path string, data []byte, times
MsgIsHighlight: false, MsgIsHighlight: false,
MsgIsService: false, MsgIsService: false,
}, },
path, homeserver,
fileID,
data, data,
gmx,
} }
} }
func (msg *UIImageMessage) RegisterGomuks(gmx ifc.Gomuks) {
msg.gmx = gmx
debug.Print(len(msg.data), msg.data)
if len(msg.data) == 0 {
go func() {
defer gmx.Recover()
msg.updateData()
}()
}
}
func (msg *UIImageMessage) updateData() {
debug.Print("Loading image:", msg.Homeserver, msg.FileID)
data, _, _, err := msg.gmx.Matrix().Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
if err != nil {
debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
return
}
msg.data = data
}
func (msg *UIImageMessage) Path() string {
return msg.gmx.Matrix().GetCachePath(msg.Homeserver, msg.FileID)
}
// CopyFrom replaces the content of this message object with the content of the given object. // CopyFrom replaces the content of this message object with the content of the given object.
func (msg *UIImageMessage) CopyFrom(from ifc.MessageMeta) { func (msg *UIImageMessage) CopyFrom(from ifc.MessageMeta) {
msg.MsgSender = from.Sender() msg.MsgSender = from.Sender()

View File

@ -31,6 +31,7 @@ type UIMessage interface {
Height() int Height() int
RealSender() string RealSender() string
RegisterGomuks(gmx ifc.Gomuks)
} }
const DateFormat = "January _2, 2006" const DateFormat = "January _2, 2006"

View File

@ -29,14 +29,14 @@ import (
"maunium.net/go/tcell" "maunium.net/go/tcell"
) )
func ParseEvent(mx ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) UIMessage { func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) UIMessage {
member := room.GetMember(evt.Sender) member := room.GetMember(evt.Sender)
if member != nil { if member != nil {
evt.Sender = member.DisplayName evt.Sender = member.DisplayName
} }
switch evt.Type { switch evt.Type {
case "m.room.message": case "m.room.message":
return ParseMessage(mx, evt) return ParseMessage(gmx, evt)
case "m.room.member": case "m.room.member":
return ParseMembershipEvent(evt) return ParseMembershipEvent(evt)
} }
@ -51,7 +51,7 @@ func unixToTime(unix int64) time.Time {
return timestamp return timestamp
} }
func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage { func ParseMessage(gmx ifc.Gomuks, evt *gomatrix.Event) UIMessage {
msgtype, _ := evt.Content["msgtype"].(string) msgtype, _ := evt.Content["msgtype"].(string)
ts := unixToTime(evt.Timestamp) ts := unixToTime(evt.Timestamp)
switch msgtype { switch msgtype {
@ -60,11 +60,11 @@ func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage {
return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts)
case "m.image": case "m.image":
url, _ := evt.Content["url"].(string) url, _ := evt.Content["url"].(string)
data, path, err := mx.Download(url) data, hs, id, err := gmx.Matrix().Download(url)
if err != nil { if err != nil {
debug.Printf("Failed to download %s: %v", url, err) debug.Printf("Failed to download %s: %v", url, err)
} }
return NewImageMessage(evt.ID, evt.Sender, msgtype, path, data, ts) return NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts)
} }
return nil return nil
} }

View File

@ -22,70 +22,14 @@ import (
"regexp" "regexp"
"time" "time"
"maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tcell"
) )
func init() { func init() {
gob.Register(&UITextMessage{}) gob.Register(&UITextMessage{})
gob.Register(&UIExpandedTextMessage{})
}
type UIExpandedTextMessage struct {
UITextMessage
MsgTStringText tstring.TString
}
// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state.
func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage {
return &UIExpandedTextMessage{
UITextMessage{
MsgSender: sender,
MsgTimestamp: timestamp,
MsgSenderColor: widget.GetHashColor(sender),
MsgType: msgtype,
MsgText: text.String(),
MsgID: id,
prevBufferWidth: 0,
MsgState: ifc.MessageStateDefault,
MsgIsHighlight: false,
MsgIsService: false,
},
text,
}
}
func (msg *UIExpandedTextMessage) GetTStringText() tstring.TString {
return msg.MsgTStringText
}
// CopyFrom replaces the content of this message object with the content of the given object.
func (msg *UIExpandedTextMessage) CopyFrom(from ifc.MessageMeta) {
msg.MsgSender = from.Sender()
msg.MsgSenderColor = from.SenderColor()
fromMsg, ok := from.(UIMessage)
if ok {
msg.MsgSender = fromMsg.RealSender()
msg.MsgID = fromMsg.ID()
msg.MsgType = fromMsg.Type()
msg.MsgTimestamp = fromMsg.Timestamp()
msg.MsgState = fromMsg.State()
msg.MsgIsService = fromMsg.IsService()
msg.MsgIsHighlight = fromMsg.IsHighlight()
msg.buffer = nil
fromExpandedMsg, ok := from.(*UIExpandedTextMessage)
if ok {
msg.MsgTStringText = fromExpandedMsg.MsgTStringText
} else {
msg.MsgTStringText = tstring.NewColorTString(fromMsg.Text(), from.TextColor())
}
msg.RecalculateBuffer()
}
} }
type UITextMessage struct { type UITextMessage struct {
@ -118,6 +62,8 @@ func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMes
} }
} }
func (msg *UITextMessage) RegisterGomuks(gmx ifc.Gomuks) {}
// CopyFrom replaces the content of this message object with the content of the given object. // CopyFrom replaces the content of this message object with the content of the given object.
func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) {
msg.MsgSender = from.Sender() msg.MsgSender = from.Sender()

View File

@ -81,8 +81,8 @@ func (view *RoomView) SaveHistory(dir string) error {
return view.MessageView().SaveHistory(view.logPath(dir)) return view.MessageView().SaveHistory(view.logPath(dir))
} }
func (view *RoomView) LoadHistory(dir string) (int, error) { func (view *RoomView) LoadHistory(gmx ifc.Gomuks, dir string) (int, error) {
return view.MessageView().LoadHistory(view.logPath(dir)) return view.MessageView().LoadHistory(gmx, view.logPath(dir))
} }
func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView { func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView {

View File

@ -23,7 +23,6 @@ import (
"time" "time"
"unicode" "unicode"
"maunium.net/go/tcell"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -34,6 +33,7 @@ import (
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tcell"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -221,7 +221,7 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *
const WheelScrollOffsetDiff = 3 const WheelScrollOffsetDiff = 3
func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse { func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
if event.Buttons() == tcell.ButtonNone { if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
return event return event
} }
view.BumpFocus() view.BumpFocus()
@ -230,11 +230,6 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo
x, y := event.Position() x, y := event.Position()
switch event.Buttons() { switch event.Buttons() {
case tcell.Button1:
mx, my, mw, mh := msgView.GetRect()
if x >= mx && y >= my && x < mx+mw && y < my+mh {
debug.Print("Message view clicked")
}
case tcell.WheelUp: case tcell.WheelUp:
if msgView.IsAtTop() { if msgView.IsAtTop() {
go view.LoadHistory(roomView.Room.ID, false) go view.LoadHistory(roomView.Room.ID, false)
@ -252,7 +247,12 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo
roomView.Room.MarkRead() roomView.Room.MarkRead()
} }
default: default:
mx, my, mw, mh := msgView.GetRect()
if x >= mx && y >= my && x < mx+mw && y < my+mh {
msgView.HandleClick(x-mx, y-my, event.Buttons())
} else {
debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y) debug.Print("Mouse event received:", event.Buttons(), event.Modifiers(), x, y)
}
return event return event
} }
@ -315,7 +315,7 @@ func (view *MainView) addRoom(index int, room string) {
view.roomView.AddPage(room, roomView, true, false) view.roomView.AddPage(room, roomView, true, false)
roomView.UpdateUserList() roomView.UpdateUserList()
count, err := roomView.LoadHistory(view.config.HistoryDir) count, err := roomView.LoadHistory(view.gmx, view.config.HistoryDir)
if err != nil { if err != nil {
debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err) debug.Printf("Failed to load history of %s: %v", roomView.Room.GetTitle(), err)
} else if count <= 0 { } else if count <= 0 {
@ -466,5 +466,5 @@ func (view *MainView) LoadHistory(room string, initial bool) {
} }
func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message { func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message {
return messages.ParseEvent(view.matrix, roomView.MxRoom(), evt) return messages.ParseEvent(view.gmx, roomView.MxRoom(), evt)
} }