aboutsummaryrefslogtreecommitdiff
path: root/ui/widget/message-view.go
diff options
context:
space:
mode:
Diffstat (limited to 'ui/widget/message-view.go')
-rw-r--r--ui/widget/message-view.go293
1 files changed, 293 insertions, 0 deletions
diff --git a/ui/widget/message-view.go b/ui/widget/message-view.go
new file mode 100644
index 0000000..3503a5f
--- /dev/null
+++ b/ui/widget/message-view.go
@@ -0,0 +1,293 @@
+// 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 widget
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
+ "maunium.net/go/gomuks/ui/types"
+ "maunium.net/go/tview"
+)
+
+type MessageView struct {
+ *tview.Box
+
+ ScrollOffset int
+ MaxSenderWidth int
+ DateFormat string
+ TimestampFormat string
+ TimestampWidth int
+ Separator rune
+
+ widestSender int
+ prevWidth int
+ prevHeight int
+ prevScrollOffset int
+ firstDisplayMessage int
+ lastDisplayMessage int
+ totalHeight int
+
+ messageIDs map[string]bool
+ messages []*types.Message
+}
+
+func NewMessageView() *MessageView {
+ return &MessageView{
+ Box: tview.NewBox(),
+ MaxSenderWidth: 20,
+ DateFormat: "January _2, 2006",
+ TimestampFormat: "15:04:05",
+ TimestampWidth: 8,
+ Separator: '|',
+ ScrollOffset: 0,
+
+ messages: make([]*types.Message, 0),
+ messageIDs: make(map[string]bool),
+
+ widestSender: 5,
+ prevWidth: -1,
+ prevHeight: -1,
+ prevScrollOffset: -1,
+ firstDisplayMessage: -1,
+ lastDisplayMessage: -1,
+ totalHeight: -1,
+ }
+}
+
+func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *types.Message {
+ return types.NewMessage(id, sender, text,
+ timestamp.Format(view.TimestampFormat),
+ timestamp.Format(view.DateFormat),
+ GetHashColor(sender))
+}
+
+func (view *MessageView) recalculateBuffers() {
+ _, _, width, _ := view.GetInnerRect()
+ width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
+ if width != view.prevWidth {
+ for _, message := range view.messages {
+ message.CalculateBuffer(width)
+ }
+ view.prevWidth = width
+ }
+}
+
+func (view *MessageView) updateWidestSender(sender string) {
+ if len(sender) > view.widestSender {
+ view.widestSender = len(sender)
+ if view.widestSender > view.MaxSenderWidth {
+ view.widestSender = view.MaxSenderWidth
+ }
+ }
+}
+
+const (
+ AppendMessage int = iota
+ PrependMessage
+)
+
+func (view *MessageView) AddMessage(message *types.Message, direction int) {
+ _, messageExists := view.messageIDs[message.ID]
+ if messageExists {
+ return
+ }
+
+ view.updateWidestSender(message.Sender)
+
+ _, _, width, _ := view.GetInnerRect()
+ width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
+ message.CalculateBuffer(width)
+
+ if direction == AppendMessage {
+ if view.ScrollOffset > 0 {
+ view.ScrollOffset += len(message.Buffer)
+ }
+ view.messages = append(view.messages, message)
+ } else if direction == PrependMessage {
+ view.messages = append([]*types.Message{message}, view.messages...)
+ }
+
+ view.messageIDs[message.ID] = true
+ view.recalculateHeight()
+}
+
+func (view *MessageView) recalculateHeight() {
+ _, _, width, height := view.GetInnerRect()
+ if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset {
+ view.firstDisplayMessage = -1
+ view.lastDisplayMessage = -1
+ view.totalHeight = 0
+ prevDate := ""
+ for i := len(view.messages) - 1; i >= 0; i-- {
+ prevTotalHeight := view.totalHeight
+ message := view.messages[i]
+ view.totalHeight += len(message.Buffer)
+ if message.Date != prevDate {
+ if len(prevDate) != 0 {
+ view.totalHeight++
+ }
+ prevDate = message.Date
+ }
+
+ if view.totalHeight < view.ScrollOffset {
+ continue
+ } else if view.firstDisplayMessage == -1 {
+ view.lastDisplayMessage = i
+ view.firstDisplayMessage = i
+ }
+
+ if prevTotalHeight < height+view.ScrollOffset {
+ view.lastDisplayMessage = i
+ }
+ }
+ view.prevScrollOffset = view.ScrollOffset
+ }
+}
+
+func (view *MessageView) PageUp() {
+ _, _, _, height := view.GetInnerRect()
+ view.ScrollOffset += height / 2
+ if view.ScrollOffset > view.totalHeight-height {
+ view.ScrollOffset = view.totalHeight - height + 5
+ }
+}
+
+func (view *MessageView) PageDown() {
+ _, _, _, height := view.GetInnerRect()
+ view.ScrollOffset -= height / 2
+ if view.ScrollOffset < 0 {
+ view.ScrollOffset = 0
+ }
+}
+
+func (view *MessageView) writeLine(screen tcell.Screen, line string, x, y int, color tcell.Color) {
+ offsetX := 0
+ for _, ch := range line {
+ chWidth := runewidth.RuneWidth(ch)
+ if chWidth == 0 {
+ continue
+ }
+
+ for localOffset := 0; localOffset < chWidth; localOffset++ {
+ screen.SetContent(x+offsetX+localOffset, y, ch, nil, tcell.StyleDefault.Foreground(color))
+ }
+ offsetX += chWidth
+ }
+}
+
+func (view *MessageView) writeLineRight(screen tcell.Screen, line string, x, y, maxWidth int, color tcell.Color) {
+ offsetX := maxWidth - runewidth.StringWidth(line)
+ if offsetX < 0 {
+ offsetX = 0
+ }
+ for _, ch := range line {
+ chWidth := runewidth.RuneWidth(ch)
+ if chWidth == 0 {
+ continue
+ }
+
+ for localOffset := 0; localOffset < chWidth; localOffset++ {
+ screen.SetContent(x+offsetX+localOffset, y, ch, nil, tcell.StyleDefault.Foreground(color))
+ }
+ offsetX += chWidth
+ if offsetX > maxWidth {
+ break
+ }
+ }
+}
+
+const (
+ TimestampSenderGap = 1
+ SenderSeparatorGap = 1
+ SenderMessageGap = 3
+)
+
+func (view *MessageView) Draw(screen tcell.Screen) {
+ view.Box.Draw(screen)
+
+ x, y, _, height := view.GetInnerRect()
+ view.recalculateBuffers()
+ view.recalculateHeight()
+
+ if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 {
+ view.writeLine(screen, "It's quite empty in here.", x, y+height, tcell.ColorDefault)
+ return
+ }
+
+ usernameOffsetX := view.TimestampWidth + TimestampSenderGap
+ messageOffsetX := usernameOffsetX + view.widestSender + SenderMessageGap
+ separatorX := x + usernameOffsetX + view.widestSender + SenderSeparatorGap
+ for separatorY := y; separatorY < y+height; separatorY++ {
+ screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault)
+ }
+
+ writeOffset := 0
+ prevDate := ""
+ prevSender := ""
+ prevSenderLine := -1
+ for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- {
+ message := view.messages[i]
+ messageHeight := len(message.Buffer)
+
+ // Show message when the date changes.
+ if message.Date != prevDate {
+ if len(prevDate) > 0 {
+ writeOffset++
+ view.writeLine(
+ screen, fmt.Sprintf("Date changed to %s", prevDate),
+ x+messageOffsetX, y+height-writeOffset, tcell.ColorGreen)
+ }
+ prevDate = message.Date
+ }
+
+ senderAtLine := y + height - writeOffset - messageHeight
+ // The message may be only partially on screen, so we need to make sure the sender
+ // is on screen even when the message is not shown completely.
+ if senderAtLine < y {
+ senderAtLine = y
+ }
+
+ view.writeLine(screen, message.Timestamp, x, senderAtLine, tcell.ColorDefault)
+ view.writeLineRight(screen, message.Sender,
+ x+usernameOffsetX, senderAtLine,
+ view.widestSender, message.SenderColor)
+
+ if message.Sender == prevSender {
+ // Sender is same as previous. We're looping from bottom to top, and we want the
+ // sender name only on the topmost message, so clear out the duplicate sender name
+ // below.
+ view.writeLineRight(screen, strings.Repeat(" ", view.widestSender),
+ x+usernameOffsetX, prevSenderLine,
+ view.widestSender, message.SenderColor)
+ }
+ prevSender = message.Sender
+ prevSenderLine = senderAtLine
+
+ for num, line := range message.Buffer {
+ offsetY := height - messageHeight - writeOffset + num
+ // Only render message if it's within the message view.
+ if offsetY >= 0 {
+ view.writeLine(screen, line, x+messageOffsetX, y+offsetY, tcell.ColorDefault)
+ }
+ }
+ writeOffset += messageHeight
+ }
+}