From 3897f23bc4dd24cf54ba39fad544d10feb273120 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 20 Mar 2018 12:16:32 +0200 Subject: [PATCH] Add support for loading more history --- README.md | 3 +- interface/ui.go | 1 - matrix/matrix.go | 18 ++- ui/view-main.go | 64 ++++++++--- ui/widget/message-view.go | 232 +++++++++++++++++++------------------- ui/widget/room-view.go | 24 ++-- 6 files changed, 194 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 854f65b..80ed3c6 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ A terminal Matrix client written in Go using [gomatrix](https://github.com/matrix-org/gomatrix) and [tview](https://github.com/rivo/tview). -Basic usage is possible, but many of the features you'd expect from a Matrix -client (like proper chat history) haven't been implemented. +Basic usage is possible, but expect bugs and missing features. ## Discussion diff --git a/interface/ui.go b/interface/ui.go index 406aa2f..088c2f4 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -48,7 +48,6 @@ type MainView interface { SetTyping(roomID string, users []string) AddServiceMessage(roomID string, message string) - GetHistory(room string) ProcessMessageEvent(evt *gomatrix.Event) (*widget.RoomView, *types.Message) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (*widget.RoomView, *types.Message) } diff --git a/matrix/matrix.go b/matrix/matrix.go index 564dc81..f20825c 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -74,7 +74,7 @@ func (c *Container) InitClient() error { c.stop = make(chan bool, 1) - if c.config.Session != nil { + if c.config.Session != nil && len(c.config.Session.AccessToken) > 0 { go c.Start() } return nil @@ -120,6 +120,11 @@ func (c *Container) Client() *gomatrix.Client { func (c *Container) UpdateRoomList() { resp, err := c.client.JoinedRooms() if err != nil { + respErr, _ := err.(gomatrix.HTTPError).WrappedError.(gomatrix.RespError) + if respErr.ErrCode == "M_UNKNOWN_TOKEN" { + c.OnLogout() + return + } debug.Print("Error fetching room list:", err) return } @@ -127,6 +132,11 @@ func (c *Container) UpdateRoomList() { c.ui.MainView().SetRooms(resp.JoinedRooms) } +func (c *Container) OnLogout() { + c.Stop() + c.ui.SetView(ifc.ViewLogin) +} + func (c *Container) OnLogin() { c.client.Store = c.config.Session @@ -145,6 +155,10 @@ func (c *Container) Start() { c.ui.SetView(ifc.ViewMain) c.OnLogin() + if c.client == nil { + return + } + debug.Print("Starting sync...") c.running = true for { @@ -167,6 +181,7 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) { room, message := c.ui.MainView().ProcessMessageEvent(evt) if room != nil { room.AddMessage(message, widget.AppendMessage) + c.ui.Render() } } @@ -184,6 +199,7 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) { room.UpdateUserList() room.AddMessage(message, widget.AppendMessage) + c.ui.Render() } } diff --git a/ui/view-main.go b/ui/view-main.go index a2848e8..2cb8d60 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -175,7 +175,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) { func (view *MainView) InputCapture(key *tcell.EventKey) *tcell.EventKey { k := key.Key() - if key.Modifiers() == tcell.ModCtrl { + if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt { if k == tcell.KeyDown { view.SwitchRoom(view.currentRoomIndex + 1) view.roomList.SetCurrentItem(view.currentRoomIndex) @@ -185,13 +185,18 @@ func (view *MainView) InputCapture(key *tcell.EventKey) *tcell.EventKey { } else { return key } - } else if k == tcell.KeyPgUp || k == tcell.KeyPgDn { + } else if k == tcell.KeyPgUp || k == tcell.KeyPgDn || k == tcell.KeyUp || k == tcell.KeyDown { msgView := view.rooms[view.CurrentRoomID()].MessageView() - if k == tcell.KeyPgUp { - msgView.PageUp() + if k == tcell.KeyPgUp || k == tcell.KeyUp { + if msgView.IsAtTop() { + go view.LoadMoreHistory(view.CurrentRoomID()) + } else { + msgView.MoveUp(k == tcell.KeyPgUp) + } } else { - msgView.PageDown() + msgView.MoveDown(k == tcell.KeyPgDn) } + view.parent.Render() } else { return key } @@ -225,11 +230,11 @@ func (view *MainView) addRoom(index int, room string) { view.SwitchRoom(index) }) if !view.roomView.HasPage(room) { - roomView := widget.NewRoomView(view, roomStore) + roomView := widget.NewRoomView(roomStore) view.rooms[room] = roomView view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() - view.GetHistory(room) + go view.LoadInitialHistory(room) } } @@ -269,7 +274,7 @@ func (view *MainView) RemoveRoom(room string) { view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...) view.roomView.RemovePage(room) delete(view.rooms, room) - view.Render() + view.parent.Render() } func (view *MainView) SetRooms(rooms []string) { @@ -294,24 +299,52 @@ func (view *MainView) SetTyping(room string, users []string) { func (view *MainView) AddServiceMessage(room, message string) { roomView, ok := view.rooms[room] if ok { - messageView := roomView.MessageView() - message := messageView.NewMessage("", "*", message, time.Now()) - messageView.AddMessage(message, widget.AppendMessage) + message := roomView.NewMessage("", "*", message, time.Now()) + roomView.AddMessage(message, widget.AppendMessage) view.parent.Render() } } -func (view *MainView) Render() { - view.parent.Render() +func (view *MainView) LoadMoreHistory(room string) { + view.UpdateLogs(room, false) } -func (view *MainView) GetHistory(room string) { +func (view *MainView) LoadInitialHistory(room string) { + view.UpdateLogs(room, true) +} + +func (view *MainView) UpdateLogs(room string, initial bool) { + defer view.gmx.Recover() roomView := view.rooms[room] - history, _, err := view.matrix.GetHistory(roomView.Room.ID, view.config.Session.NextBatch, 50) + + batch := roomView.Room.PrevBatch + lockTime := time.Now().Unix() + 1 + + roomView.FetchHistoryLock.Lock() + roomView.MessageView().LoadingMessages = true + defer func() { + roomView.FetchHistoryLock.Unlock() + roomView.MessageView().LoadingMessages = false + }() + + // There's no clean way to try to lock a mutex, so we just check if we still + // want to continue after we get the lock. This function should always be ran + // in a goroutine, so the blocking doesn't matter. + if time.Now().Unix() >= lockTime || batch != roomView.Room.PrevBatch { + return + } + + if initial { + batch = view.config.Session.NextBatch + } + debug.Print("Loading history for", room, "starting from", batch, "(initial:", initial, ")") + history, prevBatch, err := view.matrix.GetHistory(roomView.Room.ID, batch, 50) if err != nil { + view.AddServiceMessage(room, "Failed to fetch history") debug.Print("Failed to fetch history for", roomView.Room.ID, err) return } + roomView.Room.PrevBatch = prevBatch for _, evt := range history { var room *widget.RoomView var message *types.Message @@ -324,6 +357,7 @@ func (view *MainView) GetHistory(room string) { room.AddMessage(message, widget.PrependMessage) } } + view.parent.Render() } func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *widget.RoomView, message *types.Message) { diff --git a/ui/widget/message-view.go b/ui/widget/message-view.go index 3503a5f..953dd7d 100644 --- a/ui/widget/message-view.go +++ b/ui/widget/message-view.go @@ -18,7 +18,6 @@ package widget import ( "fmt" - "strings" "time" "github.com/gdamore/tcell" @@ -36,17 +35,17 @@ type MessageView struct { TimestampFormat string TimestampWidth int Separator rune + LoadingMessages bool - widestSender int - prevWidth int - prevHeight int - prevScrollOffset int - firstDisplayMessage int - lastDisplayMessage int - totalHeight int + widestSender int + prevWidth int + prevHeight int messageIDs map[string]bool messages []*types.Message + + metaBuffer []*types.Message + textBuffer []string } func NewMessageView() *MessageView { @@ -61,14 +60,12 @@ func NewMessageView() *MessageView { messages: make([]*types.Message, 0), messageIDs: make(map[string]bool), + textBuffer: make([]string, 0), + metaBuffer: make([]*types.Message, 0), - widestSender: 5, - prevWidth: -1, - prevHeight: -1, - prevScrollOffset: -1, - firstDisplayMessage: -1, - lastDisplayMessage: -1, - totalHeight: -1, + widestSender: 5, + prevWidth: -1, + prevHeight: -1, } } @@ -79,17 +76,6 @@ func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time 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) @@ -100,7 +86,7 @@ func (view *MessageView) updateWidestSender(sender string) { } const ( - AppendMessage int = iota + AppendMessage = iota PrependMessage ) @@ -126,53 +112,87 @@ func (view *MessageView) AddMessage(message *types.Message, direction int) { } view.messageIDs[message.ID] = true - view.recalculateHeight() + view.appendBuffer(message) } -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 - } +func (view *MessageView) recalculateMessageBuffers() { + _, _, width, _ := view.GetInnerRect() + width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap + if width != view.prevWidth { + for _, message := range view.messages { + message.CalculateBuffer(width) } - view.prevScrollOffset = view.ScrollOffset + view.prevWidth = width } } -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) appendBuffer(message *types.Message) { + 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, nil) + } + } + + view.textBuffer = append(view.textBuffer, message.Buffer...) + for range message.Buffer { + view.metaBuffer = append(view.metaBuffer, message) } } -func (view *MessageView) PageDown() { +func (view *MessageView) recalculateBuffer() { + _, _, width, height := view.GetInnerRect() + view.textBuffer = make([]string, 0) + view.metaBuffer = make([]*types.Message, 0) + + if height != view.prevHeight || width != view.prevWidth { + for _, message := range view.messages { + view.appendBuffer(message) + } + view.prevHeight = height + } +} + +const PaddingAtTop = 5 + +func (view *MessageView) MoveUp(page bool) { _, _, _, height := view.GetInnerRect() - view.ScrollOffset -= height / 2 + + totalHeight := len(view.textBuffer) + if view.ScrollOffset >= totalHeight-height { + // If the user is at the top and presses page up again, add a bit of blank space. + if page { + view.ScrollOffset = totalHeight - height + PaddingAtTop + } else if view.ScrollOffset < totalHeight-height+PaddingAtTop { + view.ScrollOffset++ + } + return + } + + if page { + view.ScrollOffset += height / 2 + } else { + view.ScrollOffset++ + } + if view.ScrollOffset > totalHeight-height { + view.ScrollOffset = totalHeight - height + } +} + +func (view *MessageView) IsAtTop() bool { + _, _, _, height := view.GetInnerRect() + totalHeight := len(view.textBuffer) + return view.ScrollOffset >= totalHeight-height+PaddingAtTop +} + +func (view *MessageView) MoveDown(page bool) { + _, _, _, height := view.GetInnerRect() + if page { + view.ScrollOffset -= height / 2 + } else { + view.ScrollOffset-- + } if view.ScrollOffset < 0 { view.ScrollOffset = 0 } @@ -224,10 +244,10 @@ func (view *MessageView) Draw(screen tcell.Screen) { view.Box.Draw(screen) x, y, _, height := view.GetInnerRect() - view.recalculateBuffers() - view.recalculateHeight() + view.recalculateMessageBuffers() + view.recalculateBuffer() - if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 { + if len(view.textBuffer) == 0 { view.writeLine(screen, "It's quite empty in here.", x, y+height, tcell.ColorDefault) return } @@ -239,55 +259,37 @@ func (view *MessageView) Draw(screen tcell.Screen) { 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) + var prevMeta *types.Message + var prevSender string + indexOffset := len(view.textBuffer) - view.ScrollOffset - height + if indexOffset <= -PaddingAtTop { + message := "Scroll up to load more messages." + if view.LoadingMessages { + message = "Loading more messages..." + } + view.writeLine(screen, message, x+messageOffsetX, y, tcell.ColorGreen) + } + for line := 0; line < height; line++ { + index := indexOffset + line + if index < 0 { + continue + } else if index > len(view.textBuffer) { + break + } + text, meta := view.textBuffer[index], view.metaBuffer[index] + if meta != prevMeta { + if meta != nil { + view.writeLine(screen, meta.Timestamp, x, y+line, tcell.ColorDefault) + if meta.Sender != prevSender { + view.writeLineRight( + screen, meta.Sender, + x+usernameOffsetX, y+line, + view.widestSender, meta.SenderColor) + prevSender = meta.Sender + } } - prevDate = message.Date + prevMeta = meta } - - 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 + view.writeLine(screen, text, x+messageOffsetX, y+line, tcell.ColorDefault) } } diff --git a/ui/widget/room-view.go b/ui/widget/room-view.go index eeab7b2..1fb19c7 100644 --- a/ui/widget/room-view.go +++ b/ui/widget/room-view.go @@ -19,6 +19,7 @@ package widget import ( "fmt" "strings" + "sync" "time" "github.com/gdamore/tcell" @@ -27,10 +28,6 @@ import ( "maunium.net/go/tview" ) -type Renderable interface { - Render() -} - type RoomView struct { *tview.Box @@ -40,18 +37,18 @@ type RoomView struct { userList *tview.TextView Room *rooms.Room - parent Renderable + FetchHistoryLock *sync.Mutex } -func NewRoomView(parent Renderable, room *rooms.Room) *RoomView { +func NewRoomView(room *rooms.Room) *RoomView { view := &RoomView{ - Box: tview.NewBox(), - topic: tview.NewTextView(), - content: NewMessageView(), - status: tview.NewTextView(), - userList: tview.NewTextView(), - Room: room, - parent: parent, + Box: tview.NewBox(), + topic: tview.NewTextView(), + content: NewMessageView(), + status: tview.NewTextView(), + userList: tview.NewTextView(), + FetchHistoryLock: &sync.Mutex{}, + Room: room, } view.topic. SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). @@ -148,5 +145,4 @@ func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) * func (view *RoomView) AddMessage(message *types.Message, direction int) { view.content.AddMessage(message, direction) - view.parent.Render() }