diff --git a/interface/ui.go b/interface/ui.go index 0ddbde7..2d27ea8 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -87,8 +87,9 @@ type MessageMeta interface { SenderColor() tcell.Color TextColor() tcell.Color TimestampColor() tcell.Color - Timestamp() string - Date() string + Timestamp() time.Time + FormatTime() string + FormatDate() string CopyFrom(from MessageMeta) } diff --git a/ui/message-view.go b/ui/message-view.go index f9d477b..7b18ad8 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -49,22 +49,20 @@ type MessageView struct { messageIDs map[string]messages.UIMessage messages []messages.UIMessage - textBuffer []string + textBuffer []messages.UIString metaBuffer []ifc.MessageMeta } func NewMessageView() *MessageView { return &MessageView{ - Box: tview.NewBox(), - MaxSenderWidth: 15, - DateFormat: "January _2, 2006", - TimestampFormat: "15:04:05", - TimestampWidth: 8, - ScrollOffset: 0, + Box: tview.NewBox(), + MaxSenderWidth: 15, + TimestampWidth: len(messages.TimeFormat), + ScrollOffset: 0, messages: make([]messages.UIMessage, 0), messageIDs: make(map[string]messages.UIMessage), - textBuffer: make([]string, 0), + textBuffer: make([]messages.UIString, 0), metaBuffer: make([]ifc.MessageMeta, 0), widestSender: 5, @@ -75,10 +73,7 @@ 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.Format(view.TimestampFormat), - timestamp.Format(view.DateFormat), - widget.GetHashColor(sender)) + return messages.NewMessage(id, sender, msgtype, text, timestamp, widget.GetHashColor(sender)) } func (view *MessageView) SaveHistory(path string) error { @@ -152,10 +147,10 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag return } - msg, messageExists := view.messageIDs[message.ID()] - if msg != nil && messageExists { - msg.CopyFrom(message) - message = msg + oldMsg, messageExists := view.messageIDs[message.ID()] + if messageExists { + oldMsg.CopyFrom(message) + message = oldMsg direction = ifc.IgnoreMessage } @@ -173,6 +168,8 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag view.appendBuffer(message) } else if direction == ifc.PrependMessage { view.messages = append([]messages.UIMessage{message}, view.messages...) + } else { + view.replaceBuffer(message) } view.messageIDs[message.ID()] = message @@ -181,9 +178,12 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag func (view *MessageView) appendBuffer(message messages.UIMessage) { if len(view.metaBuffer) > 0 { prevMeta := view.metaBuffer[len(view.metaBuffer)-1] - if prevMeta != nil && prevMeta.Date() != message.Date() { - view.textBuffer = append(view.textBuffer, fmt.Sprintf("Date changed to %s", message.Date())) - view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{BTextColor: tcell.ColorGreen}) + if prevMeta != nil && prevMeta.FormatDate() != message.FormatDate() { + view.textBuffer = append(view.textBuffer, messages.NewColorUIString( + fmt.Sprintf("Date changed to %s", message.FormatDate()), + tcell.ColorGreen)) + view.metaBuffer = append(view.metaBuffer, &messages.BasicMeta{ + BTimestampColor: tcell.ColorDefault, BTextColor: tcell.ColorGreen}) } } @@ -194,13 +194,42 @@ func (view *MessageView) appendBuffer(message messages.UIMessage) { view.prevMsgCount++ } +func (view *MessageView) replaceBuffer(message messages.UIMessage) { + start := -1 + end := -1 + for index, meta := range view.metaBuffer { + if meta == message { + if start == -1 { + start = index + } + end = index + } else if start != -1 { + break + } + } + + if len(view.textBuffer) > end { + end++ + } + + view.textBuffer = append(append(view.textBuffer[0:start], message.Buffer()...), view.textBuffer[end:]...) + if len(message.Buffer()) != end - start + 1 { + debug.Print(end, "-", start, "!=", len(message.Buffer())) + metaBuffer := view.metaBuffer[0:start] + for range message.Buffer() { + metaBuffer = append(metaBuffer, message) + } + view.metaBuffer = append(metaBuffer, view.metaBuffer[end:]...) + } +} + func (view *MessageView) recalculateBuffers() { _, _, width, height := view.GetInnerRect() width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap recalculateMessageBuffers := width != view.prevWidth if height != view.prevHeight || recalculateMessageBuffers || len(view.messages) != view.prevMsgCount { - view.textBuffer = []string{} + view.textBuffer = []messages.UIString{} view.metaBuffer = []ifc.MessageMeta{} view.prevMsgCount = 0 for _, message := range view.messages { @@ -219,7 +248,7 @@ const PaddingAtTop = 5 func (view *MessageView) AddScrollOffset(diff int) { _, _, _, height := view.GetInnerRect() - totalHeight := len(view.textBuffer) + totalHeight := view.TotalHeight() if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop { view.ScrollOffset = totalHeight - height + PaddingAtTop } else { @@ -285,7 +314,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { x, y, _, height := view.GetInnerRect() view.recalculateBuffers() - if len(view.textBuffer) == 0 { + if view.TotalHeight() == 0 { widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height) return } @@ -294,7 +323,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { messageX := usernameX + view.widestSender + SenderMessageGap separatorX := usernameX + view.widestSender + SenderSeparatorGap - indexOffset := len(view.textBuffer) - view.ScrollOffset - height + indexOffset := view.TotalHeight() - view.ScrollOffset - height if indexOffset <= -PaddingAtTop { message := "Scroll up to load more messages." if view.LoadingMessages { @@ -312,7 +341,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { // Black magic (aka math) used to figure out where the scroll bar should be put. { viewportHeight := float64(height) - contentHeight := float64(len(view.textBuffer)) + contentHeight := float64(view.TotalHeight()) scrollBarHeight = int(math.Ceil(viewportHeight / (contentHeight / viewportHeight))) @@ -328,12 +357,12 @@ func (view *MessageView) Draw(screen tcell.Screen) { if index < 0 { skippedLines++ continue - } else if index >= len(view.textBuffer) { + } else if index >= view.TotalHeight() { break } showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos - isTop := firstLine && view.ScrollOffset+height >= len(view.textBuffer) + isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight() isBottom := line == height-1 && view.ScrollOffset == 0 borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom) @@ -344,8 +373,8 @@ func (view *MessageView) Draw(screen tcell.Screen) { text, meta := view.textBuffer[index], view.metaBuffer[index] if meta != prevMeta { - if len(meta.Timestamp()) > 0 { - widget.WriteLineSimpleColor(screen, meta.Timestamp(), x, y+line, meta.TimestampColor()) + if len(meta.FormatTime()) > 0 { + widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor()) } if prevMeta == nil || meta.Sender() != prevMeta.Sender() { widget.WriteLineColor( @@ -355,6 +384,7 @@ func (view *MessageView) Draw(screen tcell.Screen) { } prevMeta = meta } - widget.WriteLineSimpleColor(screen, text, messageX, y+line, meta.TextColor()) + + text.Draw(screen, messageX, y+line) } } diff --git a/ui/messages/message.go b/ui/messages/message.go index ef0966c..f9ad1f7 100644 --- a/ui/messages/message.go +++ b/ui/messages/message.go @@ -16,7 +16,13 @@ package messages -import "maunium.net/go/gomuks/interface" +import ( + "strings" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/interface" +) // Message is a wrapper for the content and metadata of a Matrix message intended to be displayed. type UIMessage interface { @@ -24,6 +30,154 @@ type UIMessage interface { CalculateBuffer(width int) RecalculateBuffer() - Buffer() []string + Buffer() []UIString Height() int + + 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/meta.go b/ui/messages/meta.go index 8cb6c1b..3f9c9ab 100644 --- a/ui/messages/meta.go +++ b/ui/messages/meta.go @@ -17,13 +17,16 @@ package messages import ( + "time" + "github.com/gdamore/tcell" "maunium.net/go/gomuks/interface" ) // BasicMeta is a simple variable store implementation of MessageMeta. type BasicMeta struct { - BSender, BTimestamp, BDate string + BSender string + BTimestamp time.Time BSenderColor, BTextColor, BTimestampColor tcell.Color } @@ -37,14 +40,19 @@ func (meta *BasicMeta) SenderColor() tcell.Color { return meta.BSenderColor } -// Timestamp returns the formatted time when the message was sent. -func (meta *BasicMeta) Timestamp() string { +// Timestamp returns the full time when the message was sent. +func (meta *BasicMeta) Timestamp() time.Time { return meta.BTimestamp } -// Date returns the formatted date when the message was sent. -func (meta *BasicMeta) Date() string { - return meta.BDate +// FormatTime returns the formatted time when the message was sent. +func (meta *BasicMeta) FormatTime() string { + return meta.BTimestamp.Format(TimeFormat) +} + +// FormatDate returns the formatted date when the message was sent. +func (meta *BasicMeta) FormatDate() string { + return meta.BTimestamp.Format(DateFormat) } // TextColor returns the color the actual content of the message should be shown in. @@ -63,7 +71,6 @@ func (meta *BasicMeta) TimestampColor() tcell.Color { func (meta *BasicMeta) CopyFrom(from ifc.MessageMeta) { meta.BSender = from.Sender() meta.BTimestamp = from.Timestamp() - meta.BDate = from.Date() meta.BSenderColor = from.SenderColor() meta.BTextColor = from.TextColor() meta.BTimestampColor = from.TimestampColor() diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index 1f37418..1a53c2b 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -20,10 +20,9 @@ import ( "encoding/gob" "fmt" "regexp" - "strings" + "time" "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" "maunium.net/go/gomuks/interface" ) @@ -36,22 +35,20 @@ type UITextMessage struct { MsgType string MsgSender string MsgSenderColor tcell.Color - MsgTimestamp string - MsgDate string + MsgTimestamp time.Time MsgText string MsgState ifc.MessageState MsgIsHighlight bool MsgIsService bool - buffer []string + buffer []UIString prevBufferWidth int } // NewMessage creates a new Message object with the provided values and the default state. -func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor tcell.Color) UIMessage { +func NewMessage(id, sender, msgtype, text string, timestamp time.Time, senderColor tcell.Color) UIMessage { return &UITextMessage{ MsgSender: sender, MsgTimestamp: timestamp, - MsgDate: date, MsgSenderColor: senderColor, MsgType: msgtype, MsgText: text, @@ -66,18 +63,19 @@ func NewMessage(id, sender, msgtype, text, timestamp, date string, senderColor t // CopyFrom replaces the content of this message object with the content of the given object. func (msg *UITextMessage) CopyFrom(from ifc.MessageMeta) { msg.MsgSender = from.Sender() - msg.MsgTimestamp = from.Timestamp() - msg.MsgDate = from.Date() 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.MsgText = fromMsg.Text() msg.MsgState = fromMsg.State() msg.MsgIsService = fromMsg.IsService() msg.MsgIsHighlight = fromMsg.IsHighlight() + msg.buffer = nil msg.RecalculateBuffer() } @@ -105,6 +103,10 @@ func (msg *UITextMessage) Sender() string { } } +func (msg *UITextMessage) RealSender() string { + return msg.MsgSender +} + func (msg *UITextMessage) getStateSpecificColor() tcell.Color { switch msg.MsgState { case ifc.MessageStateSending: @@ -176,7 +178,7 @@ func (msg *UITextMessage) RecalculateBuffer() { // // N.B. This will NOT automatically calculate the buffer if it hasn't been // calculated already, as that requires the target width. -func (msg *UITextMessage) Buffer() []string { +func (msg *UITextMessage) Buffer() []UIString { return msg.buffer } @@ -185,14 +187,19 @@ func (msg *UITextMessage) Height() int { return len(msg.buffer) } -// Timestamp returns the formatted time when the message was sent. -func (msg *UITextMessage) Timestamp() string { +// Timestamp returns the full timestamp when the message was sent. +func (msg *UITextMessage) Timestamp() time.Time { return msg.MsgTimestamp } -// Date returns the formatted date when the message was sent. -func (msg *UITextMessage) Date() string { - return msg.MsgDate +// FormatTime returns the formatted time when the message was sent. +func (msg *UITextMessage) FormatTime() string { + return msg.MsgTimestamp.Format(TimeFormat) +} + +// FormatDate returns the formatted date when the message was sent. +func (msg *UITextMessage) FormatDate() string { + return msg.MsgTimestamp.Format(DateFormat) } func (msg *UITextMessage) ID() string { @@ -259,30 +266,31 @@ func (msg *UITextMessage) CalculateBuffer(width int) { return } - msg.buffer = []string{} - text := msg.MsgText + msg.buffer = []UIString{} + text := NewColorUIString(msg.Text(), msg.TextColor()) if msg.MsgType == "m.emote" { - text = fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText) + text = NewColorUIString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor()) + text.Colorize(2, 2+len(msg.MsgSender), msg.SenderColor()) } - forcedLinebreaks := strings.Split(text, "\n") + forcedLinebreaks := text.Split('\n') newlines := 0 for _, str := range forcedLinebreaks { if len(str) == 0 && newlines < 1 { - msg.buffer = append(msg.buffer, "") + msg.buffer = append(msg.buffer, UIString{}) newlines++ } else { newlines = 0 } - // From tview/textview.go#reindexBuffer() + // Mostly from tview/textview.go#reindexBuffer() for len(str) > 0 { - extract := runewidth.Truncate(str, width, "") + extract := str.Truncate(width) if len(extract) < len(str) { - if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { + if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 { extract = str[:len(extract)+spaces[1]] } - matches := boundaryPattern.FindAllStringIndex(extract, -1) + matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) if len(matches) > 0 { extract = extract[:matches[len(matches)-1][1]] }