From 455d9fc4c5726e4af9c40b36ee4f4ea18f65a8e9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 22 Feb 2020 01:17:52 +0200 Subject: Improve tags and add initial invite handling --- matrix/matrix.go | 19 +++++++------- matrix/pushrules/condition.go | 5 ++-- matrix/rooms/room.go | 53 +++++++++++++++++++++++++------------- ui/command-processor.go | 60 +++++++++++++++++++++++-------------------- ui/commands.go | 49 ++++++++++++++++++++++++++++++++++- ui/member-list.go | 5 ++-- ui/room-list.go | 53 +++++++++++++++++++++++++++++++------- ui/tag-room-list.go | 27 ++++++++++++------- 8 files changed, 193 insertions(+), 78 deletions(-) diff --git a/matrix/matrix.go b/matrix/matrix.go index d3edde5..8819a5b 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -663,7 +663,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool return directChats } -func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms.Map { shouldBeDirect := directChats[room] @@ -677,7 +677,7 @@ func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) } // HandlePushRules is the event handler for the m.push_rules account data event. -func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) { +func (c *Container) HandlePushRules(_ EventSource, evt *mautrix.Event) { debug.Print("Received updated push rules") var err error c.config.PushRules, err = pushrules.EventToPushRules(evt) @@ -689,15 +689,16 @@ func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) { } // HandleTag is the event handler for the m.tag account data event. -func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleTag(_ EventSource, evt *mautrix.Event) { + debug.Printf("Received tags for %s: %s -- %s", evt.RoomID, evt.Content.RoomTags, string(evt.Content.VeryRaw)) room := c.GetOrCreateRoom(evt.RoomID) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) index := 0 for tag, info := range evt.Content.RoomTags { - order := "0.5" + order := json.Number("0.5") if len(info.Order) > 0 { - order = info.Order.String() + order = info.Order } newTags[index] = rooms.RoomTag{ Tag: tag, @@ -714,7 +715,7 @@ func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { } // HandleTyping is the event handler for the m.typing event. -func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) { +func (c *Container) HandleTyping(_ EventSource, evt *mautrix.Event) { if !c.config.AuthCache.InitialSyncDone { return } @@ -723,7 +724,7 @@ func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) { func (c *Container) MarkRead(roomID, eventID string) { urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID) - c.client.MakeRequest("POST", urlPath, struct{}{}, nil) + _, _ = c.client.MakeRequest("POST", urlPath, struct{}{}, nil) } var mentionRegex = regexp.MustCompile("\\[(.+?)]\\(https://matrix.to/#/@.+?:.+?\\)") @@ -791,10 +792,10 @@ func (c *Container) SendTyping(roomID string, typing bool) { } if typing { - c.client.UserTyping(roomID, true, 20000) + _, _ = c.client.UserTyping(roomID, true, 20000) c.typing = ts + 15 } else { - c.client.UserTyping(roomID, false, 0) + _, _ = c.client.UserTyping(roomID, false, 0) c.typing = 0 } } diff --git a/matrix/pushrules/condition.go b/matrix/pushrules/condition.go index 0b7776a..cc62da1 100644 --- a/matrix/pushrules/condition.go +++ b/matrix/pushrules/condition.go @@ -22,6 +22,7 @@ import ( "strings" "unicode" + "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/mautrix" "maunium.net/go/gomuks/lib/glob" @@ -29,8 +30,8 @@ import ( // Room is an interface with the functions that are needed for processing room-specific push conditions type Room interface { - GetMember(mxid string) *mautrix.Member - GetMembers() map[string]*mautrix.Member + GetMember(mxid string) *rooms.Member + GetMembers() map[string]*rooms.Member GetSessionOwner() string } diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 9081ba5..7df29ad 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -19,6 +19,7 @@ package rooms import ( "compress/gzip" "encoding/gob" + "encoding/json" "fmt" "os" "time" @@ -49,7 +50,7 @@ type RoomTag struct { // The name of the tag. Tag string // The order of the tag. - Order string + Order json.Number } type UnreadMessage struct { @@ -58,6 +59,13 @@ type UnreadMessage struct { Highlight bool } +type Member struct { + mautrix.Member + + // The user who sent the membership event + Sender string `json:"-"` +} + // Room represents a single Matrix room. type Room struct { // The room ID. @@ -73,7 +81,7 @@ type Room struct { LastPrevBatch string // The MXID of the user whose session this room was created for. SessionUserID string - SessionMember *mautrix.Member + SessionMember *Member // The number of unread messages that were notified about. UnreadMessages []UnreadMessage @@ -95,12 +103,12 @@ type Room struct { // Room state cache. state map[mautrix.EventType]map[string]*mautrix.Event // MXID -> Member cache calculated from membership events. - memberCache map[string]*mautrix.Member - exMemberCache map[string]*mautrix.Member + memberCache map[string]*Member + exMemberCache map[string]*Member // The first two non-SessionUserID members in the room. Calculated at // the same time as memberCache. - firstMemberCache *mautrix.Member - secondMemberCache *mautrix.Member + firstMemberCache *Member + secondMemberCache *Member // The name of the room. Calculated from the state event name, // canonical_alias or alias or the member cache. NameCache string @@ -337,8 +345,13 @@ func (room *Room) Tags() []RoomTag { room.lock.RLock() defer room.lock.RUnlock() if len(room.RawTags) == 0 { + sessionMember := room.GetMember(room.SessionUserID) if room.IsDirect { return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}} + } else if sessionMember != nil && sessionMember.Membership == mautrix.MembershipInvite { + return []RoomTag{{"net.maunium.gomuks.fake.invite", "0.5"}} + } else if sessionMember != nil && sessionMember.Membership != mautrix.MembershipJoin { + return []RoomTag{{"net.maunium.gomuks.fake.leave", "0.5"}} } return []RoomTag{{"", "0.5"}} } @@ -403,10 +416,11 @@ func (room *Room) UpdateState(event *mautrix.Event) { func (room *Room) updateMemberState(event *mautrix.Event) { userID := event.GetStateKey() if userID == room.SessionUserID { - room.SessionMember = room.eventToMember(userID, &event.Content) + debug.Print("Updating session user state:", string(event.Content.VeryRaw)) + room.SessionMember = room.eventToMember(userID, event.Sender, &event.Content) } if room.memberCache != nil { - member := room.eventToMember(userID, &event.Content) + member := room.eventToMember(userID, event.Sender, &event.Content) if member.Membership.IsInviteOrJoin() { existingMember, ok := room.memberCache[userID] if ok { @@ -553,16 +567,19 @@ func (room *Room) ReplacedBy() string { return *room.replacedByCache } -func (room *Room) eventToMember(userID string, content *mautrix.Content) *mautrix.Member { - member := &content.Member +func (room *Room) eventToMember(userID string, sender string, content *mautrix.Content) *Member { + member := content.Member member.Membership = content.Membership if len(member.Displayname) == 0 { member.Displayname = userID } - return member + return &Member{ + Member: member, + Sender: sender, + } } -func (room *Room) updateNthMemberCache(userID string, member *mautrix.Member) { +func (room *Room) updateNthMemberCache(userID string, member *Member) { if userID != room.SessionUserID { if room.firstMemberCache == nil { room.firstMemberCache = member @@ -573,19 +590,19 @@ func (room *Room) updateNthMemberCache(userID string, member *mautrix.Member) { } // createMemberCache caches all member events into a easily processable MXID -> *Member map. -func (room *Room) createMemberCache() map[string]*mautrix.Member { +func (room *Room) createMemberCache() map[string]*Member { if len(room.memberCache) > 0 { return room.memberCache } - cache := make(map[string]*mautrix.Member) - exCache := make(map[string]*mautrix.Member) + cache := make(map[string]*Member) + exCache := make(map[string]*Member) room.lock.RLock() events := room.getStateEvents(mautrix.StateMember) room.firstMemberCache = nil room.secondMemberCache = nil if events != nil { for userID, event := range events { - member := room.eventToMember(userID, &event.Content) + member := room.eventToMember(userID, event.Sender, &event.Content) if member.Membership.IsInviteOrJoin() { cache[userID] = member room.updateNthMemberCache(userID, member) @@ -615,7 +632,7 @@ func (room *Room) createMemberCache() map[string]*mautrix.Member { // // The members are returned from the cache. // If the cache is empty, it is updated first. -func (room *Room) GetMembers() map[string]*mautrix.Member { +func (room *Room) GetMembers() map[string]*Member { room.Load() room.createMemberCache() return room.memberCache @@ -623,7 +640,7 @@ func (room *Room) GetMembers() map[string]*mautrix.Member { // GetMember returns the member with the given MXID. // If the member doesn't exist, nil is returned. -func (room *Room) GetMember(userID string) *mautrix.Member { +func (room *Room) GetMember(userID string) *Member { if userID == room.SessionUserID && room.SessionMember != nil { return room.SessionMember } diff --git a/ui/command-processor.go b/ui/command-processor.go index ee975ad..c4dabbd 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -90,34 +90,38 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { }, commands: map[string]CommandHandler{ "unknown-command": cmdUnknownCommand, - "help": cmdHelp, - "me": cmdMe, - "quit": cmdQuit, - "clearcache": cmdClearCache, - "leave": cmdLeave, - "create": cmdCreateRoom, - "pm": cmdPrivateMessage, - "join": cmdJoin, - "kick": cmdKick, - "ban": cmdBan, - "unban": cmdUnban, - "toggle": cmdToggle, - "logout": cmdLogout, - "sendevent": cmdSendEvent, - "msendevent": cmdMSendEvent, - "setstate": cmdSetState, - "msetstate": cmdMSetState, - "roomnick": cmdRoomNick, - "rainbow": cmdRainbow, - "rainbowme": cmdRainbowMe, - "notice": cmdNotice, - "tags": cmdTags, - "tag": cmdTag, - "untag": cmdUntag, - "invite": cmdInvite, - "hprof": cmdHeapProfile, - "cprof": cmdCPUProfile, - "trace": cmdTrace, + + "id": cmdID, + "help": cmdHelp, + "me": cmdMe, + "quit": cmdQuit, + "clearcache": cmdClearCache, + "leave": cmdLeave, + "create": cmdCreateRoom, + "pm": cmdPrivateMessage, + "join": cmdJoin, + "kick": cmdKick, + "ban": cmdBan, + "unban": cmdUnban, + "toggle": cmdToggle, + "logout": cmdLogout, + "accept": cmdAccept, + "reject": cmdReject, + "sendevent": cmdSendEvent, + "msendevent": cmdMSendEvent, + "setstate": cmdSetState, + "msetstate": cmdMSetState, + "roomnick": cmdRoomNick, + "rainbow": cmdRainbow, + "rainbowme": cmdRainbowMe, + "notice": cmdNotice, + "tags": cmdTags, + "tag": cmdTag, + "untag": cmdUntag, + "invite": cmdInvite, + "hprof": cmdHeapProfile, + "cprof": cmdCPUProfile, + "trace": cmdTrace, }, } } diff --git a/ui/commands.go b/ui/commands.go index 0f7a2aa..9b1ac25 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -106,8 +106,44 @@ func cmdNotice(cmd *Command) { cmd.UI.Render() } +func cmdAccept(cmd *Command) { + room := cmd.Room.MxRoom() + if room.SessionMember.Membership != "invite" { + cmd.Reply("/accept can only be used in rooms you're invited to") + return + } + _, server, _ := mautrix.ParseUserID(room.SessionMember.Sender) + _, err := cmd.Matrix.JoinRoom(room.ID, server) + if err != nil { + cmd.Reply("Failed to accept invite:", err) + } else { + cmd.Reply("Successfully accepted invite") + } +} + +func cmdReject(cmd *Command) { + room := cmd.Room.MxRoom() + if room.SessionMember.Membership != "invite" { + cmd.Reply("/reject can only be used in rooms you're invited to") + return + } + err := cmd.Matrix.LeaveRoom(room.ID) + if err != nil { + cmd.Reply("Failed to reject invite: %v", err) + } else { + cmd.Reply("Successfully accepted invite") + } +} + +func cmdID(cmd *Command) { + cmd.Reply("The internal ID of this room is %s", cmd.Room.MxRoom().ID) +} + func cmdTags(cmd *Command) { tags := cmd.Room.MxRoom().RawTags + if len(cmd.Args) > 0 && cmd.Args[0] == "--internal" { + tags = cmd.Room.MxRoom().Tags() + } if len(tags) == 0 { if cmd.Room.MxRoom().IsDirect { cmd.Reply("This room has no tags, but it's marked as a direct chat.") @@ -142,7 +178,18 @@ func cmdTag(cmd *Command) { return } } - err := cmd.Matrix.Client().AddTag(cmd.Room.MxRoom().ID, cmd.Args[0], order) + var err error + if len(cmd.Args) > 2 && cmd.Args[2] == "--reset" { + tags := mautrix.Tags{ + cmd.Args[0]: {Order: json.Number(fmt.Sprintf("%f", order))}, + } + for _, tag := range cmd.Room.MxRoom().RawTags { + tags[tag.Tag] = mautrix.Tag{Order: tag.Order} + } + err = cmd.Matrix.Client().SetTags(cmd.Room.MxRoom().ID, tags) + } else { + err = cmd.Matrix.Client().AddTag(cmd.Room.MxRoom().ID, cmd.Args[0], order) + } if err != nil { cmd.Reply("Failed to add tag:", err) } diff --git a/ui/member-list.go b/ui/member-list.go index aaddcfb..e836e18 100644 --- a/ui/member-list.go +++ b/ui/member-list.go @@ -27,6 +27,7 @@ import ( "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/widget" ) @@ -39,7 +40,7 @@ func NewMemberList() *MemberList { } type memberListItem struct { - mautrix.Member + rooms.Member PowerLevel int Sigil rune UserID string @@ -63,7 +64,7 @@ func (rml roomMemberList) Swap(i, j int) { rml[i], rml[j] = rml[j], rml[i] } -func (ml *MemberList) Update(data map[string]*mautrix.Member, levels *mautrix.PowerLevels) *MemberList { +func (ml *MemberList) Update(data map[string]*rooms.Member, levels *mautrix.PowerLevels) *MemberList { ml.list = make(roomMemberList, len(data)) i := 0 highestLevel := math.MinInt32 diff --git a/ui/room-list.go b/ui/room-list.go index 2b8af87..31e4c2a 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -19,6 +19,7 @@ package ui import ( "math" "regexp" + "sort" "strings" sync "github.com/sasha-s/go-deadlock" @@ -30,13 +31,43 @@ import ( "maunium.net/go/gomuks/matrix/rooms" ) +var tagOrder = map[string]int{ + "net.maunium.gomuks.fake.invite": 4, + "m.favourite": 3, + "net.maunium.gomuks.fake.direct": 2, + "": 1, + "m.lowpriority": -1, + "m.server_notice": -2, + "net.maunium.gomuks.fake.leave": -3, +} + +// TagNameList is a list of Matrix tag names where default names are sorted in a hardcoded way. +type TagNameList []string + +func (tnl TagNameList) Len() int { + return len(tnl) +} + +func (tnl TagNameList) Less(i, j int) bool { + orderI, _ := tagOrder[tnl[i]] + orderJ, _ := tagOrder[tnl[j]] + if orderI != orderJ { + return orderI > orderJ + } + return strings.Compare(tnl[i], tnl[j]) > 0 +} + +func (tnl TagNameList) Swap(i, j int) { + tnl[i], tnl[j] = tnl[j], tnl[i] +} + type RoomList struct { sync.RWMutex parent *MainView // The list of tags in display order. - tags []string + tags TagNameList // The list of rooms, in reverse order. items map[string]*TagRoomList // The selected room. @@ -107,13 +138,14 @@ func (list *RoomList) checkTag(tag string) { //delete(list.items, tag) ok = false } + debug.Print("Checking", tag, index, trl.IsEmpty(), ok) if ok && index == -1 { list.tags = append(list.tags, tag) - } /* TODO this doesn't work properly - else if index != -1 { + sort.Sort(list.tags) + } else if !ok && index != -1 { list.tags = append(list.tags[0:index], list.tags[index+1:]...) - }*/ + } } func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) { @@ -122,10 +154,9 @@ func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) { trl, ok := list.items[tag.Tag] if !ok { list.items[tag.Tag] = NewTagRoomList(list, tag.Tag, NewDefaultOrderedRoom(room)) - return + } else { + trl.Insert(tag.Order, room) } - - trl.Insert(tag.Order, room) list.checkTag(tag.Tag) } @@ -412,11 +443,11 @@ func (list *RoomList) ContentHeight() (height int) { return } -func (list *RoomList) OnKeyEvent(event mauview.KeyEvent) bool { +func (list *RoomList) OnKeyEvent(_ mauview.KeyEvent) bool { return false } -func (list *RoomList) OnPasteEvent(event mauview.PasteEvent) bool { +func (list *RoomList) OnPasteEvent(_ mauview.PasteEvent) bool { return false } @@ -517,6 +548,10 @@ func (list *RoomList) GetTagDisplayName(tag string) string { return "System Alerts" case tag == "net.maunium.gomuks.fake.direct": return "People" + case tag == "net.maunium.gomuks.fake.invite": + return "Invites" + case tag == "net.maunium.gomuks.fake.leave": + return "Historical" case strings.HasPrefix(tag, "u."): return tag[len("u."):] case !nsRegex.MatchString(tag): diff --git a/ui/tag-room-list.go b/ui/tag-room-list.go index dfc7cb2..d382400 100644 --- a/ui/tag-room-list.go +++ b/ui/tag-room-list.go @@ -17,10 +17,12 @@ package ui import ( + "encoding/json" "fmt" "strconv" "strings" + "maunium.net/go/gomuks/debug" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -30,10 +32,10 @@ import ( type OrderedRoom struct { *rooms.Room - order string + order json.Number } -func NewOrderedRoom(order string, room *rooms.Room) *OrderedRoom { +func NewOrderedRoom(order json.Number, room *rooms.Room) *OrderedRoom { return &OrderedRoom{ Room: room, order: order, @@ -153,23 +155,25 @@ func (trl *TagRoomList) HasVisibleRooms() bool { // ShouldBeBefore returns if the first room should be after the second room in the room list. // The manual order and last received message timestamp are considered. func (trl *TagRoomList) ShouldBeAfter(room1 *OrderedRoom, room2 *OrderedRoom) bool { - orderComp := strings.Compare(room1.order, room2.order) + orderComp := strings.Compare(string(room1.order), string(room2.order)) return orderComp == 1 || (orderComp == 0 && room2.LastReceivedMessage.After(room1.LastReceivedMessage)) } -func (trl *TagRoomList) Insert(order string, mxRoom *rooms.Room) { +func (trl *TagRoomList) Insert(order json.Number, mxRoom *rooms.Room) { room := NewOrderedRoom(order, mxRoom) - trl.rooms = append(trl.rooms, nil) // The default insert index is the newly added slot. // That index will be used if all other rooms in the list have the same LastReceivedMessage timestamp. - insertAt := len(trl.rooms) - 1 + insertAt := len(trl.rooms) // Find the spot where the new room should be put according to the last received message timestamps. for i := 0; i < len(trl.rooms)-1; i++ { - if trl.ShouldBeAfter(room, trl.rooms[i]) { + if trl.rooms[i].Room == mxRoom { + debug.Printf("Warning: tried to re-insert room %s into tag %s", mxRoom.ID, trl.name) + return + } else if trl.ShouldBeAfter(room, trl.rooms[i]) { insertAt = i - break } } + trl.rooms = append(trl.rooms, nil) // Move newer rooms forward in the array. for i := len(trl.rooms) - 1; i > insertAt; i-- { trl.rooms[i] = trl.rooms[i-1] @@ -207,7 +211,12 @@ func (trl *TagRoomList) RemoveIndex(index int) { if index < 0 || index > len(trl.rooms) { return } - trl.rooms = append(trl.rooms[0:index], trl.rooms[index+1:]...) + last := len(trl.rooms) - 1 + if index < last { + copy(trl.rooms[index:], trl.rooms[index+1:]) + } + trl.rooms[last] = nil + trl.rooms = trl.rooms[:last] } func (trl *TagRoomList) Index(room *rooms.Room) int { -- cgit v1.2.3