aboutsummaryrefslogtreecommitdiff
path: root/ui/messages
diff options
context:
space:
mode:
Diffstat (limited to 'ui/messages')
-rw-r--r--ui/messages/cell.go51
-rw-r--r--ui/messages/doc.go2
-rw-r--r--ui/messages/imagemessage.go113
-rw-r--r--ui/messages/message.go147
-rw-r--r--ui/messages/parser.go120
-rw-r--r--ui/messages/string.go138
-rw-r--r--ui/messages/textmessage.go73
7 files changed, 490 insertions, 154 deletions
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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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')