From 2a0145db884038342ba00cdb29fc29085e1faace Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 24 Apr 2018 02:13:17 +0300 Subject: [PATCH] Handle tag events --- interface/ui.go | 2 + matrix/matrix.go | 21 ++- matrix/rooms/room.go | 19 ++- ui/room-list.go | 384 ++++++++++++++++++++++++++++++++----------- ui/view-main.go | 55 ++++++- 5 files changed, 371 insertions(+), 110 deletions(-) diff --git a/interface/ui.go b/interface/ui.go index ec85b0f..c36819a 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -54,6 +54,8 @@ type MainView interface { SetRooms(roomIDs []string) SaveAllHistory() + UpdateTags(room *rooms.Room, newTags []rooms.RoomTag) + SetTyping(roomID string, users []string) ParseEvent(roomView RoomView, evt *gomatrix.Event) Message diff --git a/matrix/matrix.go b/matrix/matrix.go index 87013ac..d58ce20 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -18,7 +18,6 @@ package matrix import ( "bytes" - "encoding/json" "fmt" "io" "io/ioutil" @@ -255,9 +254,23 @@ func (c *Container) HandlePushRules(evt *gomatrix.Event) { // HandleTag is the event handler for the m.tag account data event. func (c *Container) HandleTag(evt *gomatrix.Event) { - debug.Print("Received updated tags for", evt.RoomID) - dat, _ := json.MarshalIndent(&evt.Content, "", " ") - debug.Print(string(dat)) + room := c.config.Session.GetRoom(evt.RoomID) + + tags, _ := evt.Content["tags"].(map[string]interface{}) + newTags := make([]rooms.RoomTag, len(tags)) + index := 0 + for tag, infoifc := range tags { + info, _ := infoifc.(map[string]interface{}) + order, _ := info["order"].(float64) + newTags[index] = rooms.RoomTag{ + Tag: tag, + Order: order, + } + index++ + } + + mainView := c.ui.MainView() + mainView.UpdateTags(room, newTags) } func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 7b4a8b5..44a386b 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -34,6 +34,14 @@ const ( MemberRoomName ) +// RoomTag is a tag given to a specific room. +type RoomTag struct { + // The name of the tag. + Tag string + // The order of the tag. Smaller values are ordered higher. + Order float64 +} + // Room represents a single Matrix room. type Room struct { *gomatrix.Room @@ -53,7 +61,9 @@ type Room struct { // a notificationless message like bot notices. HasNewMessages bool - Tags []string + // List of tags given to this room + RawTags []RoomTag + // Timestamp of previously received actual message. LastReceivedMessage time.Time // MXID -> Member cache calculated from membership events. @@ -102,6 +112,13 @@ func (room *Room) MarkRead() { room.HasNewMessages = false } +func (room *Room) Tags() []RoomTag { + if len(room.RawTags) == 0 { + return []RoomTag{{"", 0.5}} + } + return room.RawTags +} + // 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) { diff --git a/ui/room-list.go b/ui/room-list.go index cad1b5a..a70fd68 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -18,7 +18,9 @@ package ui import ( "fmt" + "regexp" "strconv" + "strings" "time" "maunium.net/go/gomuks/matrix/rooms" @@ -27,13 +29,21 @@ import ( "maunium.net/go/tview" ) +type roomListItem struct { + room *rooms.Room + priority float64 +} + type RoomList struct { *tview.Box + // The list of tags in display order. + tags []string // The list of rooms, in reverse order. - items []*rooms.Room + items map[string][]*rooms.Room // The selected room. - selected *rooms.Room + selected *rooms.Room + selectedTag string // The item main text color. mainTextColor tcell.Color @@ -46,7 +56,7 @@ type RoomList struct { func NewRoomList() *RoomList { return &RoomList{ Box: tview.NewBox(), - items: []*rooms.Room{}, + items: make(map[string][]*rooms.Room), mainTextColor: tcell.ColorWhite, selectedTextColor: tcell.ColorWhite, @@ -55,107 +65,248 @@ func NewRoomList() *RoomList { } func (list *RoomList) Contains(roomID string) bool { - for _, room := range list.items { - if room.ID == roomID { - return true + for _, roomList := range list.items { + for _, room := range roomList { + if room.ID == roomID { + return true + } } } return false } func (list *RoomList) Add(room *rooms.Room) { - list.items = append(list.items, nil) - insertAt := len(list.items) - 1 - for i := 0; i < len(list.items)-1; i++ { - if list.items[i].LastReceivedMessage.After(room.LastReceivedMessage) { + for _, tag := range room.Tags() { + list.AddToTag(tag.Tag, room) + } +} + +func (list *RoomList) CheckTag(tag string) { + index := list.IndexTag(tag) + + items, ok := list.items[tag] + + if len(items) == 0 { + delete(list.items, tag) + ok = false + } + + if ok && index == -1 { + list.tags = append(list.tags, tag) + } else if index != -1 { + list.tags = append(list.tags[0:index], list.tags[index+1:]...) + } +} + +func (list *RoomList) AddToTag(tag string, room *rooms.Room) { + items, ok := list.items[tag] + if !ok { + list.items[tag] = []*rooms.Room{room} + return + } + + // Add space for new item. + items = append(items, 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(items) - 1 + // Find the spot where the new room should be put according to the last received message timestamps. + for i := 0; i < len(items)-1; i++ { + if items[i].LastReceivedMessage.After(room.LastReceivedMessage) { insertAt = i break } } - for i := len(list.items) - 1; i > insertAt; i-- { - list.items[i] = list.items[i-1] + // Move newer rooms forward in the array. + for i := len(items) - 1; i > insertAt; i-- { + items[i] = items[i-1] } - list.items[insertAt] = room + // Insert room. + items[insertAt] = room + + list.items[tag] = items + list.CheckTag(tag) } func (list *RoomList) Remove(room *rooms.Room) { - index := list.Index(room) - if index != -1 { - list.items = append(list.items[0:index], list.items[index+1:]...) - if room == list.selected { - if index > 0 { - list.selected = list.items[index-1] - } else if len(list.items) > 0 { - list.selected = list.items[0] - } else { - list.selected = nil + for _, tag := range room.Tags() { + list.RemoveFromTag(tag.Tag, room) + } +} + +func (list *RoomList) RemoveFromTag(tag string, room *rooms.Room) { + items, ok := list.items[tag] + if !ok { + return + } + + index := list.indexInTag(tag, room) + if index == -1 { + return + } + + items = append(items[0:index], items[index+1:]...) + + if len(items) == 0 { + delete(list.items, tag) + } else { + list.items[tag] = items + } + + if room == list.selected { + // Room is currently selected, move selection to another room. + if index > 0 { + list.selected = items[index-1] + } else if len(items) > 0 { + list.selected = items[0] + } else if len(list.items) > 0 { + for _, tag := range list.tags { + moreItems := list.items[tag] + if len(moreItems) > 0 { + list.selected = moreItems[0] + list.selectedTag = tag + } } + } else { + list.selected = nil + list.selectedTag = "" } } + list.CheckTag(tag) } func (list *RoomList) Bump(room *rooms.Room) { + for _, tag := range room.Tags() { + list.bumpInTag(tag.Tag, room) + } +} + +func (list *RoomList) bumpInTag(tag string, room *rooms.Room) { + items, ok := list.items[tag] + if !ok { + return + } + found := false - for i := 0; i < len(list.items)-1; i++ { - if list.items[i] == room { + for i := 0; i < len(items)-1; i++ { + if items[i] == room { found = true } if found { - list.items[i] = list.items[i+1] + items[i] = items[i+1] } } - list.items[len(list.items)-1] = room - room.LastReceivedMessage = time.Now() + if found { + items[len(items)-1] = room + room.LastReceivedMessage = time.Now() + } } func (list *RoomList) Clear() { - list.items = []*rooms.Room{} + list.items = make(map[string][]*rooms.Room) list.selected = nil + list.selectedTag = "" } -func (list *RoomList) SetSelected(room *rooms.Room) { +func (list *RoomList) SetSelected(tag string, room *rooms.Room) { list.selected = room + list.selectedTag = "" } func (list *RoomList) HasSelected() bool { return list.selected != nil } -func (list *RoomList) Selected() *rooms.Room { +func (list *RoomList) Selected() (string, *rooms.Room) { + return list.selectedTag, list.selected +} + +func (list *RoomList) SelectedRoom() *rooms.Room { return list.selected } -func (list *RoomList) Previous() *rooms.Room { - if len(list.items) == 0 { - return nil - } else if list.selected == nil { - return list.items[0] +func (list *RoomList) First() (string, *rooms.Room) { + for _, tag := range list.tags { + items := list.items[tag] + if len(items) > 0 { + return tag, items[0] + } } - - index := list.Index(list.selected) - if index == len(list.items)-1 { - return list.items[0] - } - return list.items[index+1] + return "", nil } -func (list *RoomList) Next() *rooms.Room { +func (list *RoomList) Last() (string, *rooms.Room) { + for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- { + tag := list.tags[tagIndex] + items := list.items[tag] + if len(items) > 0 { + return tag, items[len(items)-1] + } + } + return "", nil +} + +func (list *RoomList) IndexTag(tag string) int { + for index, entry := range list.tags { + if tag == entry { + return index + } + } + return -1 +} + +func (list *RoomList) Previous() (string, *rooms.Room) { if len(list.items) == 0 { - return nil + return "", nil } else if list.selected == nil { - return list.items[0] + return list.First() } - index := list.Index(list.selected) + items := list.items[list.selectedTag] + index := list.indexInTag(list.selectedTag, list.selected) + if index == len(items)-1 { + tagIndex := list.IndexTag(list.selectedTag) + tagIndex++ + for ; tagIndex < len(list.tags); tagIndex++ { + nextTag := list.tags[tagIndex] + nextTagItems := list.items[nextTag] + if len(nextTagItems) > 0 { + return nextTag, nextTagItems[0] + } + } + return list.First() + } + return list.selectedTag, items[index+1] +} + +func (list *RoomList) Next() (string, *rooms.Room) { + if len(list.items) == 0 { + return "", nil + } else if list.selected == nil { + return list.First() + } + + items := list.items[list.selectedTag] + index := list.indexInTag(list.selectedTag, list.selected) if index == 0 { - return list.items[len(list.items)-1] + tagIndex := list.IndexTag(list.selectedTag) + tagIndex-- + for ; tagIndex >= 0; tagIndex-- { + prevTag := list.tags[tagIndex] + prevTagItems := list.items[prevTag] + if len(prevTagItems) > 0 { + return prevTag, prevTagItems[len(prevTagItems)-1] + } + } + return list.Last() } - return list.items[index-1] + return list.selectedTag, items[index-1] } -func (list *RoomList) Index(room *rooms.Room) int { +func (list *RoomList) indexInTag(tag string, room *rooms.Room) int { roomIndex := -1 - for index, entry := range list.items { + items := list.items[tag] + for index, entry := range items { if entry == room { roomIndex = index break @@ -164,11 +315,46 @@ func (list *RoomList) Index(room *rooms.Room) int { return roomIndex } -func (list *RoomList) Get(n int) *rooms.Room { - if n < 0 || n > len(list.items)-1 { - return nil +func (list *RoomList) Get(n int) (string, *rooms.Room) { + if n < 0 { + return "", nil + } + for _, tag := range list.tags { + // Tag header + n-- + + items := list.items[tag] + if n < 0 { + return "", nil + } else if n < len(items) { + return tag, items[len(items)-1-n] + } + + // Tag items + n -= len(items) + } + return "", nil +} + +var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$") + +func (list *RoomList) GetTagDisplayName(tag string) string { + switch { + case len(tag) == 0: + return "Rooms" + case tag == "m.favourite": + return "Favorites" + case tag == "m.lowpriority": + return "Low Priority" + case strings.HasPrefix(tag, "m."): + return strings.Title(strings.Replace(tag[len("m."):], "_", " ", -1)) + case strings.HasPrefix(tag, "u."): + return tag[len("u."):] + case !nsRegex.MatchString(tag): + return tag + default: + return "" } - return list.items[len(list.items)-1-n] } // Draw draws this primitive onto the screen. @@ -179,54 +365,60 @@ func (list *RoomList) Draw(screen tcell.Screen) { bottomLimit := y + height var offset int + /* TODO fix offset currentItemIndex := list.Index(list.selected) if currentItemIndex >= height { offset = currentItemIndex + 1 - height - } + }*/ // Draw the list items. - for i := len(list.items) - 1; i >= 0; i-- { - item := list.items[i] - index := len(list.items) - 1 - i - - if index < offset { - continue - } - - if y >= bottomLimit { - break - } - - text := item.GetTitle() - - lineWidth := width - - style := tcell.StyleDefault.Foreground(list.mainTextColor) - if item == list.selected { - style = style.Foreground(list.selectedTextColor).Background(list.selectedBackgroundColor) - } - if item.HasNewMessages { - style = style.Bold(true) - } - - if item.UnreadMessages > 0 { - unreadMessageCount := "99+" - if item.UnreadMessages < 100 { - unreadMessageCount = strconv.Itoa(item.UnreadMessages) - } - 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.AlignLeft, text, x, y, lineWidth, style) - + for _, tag := range list.tags { + items := list.items[tag] + widget.WriteLine(screen, tview.AlignLeft, list.GetTagDisplayName(tag), x, y, width, tcell.StyleDefault.Underline(true).Bold(true)) y++ - if y >= bottomLimit { - break + for i := len(items) - 1; i >= 0; i-- { + item := items[i] + index := len(items) - 1 - i + + if index < offset { + continue + } + + if y >= bottomLimit { + break + } + + text := item.GetTitle() + + lineWidth := width + + style := tcell.StyleDefault.Foreground(list.mainTextColor) + if tag == list.selectedTag && item == list.selected { + style = style.Foreground(list.selectedTextColor).Background(list.selectedBackgroundColor) + } + if item.HasNewMessages { + style = style.Bold(true) + } + + if item.UnreadMessages > 0 { + unreadMessageCount := "99+" + if item.UnreadMessages < 100 { + unreadMessageCount = strconv.Itoa(item.UnreadMessages) + } + 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.AlignLeft, text, x, y, lineWidth, style) + + y++ + if y >= bottomLimit { + break + } } } } diff --git a/ui/view-main.go b/ui/view-main.go index d1af09d..ba3a55b 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -248,7 +248,7 @@ func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMo return event } -func (view *MainView) SwitchRoom(room *rooms.Room) { +func (view *MainView) SwitchRoom(tag string, room *rooms.Room) { if room == nil { return } @@ -258,13 +258,13 @@ func (view *MainView) SwitchRoom(room *rooms.Room) { if roomView.MessageView().ScrollOffset == 0 { roomView.Room.MarkRead() } - view.roomList.SetSelected(room) + view.roomList.SetSelected(tag, room) view.parent.app.SetFocus(view) view.parent.Render() } func (view *MainView) Focus(delegate func(p tview.Primitive)) { - room := view.roomList.Selected() + room := view.roomList.SelectedRoom() if room != nil { roomView, ok := view.rooms[room.ID] if ok { @@ -283,7 +283,6 @@ func (view *MainView) SaveAllHistory() { } func (view *MainView) addRoomPage(room *rooms.Room) { - if !view.roomView.HasPage(room.ID) { roomView := NewRoomView(view, room). SetInputSubmitFunc(view.InputSubmit). @@ -304,7 +303,13 @@ func (view *MainView) addRoomPage(room *rooms.Room) { } func (view *MainView) GetRoom(roomID string) ifc.RoomView { - return view.rooms[roomID] + room, ok := view.rooms[roomID] + if !ok { + view.AddRoom(roomID) + room, _ := view.rooms[roomID] + return room + } + return room } func (view *MainView) AddRoom(roomID string) { @@ -335,14 +340,46 @@ func (view *MainView) SetRooms(roomIDs []string) { view.roomList.Clear() view.roomView.Clear() view.rooms = make(map[string]*RoomView) - for index, roomID := range roomIDs { + for _, roomID := range roomIDs { room := view.matrix.GetRoom(roomID) view.roomList.Add(room) view.addRoomPage(room) - if index == len(roomIDs)-1 { - view.SwitchRoom(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("", room) + } else if len(room.RawTags) == 0 { + view.roomList.RemoveFromTag("", room) + for _, tag := range newTags { + view.roomList.AddToTag(tag.Tag, room) + } + } else { + NewTags: + for _, newTag := range newTags { + for _, oldTag := range room.RawTags { + if newTag.Tag == oldTag.Tag { + continue NewTags + } + } + view.roomList.AddToTag(newTag.Tag, 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 } func (view *MainView) SetTyping(room string, users []string) { @@ -362,7 +399,7 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) { // Whether or not the room where the message came is the currently shown room. - isCurrent := room == view.roomList.Selected() + isCurrent := room == view.roomList.SelectedRoom() // Whether or not the terminal window is focused. isFocused := view.lastFocusTime.Add(30 * time.Second).Before(time.Now())