diff --git a/config/config.go b/config/config.go
index 9179a58..85160c6 100644
--- a/config/config.go
+++ b/config/config.go
@@ -33,6 +33,7 @@ type Config struct {
Dir string `yaml:"-"`
HistoryDir string `yaml:"history_dir"`
+ MediaDir string `yaml:"media_dir"`
Session *Session `yaml:"-"`
}
@@ -41,6 +42,7 @@ func NewConfig(dir string) *Config {
return &Config{
Dir: dir,
HistoryDir: filepath.Join(dir, "history"),
+ MediaDir: filepath.Join(dir, "media"),
}
}
diff --git a/interface/matrix.go b/interface/matrix.go
index f811dff..b7b52c3 100644
--- a/interface/matrix.go
+++ b/interface/matrix.go
@@ -34,4 +34,5 @@ type MatrixContainer interface {
LeaveRoom(roomID string) error
GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error)
GetRoom(roomID string) *rooms.Room
+ Download(mxcURL string) ([]byte, error)
}
diff --git a/interface/ui.go b/interface/ui.go
index 2d27ea8..df92308 100644
--- a/interface/ui.go
+++ b/interface/ui.go
@@ -51,8 +51,9 @@ type MainView interface {
SaveAllHistory()
SetTyping(roomID string, users []string)
- ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message
- ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message
+ ParseEvent(roomView RoomView, evt *gomatrix.Event) Message
+ //ProcessMessageEvent(roomView RoomView, evt *gomatrix.Event) Message
+ //ProcessMembershipEvent(roomView RoomView, evt *gomatrix.Event) Message
NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
}
diff --git a/matrix/matrix.go b/matrix/matrix.go
index 6fff6d8..29419b0 100644
--- a/matrix/matrix.go
+++ b/matrix/matrix.go
@@ -17,19 +17,25 @@
package matrix
import (
+ "bytes"
"encoding/json"
"fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
"strings"
"time"
-
-"maunium.net/go/gomatrix"
-"maunium.net/go/gomuks/config"
-"maunium.net/go/gomuks/debug"
-"maunium.net/go/gomuks/interface"
-"maunium.net/go/gomuks/matrix/pushrules"
-"maunium.net/go/gomuks/matrix/rooms"
-
+ "maunium.net/go/gomatrix"
+ "maunium.net/go/gomuks/config"
+ "maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/matrix/pushrules"
+ "maunium.net/go/gomuks/matrix/rooms"
)
// Container is a wrapper for a gomatrix Client and some other stuff.
@@ -222,7 +228,7 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) {
return
}
- message := mainView.ProcessMessageEvent(roomView, evt)
+ message := mainView.ParseEvent(roomView, evt)
if message != nil {
if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
@@ -279,7 +285,7 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) {
return
}
- message := mainView.ProcessMembershipEvent(roomView, evt)
+ message := mainView.ParseEvent(roomView, evt)
if message != nil {
// TODO this shouldn't be necessary
roomView.MxRoom().UpdateState(evt)
@@ -403,3 +409,53 @@ func (c *Container) GetRoom(roomID string) *rooms.Room {
}
return room
}
+
+var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)")
+
+func (c *Container) Download(mxcURL string) ([]byte, error) {
+ parts := mxcRegex.FindStringSubmatch(mxcURL)
+ if parts == nil || len(parts) != 3 {
+ debug.Print(parts)
+ return nil, fmt.Errorf("invalid matrix content URL")
+ }
+ hs := parts[1]
+ id := parts[2]
+
+ cacheFile := c.getCachePath(hs, id)
+ if _, err := os.Stat(cacheFile); err != nil {
+ data, err := ioutil.ReadFile(cacheFile)
+ if err == nil {
+ return data, nil
+ }
+ }
+
+ dlURL, _ := url.Parse(c.client.HomeserverURL.String())
+ dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id)
+
+ resp, err := c.client.Client.Get(dlURL.String())
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var buf bytes.Buffer
+ _, err = io.Copy(&buf, resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ data := buf.Bytes()
+
+ err = ioutil.WriteFile(cacheFile, data, 0600)
+ return data, err
+}
+func (c *Container) getCachePath(homeserver, fileID string) string {
+ dir := filepath.Join(c.config.MediaDir, homeserver)
+
+ err := os.MkdirAll(dir, 0700)
+ if err != nil {
+ return ""
+ }
+
+ return filepath.Join(dir, fileID)
+}
diff --git a/ui/message-view.go b/ui/message-view.go
index 7b18ad8..dd3edb5 100644
--- a/ui/message-view.go
+++ b/ui/message-view.go
@@ -21,7 +21,6 @@ import (
"fmt"
"math"
"os"
- "time"
"github.com/gdamore/tcell"
"maunium.net/go/gomuks/debug"
@@ -72,10 +71,6 @@ func NewMessageView() *MessageView {
}
}
-func (view *MessageView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
- return messages.NewMessage(id, sender, msgtype, text, timestamp, widget.GetHashColor(sender))
-}
-
func (view *MessageView) SaveHistory(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
@@ -102,14 +97,23 @@ func (view *MessageView) LoadHistory(path string) (int, error) {
}
defer file.Close()
+ var msgs []messages.UIMessage
+
dec := gob.NewDecoder(file)
- err = dec.Decode(&view.messages)
+ err = dec.Decode(&msgs)
if err != nil {
return -1, err
}
- for _, message := range view.messages {
- view.updateWidestSender(message.Sender())
+ view.messages = make([]messages.UIMessage, len(msgs))
+ indexOffset := 0
+ for index, message := range msgs {
+ if message != nil {
+ view.messages[index-indexOffset] = message
+ view.updateWidestSender(message.Sender())
+ } else {
+ indexOffset++
+ }
}
return len(view.messages), nil
@@ -213,7 +217,7 @@ func (view *MessageView) replaceBuffer(message messages.UIMessage) {
}
view.textBuffer = append(append(view.textBuffer[0:start], message.Buffer()...), view.textBuffer[end:]...)
- if len(message.Buffer()) != end - start + 1 {
+ if len(message.Buffer()) != end-start+1 {
debug.Print(end, "-", start, "!=", len(message.Buffer()))
metaBuffer := view.metaBuffer[0:start]
for range message.Buffer() {
@@ -232,7 +236,11 @@ func (view *MessageView) recalculateBuffers() {
view.textBuffer = []messages.UIString{}
view.metaBuffer = []ifc.MessageMeta{}
view.prevMsgCount = 0
- for _, message := range view.messages {
+ for i, message := range view.messages {
+ if message == nil {
+ debug.Print("O.o found nil message at", i)
+ break
+ }
if recalculateMessageBuffers {
message.CalculateBuffer(width)
}
diff --git a/ui/messages/cell.go b/ui/messages/cell.go
new file mode 100644
index 0000000..a919da7
--- /dev/null
+++ b/ui/messages/cell.go
@@ -0,0 +1,51 @@
+// 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 .
+
+package messages
+
+import (
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
+)
+
+type Cell struct {
+ Char rune
+ Style tcell.Style
+}
+
+func NewStyleCell(char rune, style tcell.Style) Cell {
+ return Cell{char, style}
+}
+
+func NewColorCell(char rune, color tcell.Color) Cell {
+ return Cell{char, tcell.StyleDefault.Foreground(color)}
+}
+
+func NewCell(char rune) Cell {
+ return Cell{char, tcell.StyleDefault}
+}
+
+func (cell Cell) RuneWidth() int {
+ return runewidth.RuneWidth(cell.Char)
+}
+
+func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
+ chWidth = cell.RuneWidth()
+ for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
+ screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
+ }
+ return
+}
diff --git a/ui/messages/doc.go b/ui/messages/doc.go
index 7c3c077..289c308 100644
--- a/ui/messages/doc.go
+++ b/ui/messages/doc.go
@@ -1,2 +1,2 @@
-// Package types contains common type definitions used by the UI.
+// Package messages contains different message types and code to generate and render them.
package messages
diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go
new file mode 100644
index 0000000..53c0588
--- /dev/null
+++ b/ui/messages/imagemessage.go
@@ -0,0 +1,113 @@
+// 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 .
+
+package messages
+
+import (
+ "bytes"
+ "encoding/gob"
+ "time"
+
+ "image/color"
+
+ "github.com/gdamore/tcell"
+ "maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/widget"
+ "maunium.net/go/pixterm/ansimage"
+)
+
+func init() {
+ gob.Register(&UIImageMessage{})
+}
+
+type UIImageMessage struct {
+ UITextMessage
+ data []byte
+}
+
+// NewImageMessage creates a new UIImageMessage object with the provided values and the default state.
+func NewImageMessage(id, sender, msgtype string, data []byte, timestamp time.Time) UIMessage {
+ return &UIImageMessage{
+ UITextMessage{
+ MsgSender: sender,
+ MsgTimestamp: timestamp,
+ MsgSenderColor: widget.GetHashColor(sender),
+ MsgType: msgtype,
+ MsgID: id,
+ prevBufferWidth: 0,
+ MsgState: ifc.MessageStateDefault,
+ MsgIsHighlight: false,
+ MsgIsService: false,
+ },
+ data,
+ }
+}
+
+// CopyFrom replaces the content of this message object with the content of the given object.
+func (msg *UIImageMessage) 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
+
+ fromImgMsg, ok := from.(*UIImageMessage)
+ if ok {
+ msg.data = fromImgMsg.data
+ }
+
+ msg.RecalculateBuffer()
+ }
+}
+
+// CalculateBuffer generates the internal buffer for this message that consists
+// of the text of this message split into lines at most as wide as the width
+// parameter.
+func (msg *UIImageMessage) CalculateBuffer(width int) {
+ if width < 2 {
+ return
+ }
+
+ image, err := ansimage.NewScaledFromReader(bytes.NewReader(msg.data), -1, width, color.Black, ansimage.ScaleModeResize, ansimage.NoDithering)
+ if err != nil {
+ msg.buffer = []UIString{NewColorUIString("Failed to display image", tcell.ColorRed)}
+ debug.Print("Failed to display image:", err)
+ return
+ }
+
+ msg.buffer = make([]UIString, image.Height())
+ pixels := image.Pixmap()
+ for row, pixelRow := range pixels {
+ msg.buffer[row] = make(UIString, len(pixelRow))
+ for column, pixel := range pixelRow {
+ pixelColor := tcell.NewRGBColor(int32(pixel.R), int32(pixel.G), int32(pixel.B))
+ msg.buffer[row][column] = Cell{
+ Char: ' ',
+ Style: tcell.StyleDefault.Background(pixelColor),
+ }
+ }
+ }
+ msg.prevBufferWidth = width
+}
diff --git a/ui/messages/message.go b/ui/messages/message.go
index f9ad1f7..f116d84 100644
--- a/ui/messages/message.go
+++ b/ui/messages/message.go
@@ -17,10 +17,6 @@
package messages
import (
- "strings"
-
- "github.com/gdamore/tcell"
- "github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/interface"
)
@@ -36,148 +32,5 @@ type UIMessage interface {
RealSender() string
}
-type Cell struct {
- Char rune
- Style tcell.Style
-}
-
-func NewStyleCell(char rune, style tcell.Style) Cell {
- return Cell{char, style}
-}
-
-func NewColorCell(char rune, color tcell.Color) Cell {
- return Cell{char, tcell.StyleDefault.Foreground(color)}
-}
-
-func NewCell(char rune) Cell {
- return Cell{char, tcell.StyleDefault}
-}
-
-func (cell Cell) RuneWidth() int {
- return runewidth.RuneWidth(cell.Char)
-}
-
-func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
- chWidth = cell.RuneWidth()
- for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
- screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
- }
- return
-}
-
-type UIString []Cell
-
-func NewUIString(str string) UIString {
- newStr := make([]Cell, len(str))
- for i, char := range str {
- newStr[i] = NewCell(char)
- }
- return newStr
-}
-
-func NewColorUIString(str string, color tcell.Color) UIString {
- newStr := make([]Cell, len(str))
- for i, char := range str {
- newStr[i] = NewColorCell(char, color)
- }
- return newStr
-}
-
-func NewStyleUIString(str string, style tcell.Style) UIString {
- newStr := make([]Cell, len(str))
- for i, char := range str {
- newStr[i] = NewStyleCell(char, style)
- }
- return newStr
-}
-
-func (str UIString) Colorize(from, to int, color tcell.Color) {
- for i := from; i < to; i++ {
- str[i].Style = str[i].Style.Foreground(color)
- }
-}
-
-func (str UIString) Draw(screen tcell.Screen, x, y int) {
- offsetX := 0
- for _, cell := range str {
- offsetX += cell.Draw(screen, x+offsetX, y)
- }
-}
-
-func (str UIString) RuneWidth() (width int) {
- for _, cell := range str {
- width += runewidth.RuneWidth(cell.Char)
- }
- return width
-}
-
-func (str UIString) String() string {
- var buf strings.Builder
- for _, cell := range str {
- buf.WriteRune(cell.Char)
- }
- return buf.String()
-}
-
-// Truncate return string truncated with w cells
-func (str UIString) Truncate(w int) UIString {
- if str.RuneWidth() <= w {
- return str[:]
- }
- width := 0
- i := 0
- for ; i < len(str); i++ {
- cw := runewidth.RuneWidth(str[i].Char)
- if width+cw > w {
- break
- }
- width += cw
- }
- return str[0:i]
-}
-
-func (str UIString) IndexFrom(r rune, from int) int {
- for i := from; i < len(str); i++ {
- if str[i].Char == r {
- return i
- }
- }
- return -1
-}
-
-func (str UIString) Index(r rune) int {
- return str.IndexFrom(r, 0)
-}
-
-func (str UIString) Count(r rune) (counter int) {
- index := 0
- for {
- index = str.IndexFrom(r, index)
- if index < 0 {
- break
- }
- index++
- counter++
- }
- return
-}
-
-func (str UIString) Split(sep rune) []UIString {
- a := make([]UIString, str.Count(sep)+1)
- i := 0
- orig := str
- for {
- m := orig.Index(sep)
- if m < 0 {
- break
- }
- a[i] = orig[:m]
- orig = orig[m+1:]
- i++
- }
- a[i] = orig
- return a[:i+1]
-}
-
const DateFormat = "January _2, 2006"
const TimeFormat = "15:04:05"
diff --git a/ui/messages/parser.go b/ui/messages/parser.go
new file mode 100644
index 0000000..4300c86
--- /dev/null
+++ b/ui/messages/parser.go
@@ -0,0 +1,120 @@
+// 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 .
+
+package messages
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/gdamore/tcell"
+ "maunium.net/go/gomatrix"
+ "maunium.net/go/gomuks/debug"
+ "maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/matrix/rooms"
+ "maunium.net/go/gomuks/ui/widget"
+)
+
+func ParseEvent(mx ifc.MatrixContainer, room *rooms.Room, evt *gomatrix.Event) UIMessage {
+ member := room.GetMember(evt.Sender)
+ if member != nil {
+ evt.Sender = member.DisplayName
+ }
+ switch evt.Type {
+ case "m.room.message":
+ return ParseMessage(mx, evt)
+ case "m.room.member":
+ return ParseMembershipEvent(evt)
+ }
+ return nil
+}
+
+func unixToTime(unix int64) time.Time {
+ timestamp := time.Now()
+ if unix != 0 {
+ timestamp = time.Unix(unix/1000, unix%1000*1000)
+ }
+ return timestamp
+}
+
+func ParseMessage(mx ifc.MatrixContainer, evt *gomatrix.Event) UIMessage {
+ msgtype, _ := evt.Content["msgtype"].(string)
+ ts := unixToTime(evt.Timestamp)
+ switch msgtype {
+ case "m.text", "m.notice":
+ text, _ := evt.Content["body"].(string)
+ return NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts)
+ case "m.image":
+ url, _ := evt.Content["url"].(string)
+ data, err := mx.Download(url)
+ if err != nil {
+ debug.Printf("Failed to download %s: %v", url, err)
+ }
+ return NewImageMessage(evt.ID, evt.Sender, msgtype, data, ts)
+ }
+ return nil
+}
+
+func getMembershipEventContent(evt *gomatrix.Event) (sender string, text UIString) {
+ membership, _ := evt.Content["membership"].(string)
+ displayname, _ := evt.Content["displayname"].(string)
+ if len(displayname) == 0 {
+ displayname = *evt.StateKey
+ }
+ prevMembership := "leave"
+ prevDisplayname := ""
+ if evt.Unsigned.PrevContent != nil {
+ prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
+ prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string)
+ }
+
+ if membership != prevMembership {
+ switch membership {
+ case "invite":
+ sender = "---"
+ text = NewColorUIString(fmt.Sprintf("%s invited %s.", evt.Sender, displayname), tcell.ColorYellow)
+ text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender))
+ text.Colorize(len(evt.Sender)+len(" invited "), len(displayname), widget.GetHashColor(displayname))
+ case "join":
+ sender = "-->"
+ text = NewColorUIString(fmt.Sprintf("%s joined the room.", displayname), tcell.ColorGreen)
+ text.Colorize(0, len(displayname), widget.GetHashColor(displayname))
+ case "leave":
+ sender = "<--"
+ if evt.Sender != *evt.StateKey {
+ reason, _ := evt.Content["reason"].(string)
+ text = NewColorUIString(fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason), tcell.ColorRed)
+ text.Colorize(0, len(evt.Sender), widget.GetHashColor(evt.Sender))
+ text.Colorize(len(evt.Sender)+len(" kicked "), len(displayname), widget.GetHashColor(displayname))
+ } else {
+ text = NewColorUIString(fmt.Sprintf("%s left the room.", displayname), tcell.ColorRed)
+ text.Colorize(0, len(displayname), widget.GetHashColor(displayname))
+ }
+ }
+ } else if displayname != prevDisplayname {
+ sender = "---"
+ text = NewColorUIString(fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname), tcell.ColorYellow)
+ text.Colorize(0, len(prevDisplayname), widget.GetHashColor(prevDisplayname))
+ text.Colorize(len(prevDisplayname)+len(" changed their display name to "), len(displayname), widget.GetHashColor(displayname))
+ }
+ return
+}
+
+func ParseMembershipEvent(evt *gomatrix.Event) UIMessage {
+ sender, text := getMembershipEventContent(evt)
+ ts := unixToTime(evt.Timestamp)
+ return NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts)
+}
diff --git a/ui/messages/string.go b/ui/messages/string.go
new file mode 100644
index 0000000..7c3143b
--- /dev/null
+++ b/ui/messages/string.go
@@ -0,0 +1,138 @@
+// 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 .
+
+package messages
+
+import (
+ "strings"
+
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
+)
+
+type UIString []Cell
+
+func NewUIString(str string) UIString {
+ newStr := make([]Cell, len(str))
+ for i, char := range str {
+ newStr[i] = NewCell(char)
+ }
+ return newStr
+}
+
+func NewColorUIString(str string, color tcell.Color) UIString {
+ newStr := make([]Cell, len(str))
+ for i, char := range str {
+ newStr[i] = NewColorCell(char, color)
+ }
+ return newStr
+}
+
+func NewStyleUIString(str string, style tcell.Style) UIString {
+ newStr := make([]Cell, len(str))
+ for i, char := range str {
+ newStr[i] = NewStyleCell(char, style)
+ }
+ return newStr
+}
+
+func (str UIString) Colorize(from, length int, color tcell.Color) {
+ for i := from; i < from+length; i++ {
+ str[i].Style = str[i].Style.Foreground(color)
+ }
+}
+
+func (str UIString) Draw(screen tcell.Screen, x, y int) {
+ offsetX := 0
+ for _, cell := range str {
+ offsetX += cell.Draw(screen, x+offsetX, y)
+ }
+}
+
+func (str UIString) RuneWidth() (width int) {
+ for _, cell := range str {
+ width += runewidth.RuneWidth(cell.Char)
+ }
+ return width
+}
+
+func (str UIString) String() string {
+ var buf strings.Builder
+ for _, cell := range str {
+ buf.WriteRune(cell.Char)
+ }
+ return buf.String()
+}
+
+// Truncate return string truncated with w cells
+func (str UIString) Truncate(w int) UIString {
+ if str.RuneWidth() <= w {
+ return str[:]
+ }
+ width := 0
+ i := 0
+ for ; i < len(str); i++ {
+ cw := runewidth.RuneWidth(str[i].Char)
+ if width+cw > w {
+ break
+ }
+ width += cw
+ }
+ return str[0:i]
+}
+
+func (str UIString) IndexFrom(r rune, from int) int {
+ for i := from; i < len(str); i++ {
+ if str[i].Char == r {
+ return i
+ }
+ }
+ return -1
+}
+
+func (str UIString) Index(r rune) int {
+ return str.IndexFrom(r, 0)
+}
+
+func (str UIString) Count(r rune) (counter int) {
+ index := 0
+ for {
+ index = str.IndexFrom(r, index)
+ if index < 0 {
+ break
+ }
+ index++
+ counter++
+ }
+ return
+}
+
+func (str UIString) Split(sep rune) []UIString {
+ a := make([]UIString, str.Count(sep)+1)
+ i := 0
+ orig := str
+ for {
+ m := orig.Index(sep)
+ if m < 0 {
+ break
+ }
+ a[i] = orig[:m]
+ orig = orig[m+1:]
+ i++
+ }
+ a[i] = orig
+ return a[:i+1]
+}
diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go
index 1a53c2b..8ad3168 100644
--- a/ui/messages/textmessage.go
+++ b/ui/messages/textmessage.go
@@ -24,10 +24,67 @@ import (
"github.com/gdamore/tcell"
"maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/ui/widget"
)
func init() {
gob.Register(&UITextMessage{})
+ gob.Register(&UIExpandedTextMessage{})
+}
+
+type UIExpandedTextMessage struct {
+ UITextMessage
+ MsgUIStringText UIString
+}
+
+// NewExpandedTextMessage creates a new UIExpandedTextMessage object with the provided values and the default state.
+func NewExpandedTextMessage(id, sender, msgtype string, text UIString, 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) GetUIStringText() UIString {
+ return msg.MsgUIStringText
+}
+
+// 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.MsgUIStringText = fromExpandedMsg.MsgUIStringText
+ } else {
+ msg.MsgUIStringText = NewColorUIString(fromMsg.Text(), from.TextColor())
+ }
+
+ msg.RecalculateBuffer()
+ }
}
type UITextMessage struct {
@@ -44,12 +101,12 @@ type UITextMessage struct {
prevBufferWidth int
}
-// NewMessage creates a new Message object with the provided values and the default state.
-func NewMessage(id, sender, msgtype, text string, timestamp time.Time, senderColor tcell.Color) UIMessage {
+// NewTextMessage creates a new UITextMessage object with the provided values and the default state.
+func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMessage {
return &UITextMessage{
MsgSender: sender,
MsgTimestamp: timestamp,
- MsgSenderColor: senderColor,
+ MsgSenderColor: widget.GetHashColor(sender),
MsgType: msgtype,
MsgText: text,
MsgID: id,
@@ -250,6 +307,10 @@ func (msg *UITextMessage) SetIsService(isService bool) {
msg.MsgIsService = isService
}
+func (msg *UITextMessage) GetUIStringText() UIString {
+ return NewColorUIString(msg.Text(), msg.TextColor())
+}
+
// Regular expressions used to split lines when calculating the buffer.
//
// From tview/textview.go
@@ -267,10 +328,10 @@ func (msg *UITextMessage) CalculateBuffer(width int) {
}
msg.buffer = []UIString{}
- text := NewColorUIString(msg.Text(), msg.TextColor())
+ text := msg.GetUIStringText()
if msg.MsgType == "m.emote" {
- text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor())
- text.Colorize(2, 2+len(msg.MsgSender), msg.SenderColor())
+ text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, text.String()), msg.TextColor())
+ text.Colorize(2, len(msg.MsgSender), msg.SenderColor())
}
forcedLinebreaks := text.Split('\n')
diff --git a/ui/room-view.go b/ui/room-view.go
index 24325b4..749a432 100644
--- a/ui/room-view.go
+++ b/ui/room-view.go
@@ -260,7 +260,7 @@ func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp t
if member != nil {
sender = member.DisplayName
}
- return view.content.NewMessage(id, sender, msgtype, text, timestamp)
+ return messages.NewTextMessage(id, sender, msgtype, text, timestamp)
}
func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) ifc.Message {
diff --git a/ui/view-main.go b/ui/view-main.go
index b3b5b82..a97f9b2 100644
--- a/ui/view-main.go
+++ b/ui/view-main.go
@@ -32,6 +32,7 @@ import (
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/notification"
+ "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tview"
)
@@ -446,12 +447,7 @@ func (view *MainView) LoadHistory(room string, initial bool) {
}
roomView.Room.PrevBatch = prevBatch
for _, evt := range history {
- var message ifc.Message
- if evt.Type == "m.room.message" {
- message = view.ProcessMessageEvent(roomView, &evt)
- } else if evt.Type == "m.room.member" {
- message = view.ProcessMembershipEvent(roomView, &evt)
- }
+ message := view.ParseEvent(roomView, &evt)
if message != nil {
roomView.AddMessage(message, ifc.PrependMessage)
}
@@ -464,61 +460,6 @@ func (view *MainView) LoadHistory(room string, initial bool) {
view.parent.Render()
}
-func (view *MainView) ProcessMessageEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message {
- text, _ := evt.Content["body"].(string)
- msgtype, _ := evt.Content["msgtype"].(string)
- return room.NewMessage(evt.ID, evt.Sender, msgtype, text, unixToTime(evt.Timestamp))
-}
-
-func (view *MainView) getMembershipEventContent(evt *gomatrix.Event) (sender, text string) {
- membership, _ := evt.Content["membership"].(string)
- displayname, _ := evt.Content["displayname"].(string)
- if len(displayname) == 0 {
- displayname = *evt.StateKey
- }
- prevMembership := "leave"
- prevDisplayname := ""
- if evt.Unsigned.PrevContent != nil {
- prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string)
- prevDisplayname, _ = evt.Unsigned.PrevContent["displayname"].(string)
- }
-
- if membership != prevMembership {
- switch membership {
- case "invite":
- sender = "---"
- text = fmt.Sprintf("%s invited %s.", evt.Sender, displayname)
- case "join":
- sender = "-->"
- text = fmt.Sprintf("%s joined the room.", displayname)
- case "leave":
- sender = "<--"
- if evt.Sender != *evt.StateKey {
- reason, _ := evt.Content["reason"].(string)
- text = fmt.Sprintf("%s kicked %s: %s", evt.Sender, displayname, reason)
- } else {
- text = fmt.Sprintf("%s left the room.", displayname)
- }
- }
- } else if displayname != prevDisplayname {
- sender = "---"
- text = fmt.Sprintf("%s changed their display name to %s.", prevDisplayname, displayname)
- }
- return
-}
-
-func (view *MainView) ProcessMembershipEvent(room ifc.RoomView, evt *gomatrix.Event) ifc.Message {
- sender, text := view.getMembershipEventContent(evt)
- if len(text) == 0 {
- return nil
- }
- return room.NewMessage(evt.ID, sender, "m.room.member", text, unixToTime(evt.Timestamp))
-}
-
-func unixToTime(unix int64) time.Time {
- timestamp := time.Now()
- if unix != 0 {
- timestamp = time.Unix(unix/1000, unix%1000*1000)
- }
- return timestamp
+func (view *MainView) ParseEvent(roomView ifc.RoomView, evt *gomatrix.Event) ifc.Message {
+ return messages.ParseEvent(view.matrix, roomView.MxRoom(), evt)
}