Improve tags and add initial invite handling

This commit is contained in:
Tulir Asokan 2020-02-22 01:17:52 +02:00
parent ecdb1166e1
commit 455d9fc4c5
8 changed files with 193 additions and 78 deletions

View File

@ -663,7 +663,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
return directChats return directChats
} }
func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) { func (c *Container) HandleDirectChatInfo(_ EventSource, evt *mautrix.Event) {
directChats := c.parseDirectChatInfo(evt) directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms.Map { for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room] 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. // 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") debug.Print("Received updated push rules")
var err error var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt) 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. // 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) room := c.GetOrCreateRoom(evt.RoomID)
newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags))
index := 0 index := 0
for tag, info := range evt.Content.RoomTags { for tag, info := range evt.Content.RoomTags {
order := "0.5" order := json.Number("0.5")
if len(info.Order) > 0 { if len(info.Order) > 0 {
order = info.Order.String() order = info.Order
} }
newTags[index] = rooms.RoomTag{ newTags[index] = rooms.RoomTag{
Tag: tag, 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. // 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 { if !c.config.AuthCache.InitialSyncDone {
return return
} }
@ -723,7 +724,7 @@ func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) {
func (c *Container) MarkRead(roomID, eventID string) { func (c *Container) MarkRead(roomID, eventID string) {
urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID) 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/#/@.+?:.+?\\)") var mentionRegex = regexp.MustCompile("\\[(.+?)]\\(https://matrix.to/#/@.+?:.+?\\)")
@ -791,10 +792,10 @@ func (c *Container) SendTyping(roomID string, typing bool) {
} }
if typing { if typing {
c.client.UserTyping(roomID, true, 20000) _, _ = c.client.UserTyping(roomID, true, 20000)
c.typing = ts + 15 c.typing = ts + 15
} else { } else {
c.client.UserTyping(roomID, false, 0) _, _ = c.client.UserTyping(roomID, false, 0)
c.typing = 0 c.typing = 0
} }
} }

View File

@ -22,6 +22,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/gomuks/lib/glob" "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 // Room is an interface with the functions that are needed for processing room-specific push conditions
type Room interface { type Room interface {
GetMember(mxid string) *mautrix.Member GetMember(mxid string) *rooms.Member
GetMembers() map[string]*mautrix.Member GetMembers() map[string]*rooms.Member
GetSessionOwner() string GetSessionOwner() string
} }

View File

@ -19,6 +19,7 @@ package rooms
import ( import (
"compress/gzip" "compress/gzip"
"encoding/gob" "encoding/gob"
"encoding/json"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -49,7 +50,7 @@ type RoomTag struct {
// The name of the tag. // The name of the tag.
Tag string Tag string
// The order of the tag. // The order of the tag.
Order string Order json.Number
} }
type UnreadMessage struct { type UnreadMessage struct {
@ -58,6 +59,13 @@ type UnreadMessage struct {
Highlight bool Highlight bool
} }
type Member struct {
mautrix.Member
// The user who sent the membership event
Sender string `json:"-"`
}
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {
// The room ID. // The room ID.
@ -73,7 +81,7 @@ type Room struct {
LastPrevBatch string LastPrevBatch string
// The MXID of the user whose session this room was created for. // The MXID of the user whose session this room was created for.
SessionUserID string SessionUserID string
SessionMember *mautrix.Member SessionMember *Member
// The number of unread messages that were notified about. // The number of unread messages that were notified about.
UnreadMessages []UnreadMessage UnreadMessages []UnreadMessage
@ -95,12 +103,12 @@ type Room struct {
// Room state cache. // Room state cache.
state map[mautrix.EventType]map[string]*mautrix.Event state map[mautrix.EventType]map[string]*mautrix.Event
// MXID -> Member cache calculated from membership events. // MXID -> Member cache calculated from membership events.
memberCache map[string]*mautrix.Member memberCache map[string]*Member
exMemberCache map[string]*mautrix.Member exMemberCache map[string]*Member
// The first two non-SessionUserID members in the room. Calculated at // The first two non-SessionUserID members in the room. Calculated at
// the same time as memberCache. // the same time as memberCache.
firstMemberCache *mautrix.Member firstMemberCache *Member
secondMemberCache *mautrix.Member secondMemberCache *Member
// The name of the room. Calculated from the state event name, // The name of the room. Calculated from the state event name,
// canonical_alias or alias or the member cache. // canonical_alias or alias or the member cache.
NameCache string NameCache string
@ -337,8 +345,13 @@ func (room *Room) Tags() []RoomTag {
room.lock.RLock() room.lock.RLock()
defer room.lock.RUnlock() defer room.lock.RUnlock()
if len(room.RawTags) == 0 { if len(room.RawTags) == 0 {
sessionMember := room.GetMember(room.SessionUserID)
if room.IsDirect { if room.IsDirect {
return []RoomTag{{"net.maunium.gomuks.fake.direct", "0.5"}} 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"}} return []RoomTag{{"", "0.5"}}
} }
@ -403,10 +416,11 @@ func (room *Room) UpdateState(event *mautrix.Event) {
func (room *Room) updateMemberState(event *mautrix.Event) { func (room *Room) updateMemberState(event *mautrix.Event) {
userID := event.GetStateKey() userID := event.GetStateKey()
if userID == room.SessionUserID { 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 { if room.memberCache != nil {
member := room.eventToMember(userID, &event.Content) member := room.eventToMember(userID, event.Sender, &event.Content)
if member.Membership.IsInviteOrJoin() { if member.Membership.IsInviteOrJoin() {
existingMember, ok := room.memberCache[userID] existingMember, ok := room.memberCache[userID]
if ok { if ok {
@ -553,16 +567,19 @@ func (room *Room) ReplacedBy() string {
return *room.replacedByCache return *room.replacedByCache
} }
func (room *Room) eventToMember(userID string, content *mautrix.Content) *mautrix.Member { func (room *Room) eventToMember(userID string, sender string, content *mautrix.Content) *Member {
member := &content.Member member := content.Member
member.Membership = content.Membership member.Membership = content.Membership
if len(member.Displayname) == 0 { if len(member.Displayname) == 0 {
member.Displayname = userID 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 userID != room.SessionUserID {
if room.firstMemberCache == nil { if room.firstMemberCache == nil {
room.firstMemberCache = member 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. // 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 { if len(room.memberCache) > 0 {
return room.memberCache return room.memberCache
} }
cache := make(map[string]*mautrix.Member) cache := make(map[string]*Member)
exCache := make(map[string]*mautrix.Member) exCache := make(map[string]*Member)
room.lock.RLock() room.lock.RLock()
events := room.getStateEvents(mautrix.StateMember) events := room.getStateEvents(mautrix.StateMember)
room.firstMemberCache = nil room.firstMemberCache = nil
room.secondMemberCache = nil room.secondMemberCache = nil
if events != nil { if events != nil {
for userID, event := range events { for userID, event := range events {
member := room.eventToMember(userID, &event.Content) member := room.eventToMember(userID, event.Sender, &event.Content)
if member.Membership.IsInviteOrJoin() { if member.Membership.IsInviteOrJoin() {
cache[userID] = member cache[userID] = member
room.updateNthMemberCache(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. // The members are returned from the cache.
// If the cache is empty, it is updated first. // 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.Load()
room.createMemberCache() room.createMemberCache()
return room.memberCache return room.memberCache
@ -623,7 +640,7 @@ func (room *Room) GetMembers() map[string]*mautrix.Member {
// GetMember returns the member with the given MXID. // GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned. // 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 { if userID == room.SessionUserID && room.SessionMember != nil {
return room.SessionMember return room.SessionMember
} }

View File

@ -90,6 +90,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
}, },
commands: map[string]CommandHandler{ commands: map[string]CommandHandler{
"unknown-command": cmdUnknownCommand, "unknown-command": cmdUnknownCommand,
"id": cmdID,
"help": cmdHelp, "help": cmdHelp,
"me": cmdMe, "me": cmdMe,
"quit": cmdQuit, "quit": cmdQuit,
@ -103,6 +105,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"unban": cmdUnban, "unban": cmdUnban,
"toggle": cmdToggle, "toggle": cmdToggle,
"logout": cmdLogout, "logout": cmdLogout,
"accept": cmdAccept,
"reject": cmdReject,
"sendevent": cmdSendEvent, "sendevent": cmdSendEvent,
"msendevent": cmdMSendEvent, "msendevent": cmdMSendEvent,
"setstate": cmdSetState, "setstate": cmdSetState,

View File

@ -106,8 +106,44 @@ func cmdNotice(cmd *Command) {
cmd.UI.Render() 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) { func cmdTags(cmd *Command) {
tags := cmd.Room.MxRoom().RawTags tags := cmd.Room.MxRoom().RawTags
if len(cmd.Args) > 0 && cmd.Args[0] == "--internal" {
tags = cmd.Room.MxRoom().Tags()
}
if len(tags) == 0 { if len(tags) == 0 {
if cmd.Room.MxRoom().IsDirect { if cmd.Room.MxRoom().IsDirect {
cmd.Reply("This room has no tags, but it's marked as a direct chat.") cmd.Reply("This room has no tags, but it's marked as a direct chat.")
@ -142,7 +178,18 @@ func cmdTag(cmd *Command) {
return 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 { if err != nil {
cmd.Reply("Failed to add tag:", err) cmd.Reply("Failed to add tag:", err)
} }

View File

@ -27,6 +27,7 @@ import (
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
) )
@ -39,7 +40,7 @@ func NewMemberList() *MemberList {
} }
type memberListItem struct { type memberListItem struct {
mautrix.Member rooms.Member
PowerLevel int PowerLevel int
Sigil rune Sigil rune
UserID string UserID string
@ -63,7 +64,7 @@ func (rml roomMemberList) Swap(i, j int) {
rml[i], rml[j] = rml[j], rml[i] 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)) ml.list = make(roomMemberList, len(data))
i := 0 i := 0
highestLevel := math.MinInt32 highestLevel := math.MinInt32

View File

@ -19,6 +19,7 @@ package ui
import ( import (
"math" "math"
"regexp" "regexp"
"sort"
"strings" "strings"
sync "github.com/sasha-s/go-deadlock" sync "github.com/sasha-s/go-deadlock"
@ -30,13 +31,43 @@ import (
"maunium.net/go/gomuks/matrix/rooms" "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 { type RoomList struct {
sync.RWMutex sync.RWMutex
parent *MainView parent *MainView
// The list of tags in display order. // The list of tags in display order.
tags []string tags TagNameList
// The list of rooms, in reverse order. // The list of rooms, in reverse order.
items map[string]*TagRoomList items map[string]*TagRoomList
// The selected room. // The selected room.
@ -107,13 +138,14 @@ func (list *RoomList) checkTag(tag string) {
//delete(list.items, tag) //delete(list.items, tag)
ok = false ok = false
} }
debug.Print("Checking", tag, index, trl.IsEmpty(), ok)
if ok && index == -1 { if ok && index == -1 {
list.tags = append(list.tags, tag) list.tags = append(list.tags, tag)
} /* TODO this doesn't work properly sort.Sort(list.tags)
else if index != -1 { } else if !ok && index != -1 {
list.tags = append(list.tags[0:index], list.tags[index+1:]...) list.tags = append(list.tags[0:index], list.tags[index+1:]...)
}*/ }
} }
func (list *RoomList) AddToTag(tag rooms.RoomTag, room *rooms.Room) { 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] trl, ok := list.items[tag.Tag]
if !ok { if !ok {
list.items[tag.Tag] = NewTagRoomList(list, tag.Tag, NewDefaultOrderedRoom(room)) 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) list.checkTag(tag.Tag)
} }
@ -412,11 +443,11 @@ func (list *RoomList) ContentHeight() (height int) {
return return
} }
func (list *RoomList) OnKeyEvent(event mauview.KeyEvent) bool { func (list *RoomList) OnKeyEvent(_ mauview.KeyEvent) bool {
return false return false
} }
func (list *RoomList) OnPasteEvent(event mauview.PasteEvent) bool { func (list *RoomList) OnPasteEvent(_ mauview.PasteEvent) bool {
return false return false
} }
@ -517,6 +548,10 @@ func (list *RoomList) GetTagDisplayName(tag string) string {
return "System Alerts" return "System Alerts"
case tag == "net.maunium.gomuks.fake.direct": case tag == "net.maunium.gomuks.fake.direct":
return "People" 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."): case strings.HasPrefix(tag, "u."):
return tag[len("u."):] return tag[len("u."):]
case !nsRegex.MatchString(tag): case !nsRegex.MatchString(tag):

View File

@ -17,10 +17,12 @@
package ui package ui
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"maunium.net/go/gomuks/debug"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -30,10 +32,10 @@ import (
type OrderedRoom struct { type OrderedRoom struct {
*rooms.Room *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{ return &OrderedRoom{
Room: room, Room: room,
order: order, 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. // 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. // The manual order and last received message timestamp are considered.
func (trl *TagRoomList) ShouldBeAfter(room1 *OrderedRoom, room2 *OrderedRoom) bool { 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)) 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) room := NewOrderedRoom(order, mxRoom)
trl.rooms = append(trl.rooms, nil)
// The default insert index is the newly added slot. // 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. // 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. // 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++ { 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 insertAt = i
break
} }
} }
trl.rooms = append(trl.rooms, nil)
// Move newer rooms forward in the array. // Move newer rooms forward in the array.
for i := len(trl.rooms) - 1; i > insertAt; i-- { for i := len(trl.rooms) - 1; i > insertAt; i-- {
trl.rooms[i] = trl.rooms[i-1] trl.rooms[i] = trl.rooms[i-1]
@ -207,7 +211,12 @@ func (trl *TagRoomList) RemoveIndex(index int) {
if index < 0 || index > len(trl.rooms) { if index < 0 || index > len(trl.rooms) {
return 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 { func (trl *TagRoomList) Index(room *rooms.Room) int {