From 0509b195625c959a7b5556e3baae4f869c4d62f6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 18 Mar 2018 17:34:42 +0200 Subject: [PATCH] Move syncer/room store changes from gomatrix fork to here, refactor and improve stuff --- debug.go | 2 +- gomuks.go | 3 +- matrix.go | 87 ++++++++------------- message-view.go | 202 +++++++++++++++++++++++++++++++++--------------- room-view.go | 13 ++-- room.go | 175 +++++++++++++++++++++++++++++++++++++++++ session.go | 28 +++++-- sync.go | 125 ++++++++++++++++++++++++++++++ view-main.go | 112 ++++++++++++++++++++++++--- 9 files changed, 602 insertions(+), 145 deletions(-) create mode 100644 room.go create mode 100644 sync.go diff --git a/debug.go b/debug.go index b91ef60..15aac60 100644 --- a/debug.go +++ b/debug.go @@ -22,7 +22,7 @@ import ( "maunium.net/go/tview" ) -const DebugPaneHeight = 40 +const DebugPaneHeight = 35 type DebugPrinter interface { Printf(text string, args ...interface{}) diff --git a/gomuks.go b/gomuks.go index 237d3e9..5fa0a8a 100644 --- a/gomuks.go +++ b/gomuks.go @@ -126,5 +126,6 @@ func (gmx *gomuks) UI() *GomuksUI { } func main() { - NewGomuks(true).Start() + debug := os.Getenv("DEBUG") + NewGomuks(len(debug) > 0).Start() } diff --git a/matrix.go b/matrix.go index 4dca289..ea1c5c6 100644 --- a/matrix.go +++ b/matrix.go @@ -113,23 +113,29 @@ func (c *MatrixContainer) Stop() { func (c *MatrixContainer) UpdateRoomList() { rooms, err := c.client.JoinedRooms() if err != nil { - c.debug.Print(err) + c.debug.Print("Error fetching room list:", err) + return } c.ui.MainView().SetRoomList(rooms.JoinedRooms) } -func (c *MatrixContainer) Start() { - defer c.gmx.Recover() +func (c *MatrixContainer) OnLogin() { c.client.Store = c.config.Session - syncer := gomatrix.NewDefaultSyncer(c.config.Session.MXID, c.config.Session) + syncer := NewGomuksSyncer(c.config.Session) syncer.OnEventType("m.room.message", c.HandleMessage) syncer.OnEventType("m.room.member", c.HandleMembership) syncer.OnEventType("m.typing", c.HandleTyping) c.client.Syncer = syncer c.UpdateRoomList() +} + +func (c *MatrixContainer) Start() { + defer c.gmx.Recover() + + c.OnLogin() c.debug.Print("Starting sync...") c.running = true @@ -151,65 +157,26 @@ func (c *MatrixContainer) Start() { } func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) { - message, _ := evt.Content["body"].(string) - - room := c.ui.MainView().GetRoom(evt.RoomID) + room, message := c.ui.MainView().ProcessMessageEvent(evt) if room != nil { - room.AddMessage(evt.ID, evt.Sender, message, unixToTime(evt.Timestamp)) + room.AddMessage(message, AppendMessage) } } -func unixToTime(unix int64) time.Time { - timestamp := time.Now() - if unix != 0 { - timestamp = time.Unix(unix/1000, unix%1000*1000) - } - return timestamp -} - func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) { - membership, _ := evt.Content["membership"].(string) - if evt.StateKey != nil && *evt.StateKey == c.config.Session.MXID { - prevMembership := "leave" - if evt.Unsigned.PrevContent != nil { - prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) - } - if membership == prevMembership { - return - } - if membership == "join" { - c.ui.MainView().AddRoom(evt.RoomID) - } else if membership == "leave" { - c.ui.MainView().RemoveRoom(evt.RoomID) - } + const Hour = 1 * 60 * 60 * 1000 + if evt.Unsigned.Age > Hour { return } - room := c.ui.MainView().GetRoom(evt.RoomID) - - // TODO this shouldn't be necessary - room.room.UpdateState(evt) + room, message := c.ui.MainView().ProcessMembershipEvent(evt, true) if room != nil { - var message, sender string - if membership == "invite" { - sender = "---" - message = fmt.Sprintf("%s invited %s.", evt.Sender, *evt.StateKey) - } else if membership == "join" { - sender = "-->" - message = fmt.Sprintf("%s joined the room.", *evt.StateKey) - } else if membership == "leave" { - sender = "<--" - if evt.Sender != *evt.StateKey { - reason, _ := evt.Content["reason"].(string) - message = fmt.Sprintf("%s kicked %s: %s", evt.Sender, *evt.StateKey, reason) - } else { - message = fmt.Sprintf("%s left the room.", *evt.StateKey) - } - } else { - return - } + // TODO this shouldn't be necessary + room.room.UpdateState(evt) + // TODO This should probably also be in a different place room.UpdateUserList() - room.AddMessage(evt.ID, sender, message, unixToTime(evt.Timestamp)) + + room.AddMessage(message, AppendMessage) } } @@ -268,14 +235,22 @@ func (c *MatrixContainer) getState(roomID string) []*gomatrix.Event { content := make([]*gomatrix.Event, 0) err := c.client.StateEvent(roomID, "", "", &content) if err != nil { - c.debug.Print(err) + c.debug.Print("Error getting state of", roomID, err) return nil } return content } -func (c *MatrixContainer) GetRoom(roomID string) *gomatrix.Room { - room := c.config.Session.LoadRoom(roomID) +func (c *MatrixContainer) GetHistory(roomID, prevBatch string, limit int) ([]gomatrix.Event, string, error) { + resp, err := c.client.Messages(roomID, prevBatch, "", 'b', limit) + if err != nil { + return nil, "", err + } + return resp.Chunk, resp.End, nil +} + +func (c *MatrixContainer) GetRoom(roomID string) *Room { + room := c.config.Session.GetRoom(roomID) if room != nil && len(room.State) == 0 { events := c.getState(room.ID) if events != nil { diff --git a/message-view.go b/message-view.go index 6029f6a..d3d2df9 100644 --- a/message-view.go +++ b/message-view.go @@ -17,6 +17,7 @@ package main import ( + "fmt" "regexp" "strings" "time" @@ -27,16 +28,27 @@ import ( ) type Message struct { - ID string - Sender string - Text string - Timestamp string - RenderSender bool + ID string + Sender string + Text string + Timestamp string + Date string buffer []string senderColor tcell.Color } +func NewMessage(id, sender, text, timestamp, date string, senderColor tcell.Color) *Message { + return &Message{ + ID: id, + Sender: sender, + Text: text, + Timestamp: timestamp, + Date: date, + senderColor: senderColor, + } +} + var ( boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") spacePattern = regexp.MustCompile(`\s+`) @@ -80,6 +92,7 @@ type MessageView struct { ScrollOffset int MaxSenderWidth int + DateFormat string TimestampFormat string TimestampWidth int Separator rune @@ -92,18 +105,23 @@ type MessageView struct { lastDisplayMessage int totalHeight int - messages []*Message + messageIDs map[string]bool + messages []*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([]*Message, 0), + messageIDs: make(map[string]bool), + widestSender: 5, prevWidth: -1, prevHeight: -1, @@ -114,62 +132,94 @@ func NewMessageView() *MessageView { } } -func (view *MessageView) recalculateBuffers(width int) { - width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap - for _, message := range view.messages { - message.calculateBuffer(width) - } - view.prevWidth = width +func (view *MessageView) NewMessage(id, sender, text string, timestamp time.Time) *Message { + return NewMessage(id, sender, text, + timestamp.Format(view.TimestampFormat), + timestamp.Format(view.DateFormat), + getColor(sender)) } -func (view *MessageView) AddMessage(id, sender, text string, timestamp time.Time) { +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 } } - message := &Message{ - ID: id, - Sender: sender, - RenderSender: true, - Text: text, - Timestamp: timestamp.Format(view.TimestampFormat), - senderColor: getColor(sender), - } - _, _, width, height := view.GetInnerRect() - width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap - message.calculateBuffer(width) - if view.ScrollOffset > 0 { - view.ScrollOffset += len(message.buffer) - } - if len(view.messages) > 0 && view.messages[len(view.messages)-1].Sender == message.Sender { - message.RenderSender = false - } - view.messages = append(view.messages, message) - view.recalculateHeight(height) } -func (view *MessageView) recalculateHeight(height int) { - view.firstDisplayMessage = -1 - view.lastDisplayMessage = -1 - view.totalHeight = 0 - for i := len(view.messages) - 1; i >= 0; i-- { - prevTotalHeight := view.totalHeight - view.totalHeight += len(view.messages[i].buffer) +const ( + AppendMessage int = iota + PrependMessage +) - 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) AddMessage(message *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([]*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 } - view.prevScrollOffset = view.ScrollOffset } func (view *MessageView) PageUp() { @@ -230,43 +280,67 @@ const ( func (view *MessageView) Draw(screen tcell.Screen) { view.Box.Draw(screen) - x, y, width, height := view.GetInnerRect() - if width != view.prevWidth { - view.recalculateBuffers(width) - } - if height != view.prevHeight || width != view.prevWidth || view.ScrollOffset != view.prevScrollOffset { - view.recalculateHeight(height) + 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) } - if view.firstDisplayMessage == -1 || view.lastDisplayMessage == -1 { - return - } - 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) - if message.RenderSender || i == view.lastDisplayMessage { - view.writeLineRight(screen, message.Sender, - x+usernameOffsetX, senderAtLine, + 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) } diff --git a/room-view.go b/room-view.go index 4b6feac..5710788 100644 --- a/room-view.go +++ b/room-view.go @@ -24,7 +24,6 @@ import ( "time" "github.com/gdamore/tcell" - "maunium.net/go/gomatrix" "maunium.net/go/tview" ) @@ -35,7 +34,7 @@ type RoomView struct { content *MessageView status *tview.TextView userList *tview.TextView - room *gomatrix.Room + room *Room parent *MainView } @@ -52,7 +51,7 @@ func init() { sort.Sort(sort.StringSlice(colorNames)) } -func NewRoomView(parent *MainView, room *gomatrix.Room) *RoomView { +func NewRoomView(parent *MainView, room *Room) *RoomView { view := &RoomView{ Box: tview.NewBox(), topic: tview.NewTextView(), @@ -166,11 +165,15 @@ func (view *RoomView) UpdateUserList() { } } -func (view *RoomView) AddMessage(id, sender, message string, timestamp time.Time) { +func (view *RoomView) NewMessage(id, sender, text string, timestamp time.Time) *Message { member := view.room.GetMember(sender) if member != nil { sender = member.DisplayName } - view.content.AddMessage(id, sender, message, timestamp) + return view.content.NewMessage(id, sender, text, timestamp) +} + +func (view *RoomView) AddMessage(message *Message, direction int) { + view.content.AddMessage(message, direction) view.parent.Render() } diff --git a/room.go b/room.go new file mode 100644 index 0000000..19c6865 --- /dev/null +++ b/room.go @@ -0,0 +1,175 @@ +// 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 . + +package main + +import ( + "maunium.net/go/gomatrix" +) + +// Room represents a single Matrix room. +type Room struct { + *gomatrix.Room + + PrevBatch string + memberCache map[string]*RoomMember + nameCache string + topicCache string +} + +// UpdateState updates the room's current state with the given Event. This will clobber events based +// on the type/state_key combination. +func (room *Room) UpdateState(event *gomatrix.Event) { + _, exists := room.State[event.Type] + if !exists { + room.State[event.Type] = make(map[string]*gomatrix.Event) + } + switch event.Type { + case "m.room.member": + room.memberCache = nil + case "m.room.name": + case "m.room.canonical_alias": + case "m.room.alias": + room.nameCache = "" + case "m.room.topic": + room.topicCache = "" + } + room.State[event.Type][*event.StateKey] = event +} + +// GetStateEvent returns the state event for the given type/state_key combo, or nil. +func (room *Room) GetStateEvent(eventType string, stateKey string) *gomatrix.Event { + stateEventMap, _ := room.State[eventType] + event, _ := stateEventMap[stateKey] + return event +} + +// GetStateEvents returns the state events for the given type. +func (room *Room) GetStateEvents(eventType string) map[string]*gomatrix.Event { + stateEventMap, _ := room.State[eventType] + return stateEventMap +} + +// GetTopic returns the topic of the room. +func (room *Room) GetTopic() string { + if len(room.topicCache) == 0 { + topicEvt := room.GetStateEvent("m.room.topic", "") + if topicEvt != nil { + room.topicCache, _ = topicEvt.Content["topic"].(string) + } + } + return room.topicCache +} + +// GetTitle returns the display title of the room. +func (room *Room) GetTitle() string { + if len(room.nameCache) == 0 { + nameEvt := room.GetStateEvent("m.room.name", "") + if nameEvt != nil { + room.nameCache, _ = nameEvt.Content["name"].(string) + } + } + if len(room.nameCache) == 0 { + canonicalAliasEvt := room.GetStateEvent("m.room.canonical_alias", "") + if canonicalAliasEvt != nil { + room.nameCache, _ = canonicalAliasEvt.Content["alias"].(string) + } + } + if len(room.nameCache) == 0 { + // TODO the spec says clients should not use m.room.aliases for room names. + // However, Riot also uses m.room.aliases, so this is here now. + aliasEvents := room.GetStateEvents("m.room.aliases") + for _, event := range aliasEvents { + aliases, _ := event.Content["aliases"].([]interface{}) + if len(aliases) > 0 { + room.nameCache, _ = aliases[0].(string) + break + } + } + } + if len(room.nameCache) == 0 { + // TODO follow other title rules in spec + room.nameCache = room.ID + } + return room.nameCache +} + +type RoomMember struct { + UserID string `json:"-"` + Membership string `json:"membership"` + DisplayName string `json:"displayname"` + AvatarURL string `json:"avatar_url"` +} + +func eventToRoomMember(userID string, event *gomatrix.Event) *RoomMember { + if event == nil { + return &RoomMember{ + UserID: userID, + Membership: "leave", + } + } + membership, _ := event.Content["membership"].(string) + avatarURL, _ := event.Content["avatar_url"].(string) + + displayName, _ := event.Content["displayname"].(string) + if len(displayName) == 0 { + displayName = userID + } + + return &RoomMember{ + UserID: userID, + Membership: membership, + DisplayName: displayName, + AvatarURL: avatarURL, + } +} + +func (room *Room) createMemberCache() map[string]*RoomMember { + cache := make(map[string]*RoomMember) + events := room.GetStateEvents("m.room.member") + if events != nil { + for userID, event := range events { + member := eventToRoomMember(userID, event) + if member.Membership != "leave" { + cache[member.UserID] = member + } + } + } + room.memberCache = cache + return cache +} + +func (room *Room) GetMembers() map[string]*RoomMember { + if len(room.memberCache) == 0 { + room.createMemberCache() + } + return room.memberCache +} + +func (room *Room) GetMember(userID string) *RoomMember { + if len(room.memberCache) == 0 { + room.createMemberCache() + } + member, _ := room.memberCache[userID] + return member +} + +// NewRoom creates a new Room with the given ID +func NewRoom(roomID string) *Room { + return &Room{ + Room: gomatrix.NewRoom(roomID), + } +} diff --git a/session.go b/session.go index b679076..eda49dc 100644 --- a/session.go +++ b/session.go @@ -30,7 +30,7 @@ type Session struct { AccessToken string NextBatch string FilterID string - Rooms map[string]*gomatrix.Room + Rooms map[string]*Room debug DebugPrinter `json:"-"` } @@ -44,11 +44,18 @@ func (config *Config) NewSession(mxid string) *Session { return &Session{ MXID: mxid, path: filepath.Join(config.dir, mxid+".session"), - Rooms: make(map[string]*gomatrix.Room), + Rooms: make(map[string]*Room), debug: config.debug, } } +func (s *Session) Clear() { + s.Rooms = make(map[string]*Room) + s.NextBatch = "" + s.FilterID = "" + s.Save() +} + func (s *Session) Load() { data, err := ioutil.ReadFile(s.path) if err != nil { @@ -85,15 +92,20 @@ func (s *Session) LoadNextBatch(_ string) string { return s.NextBatch } -func (s *Session) LoadRoom(mxid string) *gomatrix.Room { +func (s *Session) GetRoom(mxid string) *Room { room, _ := s.Rooms[mxid] if room == nil { - room = gomatrix.NewRoom(mxid) - s.SaveRoom(room) + room = NewRoom(mxid) + s.Rooms[room.ID] = room } return room } +func (s *Session) PutRoom(room *Room) { + s.Rooms[room.ID] = room + s.Save() +} + func (s *Session) SaveFilterID(_, filterID string) { s.FilterID = filterID s.Save() @@ -104,7 +116,11 @@ func (s *Session) SaveNextBatch(_, nextBatch string) { s.Save() } +func (s *Session) LoadRoom(mxid string) *gomatrix.Room { + return s.GetRoom(mxid).Room +} + func (s *Session) SaveRoom(room *gomatrix.Room) { - s.Rooms[room.ID] = room + s.GetRoom(room.ID).Room = room s.Save() } diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..2e0bbcf --- /dev/null +++ b/sync.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "fmt" + "runtime/debug" + "time" + + "maunium.net/go/gomatrix" +) + +// GomuksSyncer is the default syncing implementation. You can either write your own syncer, or selectively +// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer +// pattern to notify callers about incoming events. See GomuksSyncer.OnEventType for more information. +type GomuksSyncer struct { + Session *Session + listeners map[string][]gomatrix.OnEventListener // event type to listeners array +} + +// NewGomuksSyncer returns an instantiated GomuksSyncer +func NewGomuksSyncer(session *Session) *GomuksSyncer { + return &GomuksSyncer{ + Session: session, + listeners: make(map[string][]gomatrix.OnEventListener), + } +} + +func (s *GomuksSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) { + if !s.shouldProcessResponse(res, since) { + return + } + // gdebug.Print("Processing sync response", since, res) + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.Session.MXID, since, r, debug.Stack()) + } + }() + + for _, event := range res.Presence.Events { + s.notifyListeners(&event) + } + for roomID, roomData := range res.Rooms.Join { + room := s.Session.GetRoom(roomID) + for _, event := range roomData.State.Events { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + for _, event := range roomData.Timeline.Events { + event.RoomID = roomID + s.notifyListeners(&event) + } + for _, event := range roomData.Ephemeral.Events { + event.RoomID = roomID + s.notifyListeners(&event) + } + + if len(room.PrevBatch) == 0 { + room.PrevBatch = roomData.Timeline.PrevBatch + } + } + for roomID, roomData := range res.Rooms.Invite { + room := s.Session.GetRoom(roomID) + for _, event := range roomData.State.Events { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + } + for roomID, roomData := range res.Rooms.Leave { + room := s.Session.GetRoom(roomID) + for _, event := range roomData.Timeline.Events { + if event.StateKey != nil { + event.RoomID = roomID + room.UpdateState(&event) + s.notifyListeners(&event) + } + } + + if len(room.PrevBatch) == 0 { + room.PrevBatch = roomData.Timeline.PrevBatch + } + } + return +} + +// OnEventType allows callers to be notified when there are new events for the given event type. +// There are no duplicate checks. +func (s *GomuksSyncer) OnEventType(eventType string, callback gomatrix.OnEventListener) { + _, exists := s.listeners[eventType] + if !exists { + s.listeners[eventType] = []gomatrix.OnEventListener{} + } + s.listeners[eventType] = append(s.listeners[eventType], callback) +} + +// shouldProcessResponse returns true if the response should be processed. May modify the response to remove +// stuff that shouldn't be processed. +func (s *GomuksSyncer) shouldProcessResponse(resp *gomatrix.RespSync, since string) bool { + if since == "" { + return false + } + return true +} + +func (s *GomuksSyncer) notifyListeners(event *gomatrix.Event) { + listeners, exists := s.listeners[event.Type] + if !exists { + return + } + for _, fn := range listeners { + fn(event) + } +} + +// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error. +func (s *GomuksSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) { + return 10 * time.Second, nil +} + +// GetFilterJSON returns a filter with a timeline limit of 50. +func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { + return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`) +} diff --git a/view-main.go b/view-main.go index 57e5d6d..2fd503a 100644 --- a/view-main.go +++ b/view-main.go @@ -112,10 +112,6 @@ func findWordToTabComplete(text string) string { return output } -func (view *MainView) GetRoom(id string) *RoomView { - return view.rooms[id] -} - func (view *MainView) InputTabComplete(text string, cursorOffset int) string { roomView, _ := view.rooms[view.CurrentRoomID()] if roomView != nil { @@ -156,10 +152,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) { case "/quit": view.gmx.Stop() case "/clearcache": - view.config.Session.Rooms = make(map[string]*gomatrix.Room) - view.config.Session.NextBatch = "" - view.config.Session.FilterID = "" - view.config.Session.Save() + view.config.Session.Clear() view.gmx.Stop() case "/part": fallthrough @@ -167,12 +160,12 @@ func (view *MainView) HandleCommand(room, command string, args []string) { view.matrix.client.LeaveRoom(room) case "/join": if len(args) == 0 { - view.AddMessage(room, "Usage: /join ") + view.AddServiceMessage(room, "Usage: /join ") break } view.debug.Print(view.matrix.JoinRoom(args[0])) default: - view.AddMessage(room, "Unknown command.") + view.AddServiceMessage(room, "Unknown command.") } } @@ -229,9 +222,14 @@ func (view *MainView) addRoom(index int, room string) { view.rooms[room] = roomView view.roomView.AddPage(room, roomView, true, false) roomView.UpdateUserList() + view.GetHistory(room) } } +func (view *MainView) GetRoom(id string) *RoomView { + return view.rooms[id] +} + func (view *MainView) HasRoom(room string) bool { for _, existingRoom := range view.roomIDs { if existingRoom == room { @@ -263,6 +261,7 @@ func (view *MainView) RemoveRoom(room string) { view.roomList.RemoveItem(removeIndex) view.roomIDs = append(view.roomIDs[:removeIndex], view.roomIDs[removeIndex+1:]...) view.roomView.RemovePage(room) + delete(view.rooms, room) view.Render() } @@ -270,6 +269,7 @@ func (view *MainView) SetRoomList(rooms []string) { view.roomIDs = rooms view.roomList.Clear() view.roomView.Clear() + view.rooms = make(map[string]*RoomView) for index, room := range rooms { view.addRoom(index, room) } @@ -284,10 +284,12 @@ func (view *MainView) SetTyping(room string, users []string) { } } -func (view *MainView) AddMessage(room, message string) { +func (view *MainView) AddServiceMessage(room, message string) { roomView, ok := view.rooms[room] if ok { - roomView.content.AddMessage("", "*", message, time.Now()) + messageView := roomView.MessageView() + message := messageView.NewMessage("", "*", message, time.Now()) + messageView.AddMessage(message, AppendMessage) view.parent.Render() } } @@ -295,3 +297,89 @@ func (view *MainView) AddMessage(room, message string) { func (view *MainView) Render() { view.parent.Render() } + +func (view *MainView) GetHistory(room string) { + roomView := view.rooms[room] + history, _, err := view.matrix.GetHistory(roomView.room.ID, view.config.Session.NextBatch, 50) + if err != nil { + view.debug.Print("Failed to fetch history for", roomView.room.ID, err) + return + } + for _, evt := range history { + var room *RoomView + var message *Message + if evt.Type == "m.room.message" { + room, message = view.ProcessMessageEvent(&evt) + } else if evt.Type == "m.room.member" { + room, message = view.ProcessMembershipEvent(&evt, false) + } + if room != nil && message != nil { + room.AddMessage(message, PrependMessage) + } + } +} + +func (view *MainView) ProcessMessageEvent(evt *gomatrix.Event) (room *RoomView, message *Message) { + room = view.GetRoom(evt.RoomID) + if room != nil { + text := evt.Content["body"].(string) + message = room.NewMessage(evt.ID, evt.Sender, text, unixToTime(evt.Timestamp)) + } + return +} + +func (view *MainView) processOwnMembershipChange(evt *gomatrix.Event) { + membership, _ := evt.Content["membership"].(string) + prevMembership := "leave" + if evt.Unsigned.PrevContent != nil { + prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) + } + if membership == prevMembership { + return + } + if membership == "join" { + view.AddRoom(evt.RoomID) + } else if membership == "leave" { + view.RemoveRoom(evt.RoomID) + } +} + +func (view *MainView) ProcessMembershipEvent(evt *gomatrix.Event, new bool) (room *RoomView, message *Message) { + if new && evt.StateKey != nil && *evt.StateKey == view.config.Session.MXID { + view.processOwnMembershipChange(evt) + } + + room = view.GetRoom(evt.RoomID) + if room != nil { + membership, _ := evt.Content["membership"].(string) + var sender, text string + if membership == "invite" { + sender = "---" + text = fmt.Sprintf("%s invited %s.", evt.Sender, *evt.StateKey) + } else if membership == "join" { + sender = "-->" + text = fmt.Sprintf("%s joined the room.", *evt.StateKey) + } else if membership == "leave" { + sender = "<--" + if evt.Sender != *evt.StateKey { + reason, _ := evt.Content["reason"].(string) + text = fmt.Sprintf("%s kicked %s: %s", evt.Sender, *evt.StateKey, reason) + } else { + text = fmt.Sprintf("%s left the room.", *evt.StateKey) + } + } else { + room = nil + return + } + message = room.NewMessage(evt.ID, sender, text, unixToTime(evt.Timestamp)) + } + return +} + +func unixToTime(unix int64) time.Time { + timestamp := time.Now() + if unix != 0 { + timestamp = time.Unix(unix/1000, unix%1000*1000) + } + return timestamp +}