2018-05-22 17:30:25 +02:00
|
|
|
// gomuks - A terminal Matrix client written in Go.
|
2020-04-19 17:10:14 +02:00
|
|
|
// Copyright (C) 2020 Tulir Asokan
|
2018-05-22 17:30:25 +02:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
2019-01-17 13:13:25 +01:00
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
2018-05-22 17:30:25 +02:00
|
|
|
// 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
|
2019-01-17 13:13:25 +01:00
|
|
|
// GNU Affero General Public License for more details.
|
2018-05-22 17:30:25 +02:00
|
|
|
//
|
2019-01-17 13:13:25 +01:00
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2018-05-22 17:30:25 +02:00
|
|
|
|
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
2020-02-22 00:17:52 +01:00
|
|
|
"encoding/json"
|
2018-05-22 17:30:25 +02:00
|
|
|
"fmt"
|
2020-03-01 21:00:42 +01:00
|
|
|
"math"
|
2018-10-18 16:02:38 +02:00
|
|
|
"strconv"
|
|
|
|
|
2020-02-22 00:17:52 +01:00
|
|
|
"maunium.net/go/gomuks/debug"
|
2019-03-25 23:37:35 +01:00
|
|
|
"maunium.net/go/mauview"
|
2018-05-22 17:43:00 +02:00
|
|
|
"maunium.net/go/tcell"
|
2019-01-17 13:13:25 +01:00
|
|
|
|
|
|
|
"maunium.net/go/gomuks/matrix/rooms"
|
|
|
|
"maunium.net/go/gomuks/ui/widget"
|
2018-05-22 17:30:25 +02:00
|
|
|
)
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
type OrderedRoom struct {
|
2018-05-22 17:30:25 +02:00
|
|
|
*rooms.Room
|
2020-03-01 21:00:42 +01:00
|
|
|
order float64
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
2020-02-22 00:17:52 +01:00
|
|
|
func NewOrderedRoom(order json.Number, room *rooms.Room) *OrderedRoom {
|
2020-03-01 21:00:42 +01:00
|
|
|
numOrder, err := order.Float64()
|
|
|
|
if err != nil {
|
|
|
|
numOrder = 0.5
|
|
|
|
}
|
2018-05-22 17:38:54 +02:00
|
|
|
return &OrderedRoom{
|
2018-05-22 17:30:25 +02:00
|
|
|
Room: room,
|
2020-03-01 21:00:42 +01:00
|
|
|
order: numOrder,
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom {
|
|
|
|
return NewOrderedRoom("0.5", room)
|
|
|
|
}
|
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
func (or *OrderedRoom) Draw(roomList *RoomList, screen mauview.Screen, x, y, lineWidth int, isSelected bool) {
|
2018-05-22 17:38:54 +02:00
|
|
|
style := tcell.StyleDefault.
|
|
|
|
Foreground(roomList.mainTextColor).
|
|
|
|
Bold(or.HasNewMessages())
|
|
|
|
if isSelected {
|
|
|
|
style = style.
|
|
|
|
Foreground(roomList.selectedTextColor).
|
|
|
|
Background(roomList.selectedBackgroundColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
unreadCount := or.UnreadCount()
|
2018-05-25 07:12:22 +02:00
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLinePadded(screen, mauview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
|
2018-05-25 07:12:22 +02:00
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
if unreadCount > 0 {
|
|
|
|
unreadMessageCount := "99+"
|
|
|
|
if unreadCount < 100 {
|
|
|
|
unreadMessageCount = strconv.Itoa(unreadCount)
|
|
|
|
}
|
|
|
|
if or.Highlighted() {
|
|
|
|
unreadMessageCount += "!"
|
|
|
|
}
|
|
|
|
unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLine(screen, mauview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
|
2018-05-22 17:38:54 +02:00
|
|
|
lineWidth -= len(unreadMessageCount)
|
|
|
|
}
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type TagRoomList struct {
|
2019-03-25 23:37:35 +01:00
|
|
|
mauview.NoopEventHandler
|
2020-03-01 21:00:42 +01:00
|
|
|
// The list of rooms in the list, in reverse order
|
|
|
|
rooms []*OrderedRoom
|
|
|
|
// Maximum number of rooms to show
|
|
|
|
maxShown int
|
|
|
|
// The internal name of this tag
|
|
|
|
name string
|
|
|
|
// The displayname of this tag
|
2018-05-22 17:30:25 +02:00
|
|
|
displayname string
|
2020-03-01 21:00:42 +01:00
|
|
|
// The parent RoomList instance
|
|
|
|
parent *RoomList
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList {
|
2018-05-22 17:30:25 +02:00
|
|
|
return &TagRoomList{
|
|
|
|
maxShown: 10,
|
|
|
|
rooms: rooms,
|
|
|
|
name: name,
|
|
|
|
displayname: parent.GetTagDisplayName(name),
|
|
|
|
parent: parent,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
func (trl *TagRoomList) Visible() []*OrderedRoom {
|
2018-05-22 17:30:25 +02:00
|
|
|
return trl.rooms[len(trl.rooms)-trl.Length():]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) FirstVisible() *rooms.Room {
|
|
|
|
visible := trl.Visible()
|
|
|
|
if len(visible) > 0 {
|
|
|
|
return visible[len(visible)-1].Room
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) LastVisible() *rooms.Room {
|
|
|
|
visible := trl.Visible()
|
|
|
|
if len(visible) > 0 {
|
|
|
|
return visible[0].Room
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
func (trl *TagRoomList) All() []*OrderedRoom {
|
2018-05-22 17:30:25 +02:00
|
|
|
return trl.rooms
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) Length() int {
|
|
|
|
if len(trl.rooms) < trl.maxShown {
|
|
|
|
return len(trl.rooms)
|
|
|
|
}
|
|
|
|
return trl.maxShown
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) TotalLength() int {
|
|
|
|
return len(trl.rooms)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) IsEmpty() bool {
|
|
|
|
return len(trl.rooms) == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) IsCollapsed() bool {
|
|
|
|
return trl.maxShown == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) ToggleCollapse() {
|
|
|
|
if trl.IsCollapsed() {
|
|
|
|
trl.maxShown = 10
|
|
|
|
} else {
|
|
|
|
trl.maxShown = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) HasInvisibleRooms() bool {
|
|
|
|
return trl.maxShown < trl.TotalLength()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) HasVisibleRooms() bool {
|
|
|
|
return !trl.IsEmpty() && trl.maxShown > 0
|
|
|
|
}
|
|
|
|
|
2020-03-01 21:00:42 +01:00
|
|
|
const equalityThreshold = 1e-6
|
|
|
|
|
|
|
|
func almostEqual(a, b float64) bool {
|
|
|
|
return math.Abs(a-b) <= equalityThreshold
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShouldBeAfter returns if the first room should be after the second room in the room list.
|
2018-05-22 17:30:25 +02:00
|
|
|
// The manual order and last received message timestamp are considered.
|
2018-05-22 17:38:54 +02:00
|
|
|
func (trl *TagRoomList) ShouldBeAfter(room1 *OrderedRoom, room2 *OrderedRoom) bool {
|
2020-03-03 19:58:33 +01:00
|
|
|
// Lower order value = higher in list
|
|
|
|
return room1.order > room2.order ||
|
|
|
|
// Equal order value and more recent message = higher in the list
|
|
|
|
(almostEqual(room1.order, room2.order) && room2.LastReceivedMessage.After(room1.LastReceivedMessage))
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
2020-02-22 00:17:52 +01:00
|
|
|
func (trl *TagRoomList) Insert(order json.Number, mxRoom *rooms.Room) {
|
2018-05-22 17:38:54 +02:00
|
|
|
room := NewOrderedRoom(order, mxRoom)
|
2018-05-22 17:30:25 +02:00
|
|
|
// 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.
|
2020-02-22 00:17:52 +01:00
|
|
|
insertAt := len(trl.rooms)
|
2018-05-22 17:30:25 +02:00
|
|
|
// Find the spot where the new room should be put according to the last received message timestamps.
|
2020-03-01 21:00:42 +01:00
|
|
|
for i := 0; i < len(trl.rooms); i++ {
|
2020-02-22 00:17:52 +01:00
|
|
|
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]) {
|
2018-05-22 17:30:25 +02:00
|
|
|
insertAt = i
|
2020-03-01 21:00:42 +01:00
|
|
|
break
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
}
|
2020-02-22 00:17:52 +01:00
|
|
|
trl.rooms = append(trl.rooms, nil)
|
2020-03-03 13:01:27 +01:00
|
|
|
copy(trl.rooms[insertAt+1:], trl.rooms[insertAt:len(trl.rooms)-1])
|
2018-05-22 17:30:25 +02:00
|
|
|
trl.rooms[insertAt] = room
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) Bump(mxRoom *rooms.Room) {
|
2020-03-01 21:00:42 +01:00
|
|
|
var roomBeingBumped *OrderedRoom
|
2018-05-22 17:30:25 +02:00
|
|
|
for i := 0; i < len(trl.rooms); i++ {
|
2020-03-01 21:00:42 +01:00
|
|
|
currentIndexRoom := trl.rooms[i]
|
|
|
|
if roomBeingBumped != nil {
|
|
|
|
if trl.ShouldBeAfter(roomBeingBumped, currentIndexRoom) {
|
2018-05-22 17:30:25 +02:00
|
|
|
// This room should be after the room being bumped, so insert the
|
|
|
|
// room being bumped here and return
|
2020-03-01 21:00:42 +01:00
|
|
|
trl.rooms[i-1] = roomBeingBumped
|
2018-05-22 17:30:25 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// Move older rooms back in the array
|
2020-03-01 21:00:42 +01:00
|
|
|
trl.rooms[i-1] = currentIndexRoom
|
|
|
|
} else if currentIndexRoom.Room == mxRoom {
|
|
|
|
roomBeingBumped = currentIndexRoom
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
}
|
2020-03-03 13:01:27 +01:00
|
|
|
if roomBeingBumped == nil {
|
|
|
|
debug.Print("Warning: couldn't find room", mxRoom.ID, mxRoom.NameCache, "to bump in tag", trl.name)
|
|
|
|
return
|
|
|
|
}
|
2018-05-22 17:30:25 +02:00
|
|
|
// If the room being bumped should be first in the list, it won't be inserted during the loop.
|
2020-03-01 21:00:42 +01:00
|
|
|
trl.rooms[len(trl.rooms)-1] = roomBeingBumped
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) Remove(room *rooms.Room) {
|
|
|
|
trl.RemoveIndex(trl.Index(room))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) RemoveIndex(index int) {
|
|
|
|
if index < 0 || index > len(trl.rooms) {
|
|
|
|
return
|
|
|
|
}
|
2020-02-22 00:17:52 +01:00
|
|
|
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]
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) Index(room *rooms.Room) int {
|
|
|
|
return trl.indexInList(trl.All(), room)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (trl *TagRoomList) IndexVisible(room *rooms.Room) int {
|
|
|
|
return trl.indexInList(trl.Visible(), room)
|
|
|
|
}
|
|
|
|
|
2018-05-22 17:38:54 +02:00
|
|
|
func (trl *TagRoomList) indexInList(list []*OrderedRoom, room *rooms.Room) int {
|
2018-05-22 17:30:25 +02:00
|
|
|
for index, entry := range list {
|
|
|
|
if entry.Room == room {
|
|
|
|
return index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
var TagDisplayNameStyle = tcell.StyleDefault.Underline(true).Bold(true)
|
|
|
|
var TagRoomCountStyle = tcell.StyleDefault.Italic(true)
|
|
|
|
|
|
|
|
func (trl *TagRoomList) RenderHeight() int {
|
|
|
|
if len(trl.displayname) == 0 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if trl.IsCollapsed() {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
height := 2 + trl.Length()
|
|
|
|
if trl.HasInvisibleRooms() || trl.maxShown > 10 {
|
|
|
|
height++
|
|
|
|
}
|
|
|
|
return height
|
|
|
|
}
|
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
func (trl *TagRoomList) DrawHeader(screen mauview.Screen) {
|
|
|
|
width, _ := screen.Size()
|
2018-05-22 17:30:25 +02:00
|
|
|
roomCount := strconv.Itoa(trl.TotalLength())
|
|
|
|
|
|
|
|
// Draw tag name
|
|
|
|
displayNameWidth := width - 1 - len(roomCount)
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLine(screen, mauview.AlignLeft, trl.displayname, 0, 0, displayNameWidth, TagDisplayNameStyle)
|
2018-05-22 17:30:25 +02:00
|
|
|
|
|
|
|
// Draw tag room count
|
2019-03-25 23:37:35 +01:00
|
|
|
roomCountX := len(trl.displayname) + 1
|
2018-05-22 17:30:25 +02:00
|
|
|
roomCountWidth := width - 2 - len(trl.displayname)
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLine(screen, mauview.AlignLeft, roomCount, roomCountX, 0, roomCountWidth, TagRoomCountStyle)
|
2018-05-22 17:38:54 +02:00
|
|
|
}
|
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
func (trl *TagRoomList) Draw(screen mauview.Screen) {
|
2018-05-22 17:38:54 +02:00
|
|
|
if len(trl.displayname) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
trl.DrawHeader(screen)
|
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
width, height := screen.Size()
|
2018-05-22 17:30:25 +02:00
|
|
|
|
|
|
|
items := trl.Visible()
|
|
|
|
|
|
|
|
if trl.IsCollapsed() {
|
2019-03-25 23:37:35 +01:00
|
|
|
screen.SetCell(width-1, 0, tcell.StyleDefault, '▶')
|
2018-05-22 17:30:25 +02:00
|
|
|
return
|
|
|
|
}
|
2019-03-25 23:37:35 +01:00
|
|
|
screen.SetCell(width-1, 0, tcell.StyleDefault, '▼')
|
2018-05-22 17:30:25 +02:00
|
|
|
|
2019-03-25 23:37:35 +01:00
|
|
|
y := 1
|
2019-04-27 14:02:21 +02:00
|
|
|
for i := len(items) - 1; i >= 0; i-- {
|
2019-03-25 23:37:35 +01:00
|
|
|
if y >= height {
|
2018-05-22 17:30:25 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
item := items[i]
|
|
|
|
|
|
|
|
lineWidth := width
|
2018-05-22 17:38:54 +02:00
|
|
|
isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected
|
2019-03-25 23:37:35 +01:00
|
|
|
item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected)
|
|
|
|
y++
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
hasLess := trl.maxShown > 10
|
|
|
|
hasMore := trl.HasInvisibleRooms()
|
2019-03-25 23:37:35 +01:00
|
|
|
if (hasLess || hasMore) && y < height {
|
2018-05-22 17:30:25 +02:00
|
|
|
if hasMore {
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLine(screen, mauview.AlignRight, "More ↓", 0, y, width, tcell.StyleDefault)
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
if hasLess {
|
2019-03-25 23:37:35 +01:00
|
|
|
widget.WriteLine(screen, mauview.AlignLeft, "↑ Less", 0, y, width, tcell.StyleDefault)
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
2019-03-25 23:37:35 +01:00
|
|
|
y++
|
2018-05-22 17:30:25 +02:00
|
|
|
}
|
|
|
|
}
|