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)
SendMarkdownMessage(roomID, msgtype, message string) (string, error)
SendTyping(roomID string, typing bool)
MarkRead(roomID, eventID string)
JoinRoom(roomID, server string) (*rooms.Room, error)
LeaveRoom(roomID string) error

View File

@ -46,7 +46,7 @@ type MainView interface {
SetRooms(rooms map[string]*rooms.Room)
SaveAllHistory()
UpdateTags(room *rooms.Room, newTags []rooms.RoomTag)
UpdateTags(room *rooms.Room)
SetTyping(roomID string, users []string)
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.OnEventType("m.room.message", c.HandleMessage)
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.direct", c.HandleDirectChatInfo)
c.syncer.OnEventType("m.push_rules", c.HandlePushRules)
c.syncer.OnEventType("m.tag", c.HandleTag)
c.syncer.InitDoneCallback = func() {
@ -228,7 +230,7 @@ func (c *Container) Start() {
// HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) {
if source & EventSourceLeave != 0 {
if source&EventSourceLeave != 0 {
return
}
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.
func (c *Container) HandlePushRules(source EventSource, evt *gomatrix.Event) {
debug.Print("Received updated push rules")
@ -285,7 +363,8 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) {
}
mainView := c.ui.MainView()
mainView.UpdateTags(room, newTags)
room.RawTags = newTags
mainView.UpdateTags(room)
}
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.
func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) {
isLeave := source & EventSourceLeave != 0
isTimeline := source & EventSourceTimeline != 0
isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0
isNonTimelineLeave := isLeave && !isTimeline
if !c.config.Session.InitialSyncDone && isNonTimelineLeave {
return
@ -356,6 +435,11 @@ func (c *Container) HandleTyping(source EventSource, evt *gomatrix.Event) {
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.
func (c *Container) SendMessage(roomID, msgtype, text string) (string, error) {
defer debug.Recover()

View File

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

View File

@ -215,11 +215,19 @@ func TestRoom_GetTitle_Members_GroupChat(t *testing.T) {
func TestRoom_MarkRead(t *testing.T) {
room := rooms.NewRoom("!test:maunium.net", "@tulir:maunium.net")
room.UnreadMessages = 123
room.Highlighted = true
room.HasNewMessages = true
room.MarkRead()
assert.Zero(t, room.UnreadMessages)
assert.False(t, room.Highlighted)
assert.False(t, room.HasNewMessages)
room.AddUnread("foo", true, false)
assert.Equal(t, 1, room.UnreadCount())
assert.False(t, room.Highlighted())
room.AddUnread("bar", true, false)
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,
},
Ephemeral: gomatrix.FilterPart{
Types: []string{"m.typing"},
Types: []string{"m.typing", "m.receipt"},
},
AccountData: gomatrix.FilterPart{
Types: []string{"m.tag"},
},
},
AccountData: gomatrix.FilterPart{
Types: []string{"m.push_rules"},
Types: []string{"m.push_rules", "m.direct"},
},
Presence: gomatrix.FilterPart{
Types: []string{},

View File

@ -265,6 +265,7 @@ func (list *RoomList) Contains(roomID string) bool {
}
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() {
list.AddToTag(tag, room)
}
@ -289,10 +290,6 @@ func (list *RoomList) CheckTag(tag string) {
}
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]
if !ok {
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) {
for _, tag := range room.Tags() {
list.RemoveFromTag(tag.Tag, room)
for _, tag := range list.tags {
list.RemoveFromTag(tag, room)
}
}
@ -707,21 +704,22 @@ func (list *RoomList) Draw(screen tcell.Screen) {
if tag == list.selectedTag && item.Room == list.selected {
style = style.Foreground(list.selectedTextColor).Background(list.selectedBackgroundColor)
}
if item.HasNewMessages {
if item.HasNewMessages() {
style = style.Bold(true)
}
if item.UnreadMessages > 0 {
unreadCount := item.UnreadCount()
if unreadCount > 0 {
unreadMessageCount := "99+"
if item.UnreadMessages < 100 {
unreadMessageCount = strconv.Itoa(item.UnreadMessages)
if unreadCount < 100 {
unreadMessageCount = strconv.Itoa(unreadCount)
}
if item.Highlighted {
if item.Highlighted() {
unreadMessageCount += "!"
}
unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-6, y, 6, style)
lineWidth -= len(unreadMessageCount) + 1
widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
lineWidth -= len(unreadMessageCount)
}
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(widget.NewBorder(), 1, 0, false)
mainView.AddItem(mainView.roomView, 0, 1, true)
mainView.BumpFocus()
mainView.BumpFocus(nil)
ui.mainView = mainView
return mainView
}
func (view *MainView) BumpFocus() {
func (view *MainView) BumpFocus(roomView *RoomView) {
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) {
@ -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 {
view.BumpFocus()
view.BumpFocus(roomView)
k := key.Key()
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() {
return event
}
view.BumpFocus()
view.BumpFocus(roomView)
msgView := roomView.MessageView()
x, y := event.Position()
@ -251,12 +261,8 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo
}
case tcell.WheelDown:
msgView.AddScrollOffset(-WheelScrollOffsetDiff)
view.parent.Render()
if msgView.ScrollOffset == 0 {
roomView.Room.MarkRead()
}
view.MarkRead(roomView)
default:
if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
view.parent.Render()
@ -293,9 +299,12 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
view.roomView.SwitchToPage(room.ID)
roomView := view.rooms[room.ID]
if roomView.MessageView().ScrollOffset == 0 {
roomView.Room.MarkRead()
if roomView == nil {
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.parent.app.SetFocus(view)
view.parent.Render()
@ -353,10 +362,10 @@ func (view *MainView) GetRoom(roomID string) ifc.RoomView {
func (view *MainView) AddRoom(room *rooms.Room) {
if view.roomList.Contains(room.ID) {
debug.Print("Add aborted", room.ID)
debug.Print("Add aborted (room exists)", room.ID, room.GetTitle())
return
}
debug.Print("Adding", room.ID)
debug.Print("Adding", room.ID, room.GetTitle())
view.roomList.Add(room)
view.addRoomPage(room)
if !view.roomList.HasSelected() {
@ -367,10 +376,10 @@ func (view *MainView) AddRoom(room *rooms.Room) {
func (view *MainView) RemoveRoom(room *rooms.Room) {
roomView := view.GetRoom(room.ID)
if roomView == nil {
debug.Print("Remove aborted", room.ID)
debug.Print("Remove aborted (not found)", room.ID, room.GetTitle())
return
}
debug.Print("Removing", room.ID)
debug.Print("Removing", room.ID, room.GetTitle())
view.roomList.Remove(room)
view.SwitchRoom(view.roomList.Selected())
@ -395,38 +404,12 @@ func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
view.SwitchRoom(view.roomList.First())
}
func (view *MainView) UpdateTags(room *rooms.Room, newTags []rooms.RoomTag) {
if len(newTags) == 0 {
for _, tag := range room.RawTags {
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)
}
func (view *MainView) UpdateTags(room *rooms.Room) {
if !view.roomList.Contains(room.ID) {
return
}
room.RawTags = newTags
view.roomList.Remove(room)
view.roomList.Add(room)
}
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.
isCurrent := room == view.roomList.SelectedRoom()
// 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.
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.
room.HasNewMessages = true
room.Highlighted = should.Highlight || room.Highlighted
if shouldNotify {
room.UnreadMessages++
}
room.AddUnread(message.ID(), shouldNotify, should.Highlight)
} else {
view.matrix.MarkRead(room.ID, message.ID())
}
if shouldNotify && !isFocused {
if shouldNotify && !recentlyFocused {
// Push rules say notify and the terminal is not focused, send desktop notification.
shouldPlaySound := should.PlaySound && should.SoundName == "default"
sendNotification(room, message.Sender(), message.NotificationContent(), should.Highlight, shouldPlaySound)