Add support for loading more history

This commit is contained in:
Tulir Asokan 2018-03-20 12:16:32 +02:00
parent de2a8aee06
commit 3897f23bc4
6 changed files with 194 additions and 148 deletions

View File

@ -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). 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 Basic usage is possible, but expect bugs and missing features.
client (like proper chat history) haven't been implemented.
## Discussion ## Discussion

View File

@ -48,7 +48,6 @@ type MainView interface {
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
AddServiceMessage(roomID string, message string) AddServiceMessage(roomID string, message string)
GetHistory(room string)
ProcessMessageEvent(evt *gomatrix.Event) (*widget.RoomView, *types.Message) ProcessMessageEvent(evt *gomatrix.Event) (*widget.RoomView, *types.Message)
ProcessMembershipEvent(evt *gomatrix.Event, new bool) (*widget.RoomView, *types.Message) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (*widget.RoomView, *types.Message)
} }

View File

@ -74,7 +74,7 @@ func (c *Container) InitClient() error {
c.stop = make(chan bool, 1) 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() go c.Start()
} }
return nil return nil
@ -120,6 +120,11 @@ func (c *Container) Client() *gomatrix.Client {
func (c *Container) UpdateRoomList() { func (c *Container) UpdateRoomList() {
resp, err := c.client.JoinedRooms() resp, err := c.client.JoinedRooms()
if err != nil { 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) debug.Print("Error fetching room list:", err)
return return
} }
@ -127,6 +132,11 @@ func (c *Container) UpdateRoomList() {
c.ui.MainView().SetRooms(resp.JoinedRooms) c.ui.MainView().SetRooms(resp.JoinedRooms)
} }
func (c *Container) OnLogout() {
c.Stop()
c.ui.SetView(ifc.ViewLogin)
}
func (c *Container) OnLogin() { func (c *Container) OnLogin() {
c.client.Store = c.config.Session c.client.Store = c.config.Session
@ -145,6 +155,10 @@ func (c *Container) Start() {
c.ui.SetView(ifc.ViewMain) c.ui.SetView(ifc.ViewMain)
c.OnLogin() c.OnLogin()
if c.client == nil {
return
}
debug.Print("Starting sync...") debug.Print("Starting sync...")
c.running = true c.running = true
for { for {
@ -167,6 +181,7 @@ func (c *Container) HandleMessage(evt *gomatrix.Event) {
room, message := c.ui.MainView().ProcessMessageEvent(evt) room, message := c.ui.MainView().ProcessMessageEvent(evt)
if room != nil { if room != nil {
room.AddMessage(message, widget.AppendMessage) room.AddMessage(message, widget.AppendMessage)
c.ui.Render()
} }
} }
@ -184,6 +199,7 @@ func (c *Container) HandleMembership(evt *gomatrix.Event) {
room.UpdateUserList() room.UpdateUserList()
room.AddMessage(message, widget.AppendMessage) room.AddMessage(message, widget.AppendMessage)
c.ui.Render()
} }
} }

View File

@ -175,7 +175,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) {
func (view *MainView) InputCapture(key *tcell.EventKey) *tcell.EventKey { func (view *MainView) InputCapture(key *tcell.EventKey) *tcell.EventKey {
k := key.Key() k := key.Key()
if key.Modifiers() == tcell.ModCtrl { if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt {
if k == tcell.KeyDown { if k == tcell.KeyDown {
view.SwitchRoom(view.currentRoomIndex + 1) view.SwitchRoom(view.currentRoomIndex + 1)
view.roomList.SetCurrentItem(view.currentRoomIndex) view.roomList.SetCurrentItem(view.currentRoomIndex)
@ -185,13 +185,18 @@ func (view *MainView) InputCapture(key *tcell.EventKey) *tcell.EventKey {
} else { } else {
return key 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() msgView := view.rooms[view.CurrentRoomID()].MessageView()
if k == tcell.KeyPgUp { if k == tcell.KeyPgUp || k == tcell.KeyUp {
msgView.PageUp() if msgView.IsAtTop() {
go view.LoadMoreHistory(view.CurrentRoomID())
} else {
msgView.MoveUp(k == tcell.KeyPgUp)
}
} else { } else {
msgView.PageDown() msgView.MoveDown(k == tcell.KeyPgDn)
} }
view.parent.Render()
} else { } else {
return key return key
} }
@ -225,11 +230,11 @@ func (view *MainView) addRoom(index int, room string) {
view.SwitchRoom(index) view.SwitchRoom(index)
}) })
if !view.roomView.HasPage(room) { if !view.roomView.HasPage(room) {
roomView := widget.NewRoomView(view, roomStore) roomView := widget.NewRoomView(roomStore)
view.rooms[room] = roomView view.rooms[room] = roomView
view.roomView.AddPage(room, roomView, true, false) view.roomView.AddPage(room, roomView, true, false)
roomView.UpdateUserList() 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.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...)
view.roomView.RemovePage(room) view.roomView.RemovePage(room)
delete(view.rooms, room) delete(view.rooms, room)
view.Render() view.parent.Render()
} }
func (view *MainView) SetRooms(rooms []string) { 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) { func (view *MainView) AddServiceMessage(room, message string) {
roomView, ok := view.rooms[room] roomView, ok := view.rooms[room]
if ok { if ok {
messageView := roomView.MessageView() message := roomView.NewMessage("", "*", message, time.Now())
message := messageView.NewMessage("", "*", message, time.Now()) roomView.AddMessage(message, widget.AppendMessage)
messageView.AddMessage(message, widget.AppendMessage)
view.parent.Render() view.parent.Render()
} }
} }
func (view *MainView) Render() { func (view *MainView) LoadMoreHistory(room string) {
view.parent.Render() 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] 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 { if err != nil {
view.AddServiceMessage(room, "Failed to fetch history")
debug.Print("Failed to fetch history for", roomView.Room.ID, err) debug.Print("Failed to fetch history for", roomView.Room.ID, err)
return return
} }
roomView.Room.PrevBatch = prevBatch
for _, evt := range history { for _, evt := range history {
var room *widget.RoomView var room *widget.RoomView
var message *types.Message var message *types.Message
@ -324,6 +357,7 @@ func (view *MainView) GetHistory(room string) {
room.AddMessage(message, widget.PrependMessage) room.AddMessage(message, widget.PrependMessage)
} }
} }
view.parent.Render()
} }
func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *widget.RoomView, message *types.Message) { func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *widget.RoomView, message *types.Message) {

View File

@ -18,7 +18,6 @@ package widget
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -36,17 +35,17 @@ type MessageView struct {
TimestampFormat string TimestampFormat string
TimestampWidth int TimestampWidth int
Separator rune Separator rune
LoadingMessages bool
widestSender int widestSender int
prevWidth int prevWidth int
prevHeight int prevHeight int
prevScrollOffset int
firstDisplayMessage int
lastDisplayMessage int
totalHeight int
messageIDs map[string]bool messageIDs map[string]bool
messages []*types.Message messages []*types.Message
metaBuffer []*types.Message
textBuffer []string
} }
func NewMessageView() *MessageView { func NewMessageView() *MessageView {
@ -61,14 +60,12 @@ func NewMessageView() *MessageView {
messages: make([]*types.Message, 0), messages: make([]*types.Message, 0),
messageIDs: make(map[string]bool), messageIDs: make(map[string]bool),
textBuffer: make([]string, 0),
metaBuffer: make([]*types.Message, 0),
widestSender: 5, widestSender: 5,
prevWidth: -1, prevWidth: -1,
prevHeight: -1, prevHeight: -1,
prevScrollOffset: -1,
firstDisplayMessage: -1,
lastDisplayMessage: -1,
totalHeight: -1,
} }
} }
@ -79,17 +76,6 @@ func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time
GetHashColor(sender)) 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) { func (view *MessageView) updateWidestSender(sender string) {
if len(sender) > view.widestSender { if len(sender) > view.widestSender {
view.widestSender = len(sender) view.widestSender = len(sender)
@ -100,7 +86,7 @@ func (view *MessageView) updateWidestSender(sender string) {
} }
const ( const (
AppendMessage int = iota AppendMessage = iota
PrependMessage PrependMessage
) )
@ -126,53 +112,87 @@ func (view *MessageView) AddMessage(message *types.Message, direction int) {
} }
view.messageIDs[message.ID] = true view.messageIDs[message.ID] = true
view.recalculateHeight() view.appendBuffer(message)
} }
func (view *MessageView) recalculateHeight() { func (view *MessageView) recalculateMessageBuffers() {
_, _, width, height := view.GetInnerRect() _, _, width, _ := view.GetInnerRect()
if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset { width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
view.firstDisplayMessage = -1 if width != view.prevWidth {
view.lastDisplayMessage = -1 for _, message := range view.messages {
view.totalHeight = 0 message.CalculateBuffer(width)
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 view.prevWidth = width
} }
} }
func (view *MessageView) PageUp() { func (view *MessageView) appendBuffer(message *types.Message) {
_, _, _, height := view.GetInnerRect() if len(view.metaBuffer) > 0 {
view.ScrollOffset += height / 2 prevMeta := view.metaBuffer[len(view.metaBuffer)-1]
if view.ScrollOffset > view.totalHeight-height { if prevMeta != nil && prevMeta.Date != message.Date {
view.ScrollOffset = view.totalHeight - height + 5 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() _, _, _, 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 { if view.ScrollOffset < 0 {
view.ScrollOffset = 0 view.ScrollOffset = 0
} }
@ -224,10 +244,10 @@ func (view *MessageView) Draw(screen tcell.Screen) {
view.Box.Draw(screen) view.Box.Draw(screen)
x, y, _, height := view.GetInnerRect() x, y, _, height := view.GetInnerRect()
view.recalculateBuffers() view.recalculateMessageBuffers()
view.recalculateHeight() 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) view.writeLine(screen, "It's quite empty in here.", x, y+height, tcell.ColorDefault)
return return
} }
@ -239,55 +259,37 @@ func (view *MessageView) Draw(screen tcell.Screen) {
screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault) screen.SetContent(separatorX, separatorY, view.Separator, nil, tcell.StyleDefault)
} }
writeOffset := 0 var prevMeta *types.Message
prevDate := "" var prevSender string
prevSender := "" indexOffset := len(view.textBuffer) - view.ScrollOffset - height
prevSenderLine := -1 if indexOffset <= -PaddingAtTop {
for i := view.firstDisplayMessage; i >= view.lastDisplayMessage; i-- { message := "Scroll up to load more messages."
message := view.messages[i] if view.LoadingMessages {
messageHeight := len(message.Buffer) message = "Loading more messages..."
}
// Show message when the date changes. view.writeLine(screen, message, x+messageOffsetX, y, tcell.ColorGreen)
if message.Date != prevDate { }
if len(prevDate) > 0 { for line := 0; line < height; line++ {
writeOffset++ index := indexOffset + line
view.writeLine( if index < 0 {
screen, fmt.Sprintf("Date changed to %s", prevDate), continue
x+messageOffsetX, y+height-writeOffset, tcell.ColorGreen) } 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
} }
view.writeLine(screen, text, x+messageOffsetX, y+line, tcell.ColorDefault)
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 File

@ -19,6 +19,7 @@ package widget
import ( import (
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -27,10 +28,6 @@ import (
"maunium.net/go/tview" "maunium.net/go/tview"
) )
type Renderable interface {
Render()
}
type RoomView struct { type RoomView struct {
*tview.Box *tview.Box
@ -40,18 +37,18 @@ type RoomView struct {
userList *tview.TextView userList *tview.TextView
Room *rooms.Room Room *rooms.Room
parent Renderable FetchHistoryLock *sync.Mutex
} }
func NewRoomView(parent Renderable, room *rooms.Room) *RoomView { func NewRoomView(room *rooms.Room) *RoomView {
view := &RoomView{ view := &RoomView{
Box: tview.NewBox(), Box: tview.NewBox(),
topic: tview.NewTextView(), topic: tview.NewTextView(),
content: NewMessageView(), content: NewMessageView(),
status: tview.NewTextView(), status: tview.NewTextView(),
userList: tview.NewTextView(), userList: tview.NewTextView(),
Room: room, FetchHistoryLock: &sync.Mutex{},
parent: parent, Room: room,
} }
view.topic. view.topic.
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). 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) { func (view *RoomView) AddMessage(message *types.Message, direction int) {
view.content.AddMessage(message, direction) view.content.AddMessage(message, direction)
view.parent.Render()
} }