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
+}