Handle m.direct and m.receipt events

Fixes #12
Fixes #45
This commit is contained in:
Tulir Asokan 2018-05-16 20:09:09 +03:00
parent c88801a657
commit 8a3fbc24ab
8 changed files with 229 additions and 92 deletions

View File

@ -35,6 +35,7 @@ type MatrixContainer interface {
SendMessage(roomID, msgtype, message string) (string, error) SendMessage(roomID, msgtype, message string) (string, error)
SendMarkdownMessage(roomID, msgtype, message string) (string, error) SendMarkdownMessage(roomID, msgtype, message string) (string, error)
SendTyping(roomID string, typing bool) SendTyping(roomID string, typing bool)
MarkRead(roomID, eventID string)
JoinRoom(roomID, server string) (*rooms.Room, error) JoinRoom(roomID, server string) (*rooms.Room, error)
LeaveRoom(roomID string) error LeaveRoom(roomID string) error

View File

@ -46,7 +46,7 @@ type MainView interface {
SetRooms(rooms map[string]*rooms.Room) SetRooms(rooms map[string]*rooms.Room)
SaveAllHistory() SaveAllHistory()
UpdateTags(room *rooms.Room, newTags []rooms.RoomTag) UpdateTags(room *rooms.Room)
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
ParseEvent(roomView RoomView, evt *gomatrix.Event) Message ParseEvent(roomView RoomView, evt *gomatrix.Event) Message

View File

@ -177,7 +177,9 @@ func (c *Container) OnLogin() {
c.syncer = NewGomuksSyncer(c.config.Session) c.syncer = NewGomuksSyncer(c.config.Session)
c.syncer.OnEventType("m.room.message", c.HandleMessage) c.syncer.OnEventType("m.room.message", c.HandleMessage)
c.syncer.OnEventType("m.room.member", c.HandleMembership) c.syncer.OnEventType("m.room.member", c.HandleMembership)
c.syncer.OnEventType("m.receipt", c.HandleReadReceipt)
c.syncer.OnEventType("m.typing", c.HandleTyping) c.syncer.OnEventType("m.typing", c.HandleTyping)
c.syncer.OnEventType("m.direct", c.HandleDirectChatInfo)
c.syncer.OnEventType("m.push_rules", c.HandlePushRules) c.syncer.OnEventType("m.push_rules", c.HandlePushRules)
c.syncer.OnEventType("m.tag", c.HandleTag) c.syncer.OnEventType("m.tag", c.HandleTag)
c.syncer.InitDoneCallback = func() { c.syncer.InitDoneCallback = func() {
@ -228,7 +230,7 @@ func (c *Container) Start() {
// HandleMessage is the event handler for the m.room.message timeline event. // HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) {
if source & EventSourceLeave != 0 { if source&EventSourceLeave != 0 {
return return
} }
mainView := c.ui.MainView() mainView := c.ui.MainView()
@ -253,6 +255,82 @@ func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) {
} }
} }
func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent string) {
var largestTimestamp int64
for eventID, rawContent := range evt.Content {
content, ok := rawContent.(map[string]interface{})
if !ok {
continue
}
mRead, ok := content["m.read"].(map[string]interface{})
if !ok {
continue
}
myInfo, ok := mRead[c.config.Session.UserID].(map[string]interface{})
if !ok {
continue
}
ts, ok := myInfo["ts"].(float64)
if int64(ts) > largestTimestamp {
largestTimestamp = int64(ts)
largestTimestampEvent = eventID
}
}
return
}
func (c *Container) HandleReadReceipt(source EventSource, evt *gomatrix.Event) {
if source&EventSourceLeave != 0 {
return
}
lastReadEvent := c.parseReadReceipt(evt)
if len(lastReadEvent) == 0 {
return
}
room := c.GetRoom(evt.RoomID)
room.MarkRead(lastReadEvent)
c.ui.Render()
}
func (c *Container) parseDirectChatInfo(evt *gomatrix.Event) (map[*rooms.Room]bool){
directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content {
roomIDList, ok := rawRoomIDList.([]interface{})
if !ok {
continue
}
for _, rawRoomID := range roomIDList {
roomID, ok := rawRoomID.(string)
if !ok {
continue
}
room := c.GetRoom(roomID)
if room != nil && !room.HasLeft {
directChats[room] = true
}
}
}
return directChats
}
func (c *Container) HandleDirectChatInfo(source EventSource, evt *gomatrix.Event) {
directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Session.Rooms {
shouldBeDirect := directChats[room]
if shouldBeDirect != room.IsDirect {
room.IsDirect = shouldBeDirect
c.ui.MainView().UpdateTags(room)
}
}
}
// HandlePushRules is the event handler for the m.push_rules account data event. // HandlePushRules is the event handler for the m.push_rules account data event.
func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) { func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) {
debug.Print("Received updated push rules") debug.Print("Received updated push rules")
@ -285,7 +363,8 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) {
} }
mainView := c.ui.MainView() mainView := c.ui.MainView()
mainView.UpdateTags(room, newTags) room.RawTags = newTags
mainView.UpdateTags(room)
} }
func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) {
@ -314,8 +393,8 @@ func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) {
// HandleMembership is the event handler for the m.room.member state event. // HandleMembership is the event handler for the m.room.member state event.
func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) {
isLeave := source & EventSourceLeave != 0 isLeave := source&EventSourceLeave != 0
isTimeline := source & EventSourceTimeline != 0 isTimeline := source&EventSourceTimeline != 0
isNonTimelineLeave := isLeave && !isTimeline isNonTimelineLeave := isLeave && !isTimeline
if !c.config.Session.InitialSyncDone && isNonTimelineLeave { if !c.config.Session.InitialSyncDone && isNonTimelineLeave {
return return
@ -356,6 +435,11 @@ func (c *Container) HandleTyping(source EventSource, evt *gomatrix.Event) {
c.ui.MainView().SetTyping(evt.RoomID, strUsers) c.ui.MainView().SetTyping(evt.RoomID, strUsers)
} }
func (c *Container) MarkRead(roomID, eventID string) {
urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
c.client.MakeRequest("POST", urlPath, struct{}{}, nil)
}
// SendMessage sends a message with the given text to the given room. // SendMessage sends a message with the given text to the given room.
func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) { func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) {
defer debug.Recover() defer debug.Recover()

View File

@ -43,6 +43,12 @@ type RoomTag struct {
Order string Order string
} }
type UnreadMessage struct {
EventID string
Counted bool
Highlight bool
}
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {
*gomatrix.Room *gomatrix.Room
@ -57,13 +63,11 @@ type Room struct {
SessionUserID string SessionUserID string
// The number of unread messages that were notified about. // The number of unread messages that were notified about.
UnreadMessages int UnreadMessages []UnreadMessage
// Whether or not any of the unread messages were highlights. unreadCountCache *int
Highlighted bool highlightCache *bool
// Whether or not the room contains any new messages. // Whether or not this room is marked as a direct chat.
// This can be true even when UnreadMessages is zero if there's IsDirect bool
// a notificationless message like bot notices.
HasNewMessages bool
// List of tags given to this room // List of tags given to this room
RawTags []RoomTag RawTags []RoomTag
@ -110,14 +114,74 @@ func (room *Room) UnlockHistory() {
} }
// MarkRead clears the new message statuses on this room. // MarkRead clears the new message statuses on this room.
func (room *Room) MarkRead() { func (room *Room) MarkRead(eventID string) {
room.UnreadMessages = 0 readToIndex := -1
room.Highlighted = false for index, unreadMessage := range room.UnreadMessages {
room.HasNewMessages = false if unreadMessage.EventID == eventID {
readToIndex = index
}
}
if readToIndex >= 0 {
room.UnreadMessages = room.UnreadMessages[readToIndex+1:]
room.highlightCache = nil
room.unreadCountCache = nil
}
}
func (room *Room) UnreadCount() int {
if room.unreadCountCache == nil {
room.unreadCountCache = new(int)
for _, unreadMessage := range room.UnreadMessages {
if unreadMessage.Counted {
*room.unreadCountCache++
}
}
}
return *room.unreadCountCache
}
func (room *Room) Highlighted() bool {
if room.highlightCache == nil {
room.highlightCache = new(bool)
for _, unreadMessage := range room.UnreadMessages {
if unreadMessage.Highlight {
*room.highlightCache = true
break
}
}
}
return *room.highlightCache
}
func (room *Room) HasNewMessages() bool {
return len(room.UnreadMessages) > 0
}
func (room *Room) AddUnread(eventID string, counted, highlight bool) {
room.UnreadMessages = append(room.UnreadMessages, UnreadMessage{
EventID: eventID,
Counted: counted,
Highlight: highlight,
})
if counted {
if room.unreadCountCache == nil {
room.unreadCountCache = new(int)
}
*room.unreadCountCache++
}
if highlight {
if room.highlightCache == nil {
room.highlightCache = new(bool)
}
*room.highlightCache = true
}
} }
func (room *Room) Tags() []RoomTag { func (room *Room) Tags() []RoomTag {
if len(room.RawTags) == 0 { if len(room.RawTags) == 0 {
if room.IsDirect {
return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}}
}
return []RoomTag{{"", "0.5"}} return []RoomTag{{"", "0.5"}}
} }
return room.RawTags return room.RawTags

View File

@ -215,11 +215,19 @@ func TestRoom_GetTitle_Members_GroupChat(t *testing.T) {
func TestRoom_MarkRead(t *testing.T) { func TestRoom_MarkRead(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net") room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UnreadMessages = 123
room.Highlighted = true room.AddUnread("foo", true, false)
room.HasNewMessages = true assert.Equal(t, 1, room.UnreadCount())
room.MarkRead() assert.False(t, room.Highlighted())
assert.Zero(t, room.UnreadMessages)
assert.False(t, room.Highlighted) room.AddUnread("bar", true, false)
assert.False(t, room.HasNewMessages) assert.Equal(t, 2, room.UnreadCount())
assert.False(t, room.Highlighted())
room.AddUnread("asd", false, true)
assert.Equal(t, 2, room.UnreadCount())
assert.True(t, room.Highlighted())
room.MarkRead("")
assert.Empty(t, room.UnreadMessages)
} }

View File

@ -177,14 +177,14 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
Limit: 50, Limit: 50,
}, },
Ephemeral: gomatrix.FilterPart{ Ephemeral: gomatrix.FilterPart{
Types: []string{"m.typing"}, Types: []string{"m.typing", "m.receipt"},
}, },
AccountData: gomatrix.FilterPart{ AccountData: gomatrix.FilterPart{
Types: []string{"m.tag"}, Types: []string{"m.tag"},
}, },
}, },
AccountData: gomatrix.FilterPart{ AccountData: gomatrix.FilterPart{
Types: []string{"m.push_rules"}, Types: []string{"m.push_rules", "m.direct"},
}, },
Presence: gomatrix.FilterPart{ Presence: gomatrix.FilterPart{
Types: []string{}, Types: []string{},

View File

@ -265,6 +265,7 @@ func (list *RoomList) Contains(roomID string) bool {
} }
func (list *RoomList) Add(room *rooms.Room) { func (list *RoomList) Add(room *rooms.Room) {
debug.Print("Adding room to list", room.ID, room.GetTitle(), room.IsDirect, room.Tags())
for _, tag := range room.Tags() { for _, tag := range room.Tags() {
list.AddToTag(tag, room) list.AddToTag(tag, room)
} }
@ -289,10 +290,6 @@ func (list *RoomList) CheckTag(tag string) {
} }
func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) { func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) {
if tag.Tag == "" && len(room.GetMembers()) == 2 {
tag.Tag = "net.maunium.gomuks.fake.direct"
}
tagRoomList, ok := list.items[tag.Tag] tagRoomList, ok := list.items[tag.Tag]
if !ok { if !ok {
list.items[tag.Tag] = newTagRoomList(convertRoom(room)) list.items[tag.Tag] = newTagRoomList(convertRoom(room))
@ -304,8 +301,8 @@ func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) {
} }
func (list *RoomList) Remove(room *rooms.Room) { func (list *RoomList) Remove(room *rooms.Room) {
for _, tag := range room.Tags() { for _, tag := range list.tags {
list.RemoveFromTag(tag.Tag, room) list.RemoveFromTag(tag, room)
} }
} }
@ -707,21 +704,22 @@ func (list *RoomList) Draw(screen tcell.Screen) {
if tag == list.selectedTag && item.Room == list.selected { if tag == list.selectedTag && item.Room == list.selected {
style = style.Foreground(list.selectedTextColor).Background(list.selectedBackgroundColor) style = style.Foreground(list.selectedTextColor).Background(list.selectedBackgroundColor)
} }
if item.HasNewMessages { if item.HasNewMessages() {
style = style.Bold(true) style = style.Bold(true)
} }
if item.UnreadMessages > 0 { unreadCount := item.UnreadCount()
if unreadCount > 0 {
unreadMessageCount := "99+" unreadMessageCount := "99+"
if item.UnreadMessages < 100 { if unreadCount < 100 {
unreadMessageCount = strconv.Itoa(item.UnreadMessages) unreadMessageCount = strconv.Itoa(unreadCount)
} }
if item.Highlighted { if item.Highlighted() {
unreadMessageCount += "!" unreadMessageCount += "!"
} }
unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount) unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style) widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
lineWidth -= len(unreadMessageCount) + 1 lineWidth -= len(unreadMessageCount)
} }
widget.WriteLinePadded(screen, tview.AlignLeft, text, x, y, lineWidth, style) widget.WriteLinePadded(screen, tview.AlignLeft, text, x, y, lineWidth, style)

View File

@ -67,15 +67,25 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
mainView.AddItem(mainView.roomList, 25, 0, false) mainView.AddItem(mainView.roomList, 25, 0, false)
mainView.AddItem(widget.NewBorder(), 1, 0, false) mainView.AddItem(widget.NewBorder(), 1, 0, false)
mainView.AddItem(mainView.roomView, 0, 1, true) mainView.AddItem(mainView.roomView, 0, 1, true)
mainView.BumpFocus() mainView.BumpFocus(nil)
ui.mainView = mainView ui.mainView = mainView
return mainView return mainView
} }
func (view *MainView) BumpFocus() { func (view *MainView) BumpFocus(roomView *RoomView) {
view.lastFocusTime = time.Now() view.lastFocusTime = time.Now()
view.MarkRead(roomView)
}
func (view *MainView) MarkRead(roomView *RoomView) {
if roomView != nil && roomView.Room.HasNewMessages() && roomView.MessageView().ScrollOffset == 0 {
msgList := roomView.MessageView().messages
msg := msgList[len(msgList)-1]
roomView.Room.MarkRead(msg.ID())
view.matrix.MarkRead(roomView.Room.ID, msg.ID())
}
} }
func (view *MainView) InputChanged(roomView *RoomView, text string) { func (view *MainView) InputChanged(roomView *RoomView, text string) {
@ -182,7 +192,7 @@ func (view *MainView) HandleCommand(roomView *RoomView, command string, args []s
} }
func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey { func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey {
view.BumpFocus() view.BumpFocus(roomView)
k := key.Key() k := key.Key()
if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt { if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt {
@ -232,7 +242,7 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo
if event.Buttons() == tcell.ButtonNone || event.HasMotion() { if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
return event return event
} }
view.BumpFocus() view.BumpFocus(roomView)
msgView := roomView.MessageView() msgView := roomView.MessageView()
x, y := event.Position() x, y := event.Position()
@ -251,12 +261,8 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo
} }
case tcell.WheelDown: case tcell.WheelDown:
msgView.AddScrollOffset(-WheelScrollOffsetDiff) msgView.AddScrollOffset(-WheelScrollOffsetDiff)
view.parent.Render() view.parent.Render()
view.MarkRead(roomView)
if msgView.ScrollOffset == 0 {
roomView.Room.MarkRead()
}
default: default:
if msgView.HandleClick(x-mx, y-my, event.Buttons()) { if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
view.parent.Render() view.parent.Render()
@ -293,9 +299,12 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
view.roomView.SwitchToPage(room.ID) view.roomView.SwitchToPage(room.ID)
roomView := view.rooms[room.ID] roomView := view.rooms[room.ID]
if roomView.MessageView().ScrollOffset == 0 { if roomView == nil {
roomView.Room.MarkRead() debug.Print("Tried to switch to non-nil room with nil roomView!")
debug.Print(tag, room)
return
} }
view.MarkRead(roomView)
view.roomList.SetSelected(tag, room) view.roomList.SetSelected(tag, room)
view.parent.app.SetFocus(view) view.parent.app.SetFocus(view)
view.parent.Render() view.parent.Render()
@ -353,10 +362,10 @@ func (view *MainView) GetRoom(roomID string) ifc.RoomView {
func (view *MainView) AddRoom(room *rooms.Room) { func (view *MainView) AddRoom(room *rooms.Room) {
if view.roomList.Contains(room.ID) { if view.roomList.Contains(room.ID) {
debug.Print("Add aborted", room.ID) debug.Print("Add aborted (room exists)", room.ID, room.GetTitle())
return return
} }
debug.Print("Adding", room.ID) debug.Print("Adding", room.ID, room.GetTitle())
view.roomList.Add(room) view.roomList.Add(room)
view.addRoomPage(room) view.addRoomPage(room)
if !view.roomList.HasSelected() { if !view.roomList.HasSelected() {
@ -367,10 +376,10 @@ func (view *MainView) AddRoom(room *rooms.Room) {
func (view *MainView) RemoveRoom(room *rooms.Room) { func (view *MainView) RemoveRoom(room *rooms.Room) {
roomView := view.GetRoom(room.ID) roomView := view.GetRoom(room.ID)
if roomView == nil { if roomView == nil {
debug.Print("Remove aborted", room.ID) debug.Print("Remove aborted (not found)", room.ID, room.GetTitle())
return return
} }
debug.Print("Removing", room.ID) debug.Print("Removing", room.ID, room.GetTitle())
view.roomList.Remove(room) view.roomList.Remove(room)
view.SwitchRoom(view.roomList.Selected()) view.SwitchRoom(view.roomList.Selected())
@ -395,38 +404,12 @@ func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
view.SwitchRoom(view.roomList.First()) view.SwitchRoom(view.roomList.First())
} }
func (view *MainView) UpdateTags(room *rooms.Room, newTags []rooms.RoomTag) { func (view *MainView) UpdateTags(room *rooms.Room) {
if len(newTags) == 0 { if !view.roomList.Contains(room.ID) {
for _, tag := range room.RawTags { return
view.roomList.RemoveFromTag(tag.Tag, room)
}
view.roomList.AddToTag(rooms.RoomTag{Tag: "", Order: "0.5"}, room)
} else if len(room.RawTags) == 0 {
view.roomList.RemoveFromTag("", room)
for _, tag := range newTags {
view.roomList.AddToTag(tag, room)
}
} else {
NewTags:
for _, newTag := range newTags {
for _, oldTag := range room.RawTags {
if newTag.Tag == oldTag.Tag {
continue NewTags
}
}
view.roomList.AddToTag(newTag, room)
}
OldTags:
for _, oldTag := range room.RawTags {
for _, newTag := range newTags {
if newTag.Tag == oldTag.Tag {
continue OldTags
}
}
view.roomList.RemoveFromTag(oldTag.Tag, room)
}
} }
room.RawTags = newTags view.roomList.Remove(room)
view.roomList.Add(room)
} }
func (view *MainView) SetTyping(room string, users []string) { func (view *MainView) SetTyping(room string, users []string) {
@ -449,21 +432,20 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul
// Whether or not the room where the message came is the currently shown room. // Whether or not the room where the message came is the currently shown room.
isCurrent := room == view.roomList.SelectedRoom() isCurrent := room == view.roomList.SelectedRoom()
// Whether or not the terminal window is focused. // Whether or not the terminal window is focused.
isFocused := time.Now().Add(-30 * time.Second).Before(view.lastFocusTime) recentlyFocused := time.Now().Add(-30 * time.Second).Before(view.lastFocusTime)
isFocused := time.Now().Add(-5 * time.Second).Before(view.lastFocusTime)
// Whether or not the push rules say this message should be notified about. // Whether or not the push rules say this message should be notified about.
shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender() != view.config.Session.UserID shouldNotify := (should.Notify || !should.NotifySpecified) && message.Sender() != view.config.Session.UserID
if !isCurrent { if !isCurrent || !isFocused {
// The message is not in the current room, show new message status in room list. // The message is not in the current room, show new message status in room list.
room.HasNewMessages = true room.AddUnread(message.ID(), shouldNotify, should.Highlight)
room.Highlighted = should.Highlight || room.Highlighted } else {
if shouldNotify { view.matrix.MarkRead(room.ID, message.ID())
room.UnreadMessages++
}
} }
if shouldNotify && !isFocused { if shouldNotify && !recentlyFocused {
// Push rules say notify and the terminal is not focused, send desktop notification. // Push rules say notify and the terminal is not focused, send desktop notification.
shouldPlaySound := should.PlaySound && should.SoundName == "default" shouldPlaySound := should.PlaySound && should.SoundName == "default"
sendNotification(room, message.Sender(), message.NotificationContent(), should.Highlight, shouldPlaySound) sendNotification(room, message.Sender(), message.NotificationContent(), should.Highlight, shouldPlaySound)