diff options
Diffstat (limited to 'ui/widget/message-view.go')
-rw-r--r-- | ui/widget/message-view.go | 293 |
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 + } +} |