diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | config/config.go | 137 | ||||
-rw-r--r-- | go.mod | 30 | ||||
-rw-r--r-- | go.sum | 68 | ||||
-rw-r--r-- | gomuks.go | 14 | ||||
-rw-r--r-- | interface/gomuks.go | 2 | ||||
-rw-r--r-- | interface/matrix.go | 1 | ||||
-rw-r--r-- | interface/ui.go | 9 | ||||
-rw-r--r-- | matrix/history.go | 25 | ||||
-rw-r--r-- | matrix/matrix.go | 119 | ||||
-rw-r--r-- | matrix/rooms/room.go | 316 | ||||
-rw-r--r-- | matrix/rooms/roomcache.go | 319 | ||||
-rw-r--r-- | matrix/sync.go | 6 | ||||
-rw-r--r-- | ui/command-processor.go | 2 | ||||
-rw-r--r-- | ui/commands.go | 61 | ||||
-rw-r--r-- | ui/message-view.go | 104 | ||||
-rw-r--r-- | ui/messages/base.go | 235 | ||||
-rw-r--r-- | ui/messages/expandedtextmessage.go | 67 | ||||
-rw-r--r-- | ui/messages/htmlmessage.go | 35 | ||||
-rw-r--r-- | ui/messages/imagemessage.go | 43 | ||||
-rw-r--r-- | ui/messages/message.go | 53 | ||||
-rw-r--r-- | ui/messages/parser.go | 55 | ||||
-rw-r--r-- | ui/messages/textbase.go | 11 | ||||
-rw-r--r-- | ui/messages/textmessage.go | 78 | ||||
-rw-r--r-- | ui/room-list.go | 25 | ||||
-rw-r--r-- | ui/room-view.go | 50 | ||||
-rw-r--r-- | ui/view-login.go | 2 | ||||
-rw-r--r-- | ui/view-main.go | 38 |
29 files changed, 1270 insertions, 639 deletions
@@ -7,3 +7,4 @@ gomuks coverage.out coverage.html deb/usr +*.prof @@ -8,7 +8,7 @@ ![Chat Preview](chat-preview.png) -A terminal Matrix client written in Go using [mautrix](https://github.com/matrix-org/mautrix) and [tview](https://github.com/rivo/tview). +A terminal Matrix client written in Go using [mautrix](https://github.com/tulir/mautrix-go) and [mauview](https://github.com/tulir/mauview). Basic usage is possible, but expect bugs and missing features. @@ -62,6 +62,7 @@ func Foo() { * `/quit` - Close gomuks * `/clearcache` - Clear room state and close gomuks * `/leave` - Leave the current room +* `/create <room name>` - Create a new Matrix room. * `/join <room>` - Join the room with the given room ID or alias * `/toggle <rooms/users/baremessages/images/typingnotif>` - Change user preferences * `/logout` - Log out, clear caches and go back to the login view diff --git a/config/config.go b/config/config.go index f77275f..d2d8ff7 100644 --- a/config/config.go +++ b/config/config.go @@ -53,15 +53,19 @@ type Config struct { AccessToken string `yaml:"access_token"` HS string `yaml:"homeserver"` - Dir string `yaml:"-"` - CacheDir string `yaml:"cache_dir"` - HistoryPath string `yaml:"history_path"` - MediaDir string `yaml:"media_dir"` - StateDir string `yaml:"state_dir"` + RoomCacheSize int `yaml:"room_cache_size"` + RoomCacheAge int64 `yaml:"room_cache_age"` + + Dir string `yaml:"-"` + CacheDir string `yaml:"cache_dir"` + HistoryPath string `yaml:"history_path"` + RoomListPath string `yaml:"room_list_path"` + MediaDir string `yaml:"media_dir"` + StateDir string `yaml:"state_dir"` Preferences UserPreferences `yaml:"-"` AuthCache AuthCache `yaml:"-"` - Rooms map[string]*rooms.Room `yaml:"-"` + Rooms *rooms.RoomCache `yaml:"-"` PushRules *pushrules.PushRuleset `yaml:"-"` nosave bool @@ -70,36 +74,39 @@ type Config struct { // NewConfig creates a config that loads data from the given directory. func NewConfig(configDir, cacheDir string) *Config { return &Config{ - Dir: configDir, - CacheDir: cacheDir, - HistoryPath: filepath.Join(cacheDir, "history.db"), - StateDir: filepath.Join(cacheDir, "state"), - MediaDir: filepath.Join(cacheDir, "media"), - - Rooms: make(map[string]*rooms.Room), + Dir: configDir, + CacheDir: cacheDir, + HistoryPath: filepath.Join(cacheDir, "history.db"), + RoomListPath: filepath.Join(cacheDir, "rooms.gob.gz"), + StateDir: filepath.Join(cacheDir, "state"), + MediaDir: filepath.Join(cacheDir, "media"), + + RoomCacheSize: 32, + RoomCacheAge: 1 * 60, } } // Clear clears the session cache and removes all history. func (config *Config) Clear() { - os.Remove(config.HistoryPath) - os.RemoveAll(config.StateDir) - os.RemoveAll(config.MediaDir) - os.RemoveAll(config.CacheDir) + _ = os.Remove(config.HistoryPath) + _ = os.Remove(config.RoomListPath) + _ = os.RemoveAll(config.StateDir) + _ = os.RemoveAll(config.MediaDir) + _ = os.RemoveAll(config.CacheDir) config.nosave = true } func (config *Config) CreateCacheDirs() { - os.MkdirAll(config.CacheDir, 0700) - os.MkdirAll(config.StateDir, 0700) - os.MkdirAll(config.MediaDir, 0700) + _ = os.MkdirAll(config.CacheDir, 0700) + _ = os.MkdirAll(config.StateDir, 0700) + _ = os.MkdirAll(config.MediaDir, 0700) } func (config *Config) DeleteSession() { config.AuthCache.NextBatch = "" config.AuthCache.InitialSyncDone = false config.AccessToken = "" - config.Rooms = make(map[string]*rooms.Room) + config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID) config.PushRules = nil config.Clear() @@ -109,10 +116,14 @@ func (config *Config) DeleteSession() { func (config *Config) LoadAll() { config.Load() + config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID) config.LoadAuthCache() config.LoadPushRules() config.LoadPreferences() - config.LoadRooms() + err := config.Rooms.LoadList() + if err != nil { + panic(err) + } } // Load loads the config from config.yaml in the directory given to the config struct. @@ -126,7 +137,11 @@ func (config *Config) SaveAll() { config.SaveAuthCache() config.SavePushRules() config.SavePreferences() - config.SaveRooms() + err := config.Rooms.SaveList() + if err != nil { + panic(err) + } + config.Rooms.SaveLoadedRooms() } // Save saves this config to config.yaml in the directory given to the config struct. @@ -161,48 +176,13 @@ func (config *Config) SavePushRules() { config.save("push rules", config.CacheDir, "pushrules.json", &config.PushRules) } -func (config *Config) LoadRooms() { - os.MkdirAll(config.StateDir, 0700) - - roomFiles, err := ioutil.ReadDir(config.StateDir) +func (config *Config) load(name, dir, file string, target interface{}) { + err := os.MkdirAll(dir, 0700) if err != nil { - debug.Print("Failed to list rooms state caches in", config.StateDir) + debug.Print("Failed to create", dir) panic(err) } - for _, roomFile := range roomFiles { - if roomFile.IsDir() || !strings.HasSuffix(roomFile.Name(), ".gmxstate") { - continue - } - path := filepath.Join(config.StateDir, roomFile.Name()) - room := &rooms.Room{} - err = room.Load(path) - if err != nil { - debug.Printf("Failed to load room state cache from %s: %v", path, err) - continue - } - config.Rooms[room.ID] = room - } -} - -func (config *Config) SaveRooms() { - if config.nosave { - return - } - - os.MkdirAll(config.StateDir, 0700) - for _, room := range config.Rooms { - path := config.getRoomCachePath(room) - err := room.Save(path) - if err != nil { - debug.Printf("Failed to save room state cache to file %s: %v", path, err) - } - } -} - -func (config *Config) load(name, dir, file string, target interface{}) { - os.MkdirAll(dir, 0700) - path := filepath.Join(dir, file) data, err := ioutil.ReadFile(path) if err != nil { @@ -229,9 +209,12 @@ func (config *Config) save(name, dir, file string, source interface{}) { return } - os.MkdirAll(dir, 0700) + err := os.MkdirAll(dir, 0700) + if err != nil { + debug.Print("Failed to create", dir) + panic(err) + } var data []byte - var err error if strings.HasSuffix(file, ".yaml") { data, err = yaml.Marshal(source) } else { @@ -272,30 +255,14 @@ func (config *Config) LoadNextBatch(_ string) string { return config.AuthCache.NextBatch } -func (config *Config) GetRoom(roomID string) *rooms.Room { - room, _ := config.Rooms[roomID] - if room == nil { - room = rooms.NewRoom(roomID, config.UserID) - config.Rooms[room.ID] = room - } - return room -} - -func (config *Config) getRoomCachePath(room *rooms.Room) string { - return filepath.Join(config.StateDir, room.ID+".gmxstate") -} - -func (config *Config) PutRoom(room *rooms.Room) { - config.Rooms[room.ID] = room - room.Save(config.getRoomCachePath(room)) -} - func (config *Config) SaveRoom(room *mautrix.Room) { - gmxRoom := config.GetRoom(room.ID) - gmxRoom.Room = room - gmxRoom.Save(config.getRoomCachePath(gmxRoom)) + panic("SaveRoom is not supported") } func (config *Config) LoadRoom(roomID string) *mautrix.Room { - return config.GetRoom(roomID).Room + panic("LoadRoom is not supported") +} + +func (config *Config) GetRoom(roomID string) *rooms.Room { + return config.Rooms.GetOrCreate(roomID) } @@ -1,33 +1,39 @@ module maunium.net/go/gomuks -go 1.12 +go 1.11 require ( github.com/alecthomas/chroma v0.6.3 - github.com/alecthomas/kong v0.1.16 // indirect github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect github.com/disintegration/imaging v1.6.0 + github.com/kr/pretty v0.1.0 // indirect github.com/kyokomi/emoji v2.1.0+incompatible github.com/lithammer/fuzzysearch v1.0.2 github.com/lucasb-eyer/go-colorful v1.0.2 - github.com/mattn/go-colorable v0.1.1 // indirect - github.com/mattn/go-isatty v0.0.7 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-runewidth v0.0.4 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/pkg/errors v0.8.1 github.com/sasha-s/go-deadlock v0.2.0 - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/stretchr/objx v0.2.0 // indirect + github.com/stretchr/testify v1.3.0 go.etcd.io/bbolt v1.3.2 - golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec - golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 + golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff + golang.org/x/net v0.0.0-20190603091049-60506f45cf65 + golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/russross/blackfriday.v2 v2.0.1 - gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect + gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/yaml.v2 v2.2.2 - maunium.net/go/mautrix v0.1.0-alpha.3.0.20190512142959-897a8c5be1d9 - maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125 - maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b + maunium.net/go/mautrix v0.1.0-alpha.3.0.20190616114735-e5bf3141e88e + maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d + maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed ) replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1 + +replace ( + maunium.net/go/mautrix => ../mautrix-go + maunium.net/go/mauview => ../../Go/mauview + maunium.net/go/tcell => ../../Go/tcell +) @@ -1,15 +1,19 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.6.3 h1:8H1D0yddf0mvgvO4JDBKnzLd9ERmzzAijBxnZXGV/FA= github.com/alecthomas/chroma v0.6.3/go.mod h1:quT2EpvJNqkuPi6DmBHB+E33FXBgBBPzyH5++Dn1LPc= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.1.15/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU= -github.com/alecthomas/kong v0.1.16/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E= github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= @@ -17,6 +21,11 @@ github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o= github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= github.com/lithammer/fuzzysearch v1.0.2 h1:AjCE2iwc5y+8K+h2nXVc0Pmrpjvu+JVqMgiZ0oakXDM= @@ -24,65 +33,68 @@ github.com/lithammer/fuzzysearch v1.0.2/go.mod h1:bvAJyokfCQ7Vknrd4Kgc+izmMrPj5C github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/npat-efault/poller v2.0.0+incompatible h1:jtTdXWKgN5kDK41ts8hoY1rvTEi0K08MTB8/bRO9MqE= +github.com/npat-efault/poller v2.0.0+incompatible/go.mod h1:lni01B89P8PtVpwlAhdhK1niN5rPkDGGpGGgBJzpSgo= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs= -github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= +github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44 h1:XKCbzPvK4/BbMXoMJOkYP2ANxiAEO0HM1xn6psSbXxY= +github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y= github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8aswvM7nTtAMVm8ARPWzkE9n6eZDOccY= github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY= +github.com/zyedidia/poller v2.0.1-0.20170616160828-ab09682913b7+incompatible h1:8VIuqV713C9SmwvUGGpMhrK/5RdsRyp9N4YnPDPDe6c= +github.com/zyedidia/poller v2.0.1-0.20170616160828-ab09682913b7+incompatible/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec h1:arXJwtMuk5vqI1NHX0UTnNw977rYk5Sl4jQqHj+hun4= -golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190425145619-16072639606e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4 h1:3i7qG/aA9NUAzdnJHfhgxSKSmxbAebomYR5IZgFbC5Y= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190511041617-99f201b6807e/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo= gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -maunium.net/go/mautrix v0.1.0-alpha.3.0.20190512142959-897a8c5be1d9 h1:bmXWacD7spGlj2DriogI5ahet6dzY2Q33Aa8bCyQdtI= -maunium.net/go/mautrix v0.1.0-alpha.3.0.20190512142959-897a8c5be1d9/go.mod h1:cyZKXVQphK5gnbKMvRLXoxfKMySfAQJvn8ttR4x/23c= -maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125 h1:wSrf+ZYCavbnU21f3Q1fKytL2mJyCyi2+Dosbck780Q= -maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125/go.mod h1:TIbj5iET7pJSq4SpVxZ080mAe6dgoQxGN5oRHwnSXnI= -maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b h1:xncLOTadq4VzDKFUV7Jm3OSkehS2KAL88pfwowcAmRA= -maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b/go.mod h1:V4YmSYrOlCtlTM188iXR8VwWSo+ksAVawxQLXibeAyQ= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc h1:G7Nse6r/XaCu+p7yc/3m/nfFuOFZZ87Hb3AOX4INOEk= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac h1:r0X7mMPcc8eJaCaHdbW9ibfCLe3EruuqZIH2FM8oLIs= +maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg= +maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d h1:H4wZ4vMVnOh5QFsb4xZtssgpv3DDEkBRzQ8iyEg2fX0= +maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d/go.mod h1:GL+akv58wNFzzX4IKLvryKx0F/AcYKHql35DiBzBc/w= +maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed h1:sAcUrUZG2LFWBTkTtLKPQvHPHFM5d6huAhr5ZZuxtbQ= +maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed/go.mod h1:8UOoBx9iuQZewMnaDHz9KQZtwFvl0TOA1f6hQhycgBw= @@ -58,8 +58,6 @@ func NewGomuks(uiProvider ifc.UIProvider, configDir, cacheDir string) *Gomuks { // Save saves the active session and message history. func (gmx *Gomuks) Save() { gmx.config.SaveAll() - //debug.Print("Saving history...") - //gmx.ui.MainView().SaveAllHistory() } // StartAutosave calls Save() every minute until it receives a stop signal @@ -70,7 +68,9 @@ func (gmx *Gomuks) StartAutosave() { for { select { case <-ticker.C: - gmx.Save() + if gmx.config.AuthCache.InitialSyncDone { + gmx.Save() + } case val := <-gmx.stop: if val { return @@ -81,13 +81,15 @@ func (gmx *Gomuks) StartAutosave() { // Stop stops the Matrix syncer, the tview app and the autosave goroutine, // then saves everything and calls os.Exit(0). -func (gmx *Gomuks) Stop() { +func (gmx *Gomuks) Stop(save bool) { debug.Print("Disconnecting from Matrix...") gmx.matrix.Stop() debug.Print("Cleaning up UI...") gmx.ui.Stop() gmx.stop <- true - gmx.Save() + if save { + gmx.Save() + } os.Exit(0) } @@ -102,7 +104,7 @@ func (gmx *Gomuks) Start() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - gmx.Stop() + gmx.Stop(true) }() go gmx.StartAutosave() diff --git a/interface/gomuks.go b/interface/gomuks.go index f2565fb..5937a6b 100644 --- a/interface/gomuks.go +++ b/interface/gomuks.go @@ -27,5 +27,5 @@ type Gomuks interface { Config() *config.Config Start() - Stop() + Stop(save bool) } diff --git a/interface/matrix.go b/interface/matrix.go index f312df1..6a1a977 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -45,6 +45,7 @@ type MatrixContainer interface { GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, error) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, error) GetRoom(roomID string) *rooms.Room + GetOrCreateRoom(roomID string) *rooms.Room Download(mxcURL string) ([]byte, string, string, error) GetDownloadURL(homeserver, fileID string) string diff --git a/interface/ui.go b/interface/ui.go index 781f803..ad2458a 100644 --- a/interface/ui.go +++ b/interface/ui.go @@ -43,14 +43,14 @@ type MainView interface { GetRoom(roomID string) RoomView AddRoom(room *rooms.Room) RemoveRoom(room *rooms.Room) - SetRooms(rooms map[string]*rooms.Room) + SetRooms(rooms *rooms.RoomCache) + Bump(room *rooms.Room) UpdateTags(room *rooms.Room) SetTyping(roomID string, users []string) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) - InitialSyncDone() } type RoomView interface { @@ -68,13 +68,10 @@ type RoomView interface { type Message interface { ID() string - TxnID() string - SenderID() string - Timestamp() time.Time + Time() time.Time NotificationSenderName() string NotificationContent() string - SetState(state mautrix.OutgoingEventState) SetIsHighlight(highlight bool) SetID(id string) } diff --git a/matrix/history.go b/matrix/history.go index 767cace..7275c15 100644 --- a/matrix/history.go +++ b/matrix/history.go @@ -18,6 +18,7 @@ package matrix import ( "bytes" + "compress/gzip" "encoding/binary" "encoding/gob" @@ -28,6 +29,10 @@ import ( "maunium.net/go/mautrix" ) +func init() { + gob.Register(&mautrix.Event{}) +} + type HistoryManager struct { sync.Mutex @@ -226,13 +231,27 @@ func btoi(b []byte) uint64 { func marshalEvent(event *mautrix.Event) ([]byte, error) { var buf bytes.Buffer - err := gob.NewEncoder(&buf).Encode(event) - return buf.Bytes(), err + enc := gzip.NewWriter(&buf) + if err := gob.NewEncoder(enc).Encode(event); err != nil { + _ = enc.Close() + return nil, err + } else if err := enc.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil } func unmarshalEvent(data []byte) (*mautrix.Event, error) { event := &mautrix.Event{} - return event, gob.NewDecoder(bytes.NewReader(data)).Decode(event) + if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil { + return nil, err + } else if err := gob.NewDecoder(cmpReader).Decode(event); err != nil { + _ = cmpReader.Close() + return nil, err + } else if err := cmpReader.Close(); err != nil { + return nil, err + } + return event, nil } func put(streams, eventIDs *bolt.Bucket, event *mautrix.Event, key uint64) error { diff --git a/matrix/matrix.go b/matrix/matrix.go index ef272b0..28eef44 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -29,7 +29,9 @@ import ( "path" "path/filepath" "regexp" + "runtime" "time" + dbg "runtime/debug" "maunium.net/go/mautrix" "maunium.net/go/mautrix/format" @@ -204,6 +206,9 @@ func (c *Container) OnLogin() { debug.Print("Initializing syncer") c.syncer = NewGomuksSyncer(c.config) c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) + // Just pass encrypted events as messages, they'll show up with an encryption unsupported message. + c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage) + c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage) c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage) c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage) @@ -218,9 +223,22 @@ func (c *Container) OnLogin() { c.syncer.InitDoneCallback = func() { debug.Print("Initial sync done") c.config.AuthCache.InitialSyncDone = true - c.config.SaveAuthCache() - c.ui.MainView().InitialSyncDone() + debug.Print("Updating title caches") + for _, room := range c.config.Rooms.Map { + room.GetTitle() + } + debug.Print("Cleaning cached rooms from memory") + c.config.Rooms.ForceClean() + debug.Print("Saving all data") + c.config.SaveAll() + debug.Print("Adding rooms to UI") + c.ui.MainView().SetRooms(c.config.Rooms) c.ui.Render() + // The initial sync can be a bit heavy, so we force run the GC here + // after cleaning up rooms from memory above. + debug.Print("Running GC") + runtime.GC() + dbg.FreeOSMemory() } c.client.Syncer = c.syncer @@ -274,7 +292,9 @@ func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) { return } debug.Print("Updated preferences:", orig, "->", c.config.Preferences) - c.ui.HandleNewPreferences() + if c.config.AuthCache.InitialSyncDone { + c.ui.HandleNewPreferences() + } } func (c *Container) SendPreferencesToMatrix() { @@ -289,9 +309,24 @@ func (c *Container) SendPreferencesToMatrix() { // HandleMessage is the event handler for the m.room.message timeline event. func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { - if source&EventSourceLeave != 0 || source&EventSourceState != 0 { + room := c.GetOrCreateRoom(evt.RoomID) + if source&EventSourceLeave != 0 { + room.HasLeft = true + return + } else if source&EventSourceState != 0 { + return + } + + err := c.history.Append(room, []*mautrix.Event{evt}) + if err != nil { + debug.Printf("Failed to add event %s to history: %v", evt.ID, err) + } + + if !c.config.AuthCache.InitialSyncDone { + room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000) return } + mainView := c.ui.MainView() roomView := mainView.GetRoom(evt.RoomID) @@ -300,23 +335,29 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { return } - err := c.history.Append(roomView.MxRoom(), []*mautrix.Event{evt}) - if err != nil { - debug.Printf("Failed to add event %s to history: %v", evt.ID, err) + if !room.Loaded() { + pushRules := c.PushRules().GetActions(room, evt).Should() + shouldNotify := pushRules.Notify || !pushRules.NotifySpecified + if !shouldNotify { + room.LastReceivedMessage = time.Unix(evt.Timestamp/1000, evt.Timestamp%1000*1000) + room.AddUnread(evt.ID, shouldNotify, pushRules.Highlight) + mainView.Bump(room) + return + } } // TODO switch to roomView.AddEvent message := roomView.ParseEvent(evt) if message != nil { roomView.AddMessage(message) - roomView.MxRoom().LastReceivedMessage = message.Timestamp() + roomView.MxRoom().LastReceivedMessage = message.Time() if c.syncer.FirstSyncDone { pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should() mainView.NotifyMessage(roomView.MxRoom(), message, pushRules) c.ui.Render() } } else { - debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type, evt.Content.Raw, evt.Sender, evt.RoomID) + debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type.String(), evt.Content.Raw, evt.Sender, evt.RoomID) } } @@ -324,6 +365,9 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) { func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) { isLeave := source&EventSourceLeave != 0 isTimeline := source&EventSourceTimeline != 0 + if isLeave { + c.GetOrCreateRoom(evt.RoomID).HasLeft = true + } isNonTimelineLeave := isLeave && !isTimeline if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { return @@ -350,11 +394,16 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) { room := c.GetRoom(evt.RoomID) switch membership { case "join": - c.ui.MainView().AddRoom(room) + if c.config.AuthCache.InitialSyncDone { + c.ui.MainView().AddRoom(room) + } room.HasLeft = false case "leave": - c.ui.MainView().RemoveRoom(room) + if c.config.AuthCache.InitialSyncDone { + c.ui.MainView().RemoveRoom(room) + } room.HasLeft = true + room.Unload() case "invite": // TODO handle debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID) @@ -399,8 +448,12 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) { } room := c.GetRoom(evt.RoomID) - room.MarkRead(lastReadEvent) - c.ui.Render() + if room != nil { + room.MarkRead(lastReadEvent) + if c.config.AuthCache.InitialSyncDone { + c.ui.Render() + } + } } func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool { @@ -417,7 +470,7 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool continue } - room := c.GetRoom(roomID) + room := c.GetOrCreateRoom(roomID) if room != nil && !room.HasLeft { directChats[room] = true } @@ -428,11 +481,13 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) { directChats := c.parseDirectChatInfo(evt) - for _, room := range c.config.Rooms { + for _, room := range c.config.Rooms.Map { shouldBeDirect := directChats[room] if shouldBeDirect != room.IsDirect { room.IsDirect = shouldBeDirect - c.ui.MainView().UpdateTags(room) + if c.config.AuthCache.InitialSyncDone { + c.ui.MainView().UpdateTags(room) + } } } } @@ -451,7 +506,7 @@ 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) { - room := c.config.GetRoom(evt.RoomID) + room := c.GetOrCreateRoom(evt.RoomID) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) index := 0 @@ -466,14 +521,19 @@ func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { } index++ } - - mainView := c.ui.MainView() room.RawTags = newTags - mainView.UpdateTags(room) + + if c.config.AuthCache.InitialSyncDone { + mainView := c.ui.MainView() + mainView.UpdateTags(room) + } } // HandleTyping is the event handler for the m.typing event. func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) { + if !c.config.AuthCache.InitialSyncDone { + return + } c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs) } @@ -544,7 +604,7 @@ func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error) if err != nil { return nil, err } - room := c.GetRoom(resp.RoomID) + room := c.GetOrCreateRoom(resp.RoomID) return room, nil } @@ -557,7 +617,6 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) { room := c.GetRoom(resp.RoomID) room.HasLeft = false - return room, nil } @@ -568,8 +627,9 @@ func (c *Container) LeaveRoom(roomID string) error { return err } - room := c.GetRoom(roomID) - room.HasLeft = true + node := c.GetOrCreateRoom(roomID) + node.HasLeft = true + node.Unload() return nil } @@ -593,9 +653,9 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, e return nil, err } } - room.PrevBatch = resp.End - c.config.PutRoom(room) debug.Printf("Loaded %d events for %s from server from %s to %s", len(resp.Chunk), room.ID, resp.Start, resp.End) + room.PrevBatch = resp.End + c.config.Rooms.Put(room) return resp.Chunk, nil } @@ -613,9 +673,14 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, return event, nil } +// GetOrCreateRoom gets the room instance stored in the session. +func (c *Container) GetOrCreateRoom(roomID string) *rooms.Room { + return c.config.Rooms.GetOrCreate(roomID) +} + // GetRoom gets the room instance stored in the session. func (c *Container) GetRoom(roomID string) *rooms.Room { - return c.config.GetRoom(roomID) + return c.config.Rooms.Get(roomID) } var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index 717636b..4928871 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -17,6 +17,7 @@ package rooms import ( + "compress/gzip" "encoding/gob" "fmt" "os" @@ -31,17 +32,18 @@ import ( ) func init() { - gob.Register([]interface{}{}) gob.Register(map[string]interface{}{}) + gob.Register([]interface{}{}) } type RoomNameSource int const ( - ExplicitRoomName RoomNameSource = iota - CanonicalAliasRoomName - AliasRoomName + UnknownRoomName RoomNameSource = iota MemberRoomName + AliasRoomName + CanonicalAliasRoomName + ExplicitRoomName ) // RoomTag is a tag given to a specific room. @@ -60,7 +62,8 @@ type UnreadMessage struct { // Room represents a single Matrix room. type Room struct { - *mautrix.Room + // The room ID. + ID string // Whether or not the user has left the room. HasLeft bool @@ -70,6 +73,7 @@ type Room struct { PrevBatch string // The MXID of the user whose session this room was created for. SessionUserID string + SessionMember *mautrix.Member // The number of unread messages that were notified about. UnreadMessages []UnreadMessage @@ -79,19 +83,22 @@ type Room struct { // Whether or not this room is marked as a direct chat. IsDirect bool - // List of tags given to this room + // List of tags given to this room. RawTags []RoomTag // Timestamp of previously received actual message. LastReceivedMessage time.Time + // Room state cache. + state map[mautrix.EventType]map[string]*mautrix.Event // MXID -> Member cache calculated from membership events. memberCache map[string]*mautrix.Member - // The first non-SessionUserID member in the room. Calculated at + // The first two non-SessionUserID members in the room. Calculated at // the same time as memberCache. - firstMemberCache *mautrix.Member + firstMemberCache *mautrix.Member + secondMemberCache *mautrix.Member // The name of the room. Calculated from the state event name, // canonical_alias or alias or the member cache. - nameCache string + NameCache string // The event type from which the name cache was calculated from. nameCacheSource RoomNameSource // The topic of the room. Directly fetched from the m.room.topic state event. @@ -101,31 +108,143 @@ type Room struct { // The list of aliases. Directly fetched from the m.room.aliases state event. aliasesCache []string + // Path for state store file. + path string + // Room cache object + cache *RoomCache + // Lock for state and other room stuff. lock sync.RWMutex + // Pre/post un/load hooks + preUnload func() bool + preLoad func() bool + postUnload func() + postLoad func() + // Whether or not the room state has changed + changed bool + + // Room state cache linked list. + prev *Room + next *Room + touch int64 } -func (room *Room) Load(path string) error { - file, err := os.OpenFile(path, os.O_RDONLY, 0600) - if err != nil { - return err +func debugPrintError(fn func() error, message string) { + if err := fn(); err != nil { + debug.Printf("%s: %v", message, err) + } +} + +func (room *Room) Loaded() bool { + return room.state != nil +} + +func (room *Room) Load() { + room.cache.TouchNode(room) + if room.Loaded() { + return + } + if room.preLoad != nil && !room.preLoad() { + return } - defer file.Close() - dec := gob.NewDecoder(file) room.lock.Lock() - defer room.lock.Unlock() - return dec.Decode(room) + room.load() + room.lock.Unlock() + if room.postLoad != nil { + room.postLoad() + } +} + +func (room *Room) load() { + if room.Loaded() { + return + } + debug.Print("Loading state for room", room.ID, "from disk") + room.state = make(map[mautrix.EventType]map[string]*mautrix.Event) + file, err := os.OpenFile(room.path, os.O_RDONLY, 0600) + if err != nil { + if !os.IsNotExist(err) { + debug.Print("Failed to open room state file for reading:", err) + } else { + debug.Print("Room state file for", room.ID, "does not exist") + } + return + } + defer debugPrintError(file.Close, "Failed to close room state file after reading") + cmpReader, err := gzip.NewReader(file) + if err != nil { + debug.Print("Failed to open room state gzip reader:", err) + return + } + defer debugPrintError(cmpReader.Close, "Failed to close room state gzip reader") + dec := gob.NewDecoder(cmpReader) + if err = dec.Decode(&room.state); err != nil { + debug.Print("Failed to decode room state:", err) + } + room.changed = false +} + +func (room *Room) Touch() { + room.cache.TouchNode(room) +} + +func (room *Room) Unload() bool { + if room.preUnload != nil && !room.preUnload() { + return false + } + debug.Print("Unloading", room.ID) + room.Save() + room.state = nil + room.aliasesCache = nil + room.topicCache = "" + room.canonicalAliasCache = "" + room.firstMemberCache = nil + room.secondMemberCache = nil + if room.postUnload != nil { + room.postUnload() + } + return true } -func (room *Room) Save(path string) error { - file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) +func (room *Room) SetPreUnload(fn func() bool) { + room.preUnload = fn +} + +func (room *Room) SetPreLoad(fn func() bool) { + room.preLoad = fn +} + +func (room *Room) SetPostUnload(fn func()) { + room.postUnload = fn +} + +func (room *Room) SetPostLoad(fn func()) { + room.postLoad = fn +} + +func (room *Room) Save() { + if !room.Loaded() { + debug.Print("Failed to save room", room.ID, "state: room not loaded") + return + } + if !room.changed { + debug.Print("Not saving", room.ID, "as state hasn't changed") + return + } + debug.Print("Saving state for room", room.ID, "to disk") + file, err := os.OpenFile(room.path, os.O_WRONLY|os.O_CREATE, 0600) if err != nil { - return err + debug.Print("Failed to open room state file for writing:", err) + return } - defer file.Close() - enc := gob.NewEncoder(file) + defer debugPrintError(file.Close, "Failed to close room state file after writing") + cmpWriter := gzip.NewWriter(file) + defer debugPrintError(cmpWriter.Close, "Failed to close room state gzip writer") + enc := gob.NewEncoder(cmpWriter) room.lock.RLock() defer room.lock.RUnlock() - return enc.Encode(room) + if err := enc.Encode(&room.state); err != nil { + debug.Print("Failed to encode room state:", err) + } } // MarkRead clears the new message statuses on this room. @@ -220,62 +339,79 @@ func (room *Room) Tags() []RoomTag { // 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 *mautrix.Event) { + room.Load() room.lock.Lock() defer room.lock.Unlock() - _, exists := room.State[event.Type] + room.changed = true + _, exists := room.state[event.Type] if !exists { - room.State[event.Type] = make(map[string]*mautrix.Event) + room.state[event.Type] = make(map[string]*mautrix.Event) } switch event.Type { case mautrix.StateRoomName: - room.nameCache = "" + room.NameCache = event.Content.Name + room.nameCacheSource = ExplicitRoomName case mautrix.StateCanonicalAlias: - if room.nameCacheSource >= CanonicalAliasRoomName { - room.nameCache = "" + if room.nameCacheSource <= CanonicalAliasRoomName { + room.NameCache = event.Content.Alias + room.nameCacheSource = CanonicalAliasRoomName } - room.canonicalAliasCache = "" + room.canonicalAliasCache = event.Content.Alias case mautrix.StateAliases: - if room.nameCacheSource >= AliasRoomName { - room.nameCache = "" + if room.nameCacheSource <= AliasRoomName { + room.NameCache = "" } room.aliasesCache = nil case mautrix.StateMember: - room.memberCache = nil - room.firstMemberCache = nil - if room.nameCacheSource >= MemberRoomName { - room.nameCache = "" + userID := event.GetStateKey() + if userID == room.SessionUserID { + room.SessionMember = room.eventToMember(userID, &event.Content) + } + if room.memberCache != nil { + if event.Content.Membership == mautrix.MembershipLeave || event.Content.Membership == mautrix.MembershipBan { + delete(room.memberCache, userID) + } else if event.Content.Membership == mautrix.MembershipInvite || event.Content.Membership == mautrix.MembershipJoin { + member := room.eventToMember(userID, &event.Content) + existingMember, ok := room.memberCache[userID] + if ok { + *existingMember = *member + } else { + room.memberCache[userID] = member + room.updateNthMemberCache(userID, member) + } + } + } + if room.nameCacheSource <= MemberRoomName { + room.NameCache = "" } case mautrix.StateTopic: - room.topicCache = "" + room.topicCache = event.Content.Topic } - stateKey := "" - if event.StateKey != nil { - stateKey = *event.StateKey - } if event.Type != mautrix.StateMember { - debug.Printf("Updating state %s#%s for %s", event.Type, stateKey, room.ID) + debug.Printf("Updating state %s#%s for %s", event.Type.String(), event.GetStateKey(), room.ID) } if event.StateKey == nil { - room.State[event.Type][""] = event + room.state[event.Type][""] = event } else { - room.State[event.Type][*event.StateKey] = event + room.state[event.Type][*event.StateKey] = event } } // GetStateEvent returns the state event for the given type/state_key combo, or nil. func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event { + room.Load() room.lock.RLock() defer room.lock.RUnlock() - stateEventMap, _ := room.State[eventType] + stateEventMap, _ := room.state[eventType] event, _ := stateEventMap[stateKey] return event } // getStateEvents returns the state events for the given type. func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event { - stateEventMap, _ := room.State[eventType] + stateEventMap, _ := room.state[eventType] return stateEventMap } @@ -323,7 +459,7 @@ func (room *Room) GetAliases() []string { func (room *Room) updateNameFromNameEvent() { nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") if nameEvt != nil { - room.nameCache = nameEvt.Content.Name + room.NameCache = nameEvt.Content.Name } } @@ -336,7 +472,7 @@ func (room *Room) updateNameFromAliases() { aliases := room.GetAliases() if len(aliases) > 0 { sort.Sort(sort.StringSlice(aliases)) - room.nameCache = aliases[0] + room.NameCache = aliases[0] } } @@ -351,33 +487,40 @@ func (room *Room) updateNameFromAliases() { func (room *Room) updateNameFromMembers() { members := room.GetMembers() if len(members) <= 1 { - room.nameCache = "Empty room" + room.NameCache = "Empty room" } else if room.firstMemberCache == nil { - room.nameCache = "Room" + room.NameCache = "Room" } else if len(members) == 2 { - room.nameCache = room.firstMemberCache.Displayname + room.NameCache = room.firstMemberCache.Displayname + } else if len(members) == 3 && room.secondMemberCache != nil { + room.NameCache = fmt.Sprintf("%s and %s", room.firstMemberCache.Displayname, room.secondMemberCache.Displayname) } else { - firstMember := room.firstMemberCache.Displayname - room.nameCache = fmt.Sprintf("%s and %d others", firstMember, len(members)-2) + members := room.firstMemberCache.Displayname + count := len(members) - 2 + if room.secondMemberCache != nil { + members += ", " + room.secondMemberCache.Displayname + count-- + } + room.NameCache = fmt.Sprintf("%s and %d others", members, count) } } // updateNameCache updates the room display name based on the room state in the order // specified in spec section 11.2.2.5. func (room *Room) updateNameCache() { - if len(room.nameCache) == 0 { + if len(room.NameCache) == 0 { room.updateNameFromNameEvent() room.nameCacheSource = ExplicitRoomName } - if len(room.nameCache) == 0 { - room.nameCache = room.GetCanonicalAlias() + if len(room.NameCache) == 0 { + room.NameCache = room.GetCanonicalAlias() room.nameCacheSource = CanonicalAliasRoomName } - if len(room.nameCache) == 0 { + if len(room.NameCache) == 0 { room.updateNameFromAliases() room.nameCacheSource = AliasRoomName } - if len(room.nameCache) == 0 { + if len(room.NameCache) == 0 { room.updateNameFromMembers() room.nameCacheSource = MemberRoomName } @@ -389,27 +532,47 @@ func (room *Room) updateNameCache() { // If the cache is empty, it is updated first. func (room *Room) GetTitle() string { room.updateNameCache() - return room.nameCache + return room.NameCache +} + +func (room *Room) eventToMember(userID string, content *mautrix.Content) *mautrix.Member { + member := &content.Member + member.Membership = content.Membership + if len(member.Displayname) == 0 { + member.Displayname = userID + } + return member +} + +func (room *Room) updateNthMemberCache(userID string, member *mautrix.Member) { + if userID != room.SessionUserID { + if room.firstMemberCache == nil { + room.firstMemberCache = member + } else if room.secondMemberCache == nil { + room.secondMemberCache = member + } + } } // createMemberCache caches all member events into a easily processable MXID -> *Member map. func (room *Room) createMemberCache() map[string]*mautrix.Member { + if len(room.memberCache) > 0 { + return room.memberCache + } cache := make(map[string]*mautrix.Member) room.lock.RLock() events := room.getStateEvents(mautrix.StateMember) room.firstMemberCache = nil + room.secondMemberCache = nil if events != nil { for userID, event := range events { - member := &event.Content.Member - member.Membership = event.Content.Membership - if len(member.Displayname) == 0 { - member.Displayname = userID - } - if room.firstMemberCache == nil && userID != room.SessionUserID { - room.firstMemberCache = member - } + member := room.eventToMember(userID, &event.Content) if member.Membership == mautrix.MembershipJoin || member.Membership == mautrix.MembershipInvite { cache[userID] = member + room.updateNthMemberCache(userID, member) + } + if userID == room.SessionUserID { + room.SessionMember = member } } } @@ -425,18 +588,19 @@ 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 { - if len(room.memberCache) == 0 || room.firstMemberCache == nil { - room.createMemberCache() - } + room.Load() + room.createMemberCache() return room.memberCache } // 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 { - if len(room.memberCache) == 0 { - room.createMemberCache() + if userID == room.SessionUserID && room.SessionMember != nil { + return room.SessionMember } + room.Load() + room.createMemberCache() room.lock.RLock() member, _ := room.memberCache[userID] room.lock.RUnlock() @@ -449,9 +613,13 @@ func (room *Room) GetSessionOwner() string { } // NewRoom creates a new Room with the given ID -func NewRoom(roomID, owner string) *Room { +func NewRoom(roomID string, cache *RoomCache) *Room { return &Room{ - Room: mautrix.NewRoom(roomID), - SessionUserID: owner, + ID: roomID, + state: make(map[mautrix.EventType]map[string]*mautrix.Event), + path: cache.roomPath(roomID), + cache: cache, + + SessionUserID: cache.getOwner(), } } diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go new file mode 100644 index 0000000..6fc400c --- /dev/null +++ b/matrix/rooms/roomcache.go @@ -0,0 +1,319 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// 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 +// GNU Affero General Public License for more details. +// +// 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/>. + +package rooms + +import ( + "compress/gzip" + "encoding/gob" + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" + sync "github.com/sasha-s/go-deadlock" + + "maunium.net/go/gomuks/debug" +) + +// RoomCache contains room state info in a hashmap and linked list. +type RoomCache struct { + sync.Mutex + + listPath string + directory string + maxSize int + maxAge int64 + getOwner func() string + + Map map[string]*Room + head *Room + tail *Room + size int +} + +func NewRoomCache(listPath, directory string, maxSize int, maxAge int64, getOwner func() string) *RoomCache { + return &RoomCache{ + listPath: listPath, + directory: directory, + maxSize: maxSize, + maxAge: maxAge, + getOwner: getOwner, + + Map: make(map[string]*Room), + } +} + +func (cache *RoomCache) LoadList() error { + cache.Lock() + defer cache.Unlock() + + // Open room list file + file, err := os.OpenFile(cache.listPath, os.O_RDONLY, 0600) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return errors.Wrap(err, "failed to open room list file for reading") + } + defer debugPrintError(file.Close, "Failed to close room list file after reading") + + // Open gzip reader for room list file + cmpReader, err := gzip.NewReader(file) + if err != nil { + return errors.Wrap(err, "failed to read gzip room list") + } + defer debugPrintError(cmpReader.Close, "Failed to close room list gzip reader") + + // Open gob decoder for gzip reader + dec := gob.NewDecoder(cmpReader) + // Read number of items in list + var size int + err = dec.Decode(&size) + if err != nil { + return errors.Wrap(err, "failed to read size of room list") + } + + // Read list + cache.Map = make(map[string]*Room, size) + for i := 0; i < size; i++ { + room := &Room{} + err = dec.Decode(room) + if err != nil { + debug.Printf("Failed to decode %dth room list entry: %v", i+1, err) + continue + } + room.path = cache.roomPath(room.ID) + room.cache = cache + cache.Map[room.ID] = room + } + return nil +} + +func (cache *RoomCache) SaveLoadedRooms() { + cache.Lock() + defer cache.Unlock() + cache.clean(false) + for node := cache.head; node != nil; node = node.prev { + node.Save() + } +} + +func (cache *RoomCache) SaveList() error { + cache.Lock() + defer cache.Unlock() + + debug.Print("Saving room list...") + // Open room list file + file, err := os.OpenFile(cache.listPath, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return errors.Wrap(err, "failed to open room list file for writing") + } + defer debugPrintError(file.Close, "Failed to close room list file after writing") + + // Open gzip writer for room list file + cmpWriter := gzip.NewWriter(file) + defer debugPrintError(cmpWriter.Close, "Failed to close room list gzip writer") + + // Open gob encoder for gzip writer + enc := gob.NewEncoder(cmpWriter) + // Write number of items in list + err = enc.Encode(len(cache.Map)) + if err != nil { + return errors.Wrap(err, "failed to write size of room list") + } + + // Write list + for _, node := range cache.Map { + err = enc.Encode(node) + if err != nil { + debug.Printf("Failed to encode room list entry of %s: %v", node.ID, err) + } + } + debug.Print("Room list saved to", cache.listPath, len(cache.Map), cache.size) + return nil +} + +func (cache *RoomCache) Touch(roomID string) { + cache.Lock() + node, ok := cache.Map[roomID] + if !ok || node == nil { + cache.Unlock() + return + } + cache.touch(node) + cache.Unlock() +} + +func (cache *RoomCache) TouchNode(node *Room) { + cache.Lock() + cache.touch(node) + cache.Unlock() +} + +func (cache *RoomCache) touch(node *Room) { + if node == cache.head { + return + } + debug.Print("Touching", node.ID) + cache.llPop(node) + cache.llPush(node) + node.touch = time.Now().Unix() +} + +func (cache *RoomCache) Get(roomID string) *Room { + cache.Lock() + node := cache.get(roomID) + cache.Unlock() + return node +} + +func (cache *RoomCache) GetOrCreate(roomID string) *Room { + cache.Lock() + node := cache.get(roomID) + if node == nil { + node = cache.newRoom(roomID) + cache.llPush(node) + } + cache.Unlock() + return node +} + +func (cache *RoomCache) get(roomID string) *Room { + node, ok := cache.Map[roomID] + if ok && node != nil { + return node + } + return nil +} +func (cache *RoomCache) Put(room *Room) { + cache.Lock() + node := cache.get(room.ID) + if node != nil { + cache.touch(node) + } else { + cache.Map[room.ID] = room + if room.Loaded() { + cache.llPush(room) + } + node = room + } + cache.Unlock() + node.Save() +} + +func (cache *RoomCache) roomPath(roomID string) string { + return filepath.Join(cache.directory, roomID+".gob.gz") +} + +func (cache *RoomCache) Load(roomID string) *Room { + cache.Lock() + defer cache.Unlock() + node, ok := cache.Map[roomID] + if ok { + return node + } + + node = NewRoom(roomID, cache) + node.Load() + return node +} + +func (cache *RoomCache) llPop(node *Room) { + if node.prev == nil && node.next == nil { + return + } + if node.prev != nil { + node.prev.next = node.next + } + if node.next != nil { + node.next.prev = node.prev + } + if node == cache.tail { + cache.tail = node.next + } + if node == cache.head { + cache.head = node.prev + } + node.next = nil + node.prev = nil + cache.size-- +} + +func (cache *RoomCache) llPush(node *Room) { + if node.next != nil || node.prev != nil { + debug.PrintStack() + debug.Print("Tried to llPush node that is already in stack") + return + } + if node == cache.head { + return + } + if cache.head != nil { + cache.head.next = node + } + node.prev = cache.head + node.next = nil + cache.head = node + if cache.tail == nil { + cache.tail = node + } + cache.size++ + cache.clean(false) +} + +func (cache *RoomCache) ForceClean() { + cache.Lock() + cache.clean(true) + cache.Unlock() +} + +func (cache *RoomCache) clean(force bool) { + origSize := cache.size + maxTS := time.Now().Unix() - cache.maxAge + for cache.size > cache.maxSize { + if cache.tail.touch > maxTS && !force { + break + } + ok := cache.tail.Unload() + node := cache.tail + cache.llPop(node) + if !ok { + debug.Print("Unload returned false, pushing node back") + cache.llPush(node) + } + } + if cleaned := origSize - cache.size; cleaned > 0 { + debug.Print("Cleaned", cleaned, "rooms") + } +} + +func (cache *RoomCache) Unload(node *Room) { + cache.Lock() + defer cache.Unlock() + cache.llPop(node) + ok := node.Unload() + if !ok { + debug.Print("Unload returned false, pushing node back") + cache.llPush(node) + } +} + +func (cache *RoomCache) newRoom(roomID string) *Room { + node := NewRoom(roomID, cache) + cache.Map[node.ID] = node + return node +} diff --git a/matrix/sync.go b/matrix/sync.go index 7f9c902..ab0e9f8 100644 --- a/matrix/sync.go +++ b/matrix/sync.go @@ -107,8 +107,6 @@ func NewGomuksSyncer(session SyncerSession) *GomuksSyncer { // ProcessResponse processes a Matrix sync response. func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) { debug.Print("Received sync response") -// dat, _ := json.MarshalIndent(res, "", " ") -// debug.Print(string(dat)) s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence) s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData) @@ -215,6 +213,10 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage { Timeline: mautrix.FilterPart{ Types: []string{ "m.room.message", + "m.room.encrypted", + "m.sticker", + "m.reaction", + "m.room.member", "m.room.name", "m.room.topic", diff --git a/ui/command-processor.go b/ui/command-processor.go index 96b1ada..26644c5 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -108,6 +108,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "rainbow": cmdRainbow, "invite": cmdInvite, "hprof": cmdHeapProfile, + "cprof": cmdCPUProfile, + "trace": cmdTrace, }, } } diff --git a/ui/commands.go b/ui/commands.go index 5bcba98..0b15d8e 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -19,10 +19,15 @@ package ui import ( "encoding/json" "fmt" + "io" "os" "runtime" + dbg "runtime/debug" "runtime/pprof" + "runtime/trace" + "strconv" "strings" + "time" "unicode" "github.com/lucasb-eyer/go-colorful" @@ -72,40 +77,80 @@ var rainbow = GradientTable{ func cmdHeapProfile(cmd *Command) { runtime.GC() - memProfile, err := os.Create("gomuks.prof") + dbg.FreeOSMemory() + memProfile, err := os.Create("gomuks.heap.prof") if err != nil { - debug.Print(err) + debug.Print("Failed to open gomuks.heap.prof:", err) + return } - defer memProfile.Close() + defer func() { + err := memProfile.Close() + if err != nil { + debug.Print("Failed to close gomuks.heap.prof:", err) + } + }() if err := pprof.WriteHeapProfile(memProfile); err != nil { - debug.Print(err) + debug.Print("Heap profile error:", err) } } +func runTimedProfile(cmd *Command, start func(writer io.Writer) error, stop func(), task, file string) { + if len(cmd.Args) == 0 { + cmd.Reply("Usage: /%s <seconds>", cmd.Command) + } else if dur, err := strconv.Atoi(cmd.Args[0]); err != nil || dur < 0 { + cmd.Reply("Usage: /%s <seconds>", cmd.Command) + } else if cpuProfile, err := os.Create(file); err != nil { + debug.Printf("Failed to open %s: %v", file, err) + } else if err = start(cpuProfile); err != nil { + _ = cpuProfile.Close() + debug.Print(task, "error:", err) + } else { + cmd.Reply("Started %s for %d seconds", task, dur) + go func() { + time.Sleep(time.Duration(dur) * time.Second) + stop() + cmd.Reply("%s finished.", task) + + err := cpuProfile.Close() + if err != nil { + debug.Print("Failed to close gomuks.cpu.prof:", err) + } + }() + } +} + +func cmdCPUProfile(cmd *Command) { + runTimedProfile(cmd, pprof.StartCPUProfile, pprof.StopCPUProfile, "CPU profiling", "gomuks.cpu.prof") +} + +func cmdTrace(cmd *Command) { + runTimedProfile(cmd, trace.Start, trace.Stop, "Call tracing", "gomuks.trace") +} + // TODO this command definitely belongs in a plugin once we have a plugin system. func cmdRainbow(cmd *Command) { text := strings.Join(cmd.Args, " ") var html strings.Builder - fmt.Fprint(&html, "**🌈** ") + _, _ = fmt.Fprint(&html, "**🌈** ") for i, char := range text { if unicode.IsSpace(char) { html.WriteRune(char) continue } color := rainbow.GetInterpolatedColorFor(float64(i) / float64(len(text))).Hex() - fmt.Fprintf(&html, "<font color=\"%s\">%c</font>", color, char) + _, _ = fmt.Fprintf(&html, "<font color=\"%s\">%c</font>", color, char) } go cmd.Room.SendMessage("m.text", html.String()) cmd.UI.Render() } func cmdQuit(cmd *Command) { - cmd.Gomuks.Stop() + cmd.Gomuks.Stop(true) } func cmdClearCache(cmd *Command) { cmd.Config.Clear() - cmd.Gomuks.Stop() + cmd.Gomuks.Stop(false) } func cmdUnknownCommand(cmd *Command) { diff --git a/ui/message-view.go b/ui/message-view.go index 75cd022..9aa38a1 100644 --- a/ui/message-view.go +++ b/ui/message-view.go @@ -25,6 +25,7 @@ import ( "github.com/mattn/go-runewidth" sync "github.com/sasha-s/go-deadlock" + "maunium.net/go/mautrix" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -58,11 +59,13 @@ type MessageView struct { prevPrefs config.UserPreferences messageIDLock sync.RWMutex - messageIDs map[string]messages.UIMessage + messageIDs map[string]*messages.UIMessage messagesLock sync.RWMutex - messages []messages.UIMessage + messages []*messages.UIMessage msgBufferLock sync.RWMutex - msgBuffer []messages.UIMessage + msgBuffer []*messages.UIMessage + + initialHistoryLoaded bool } func NewMessageView(parent *RoomView) *MessageView { @@ -74,9 +77,9 @@ func NewMessageView(parent *RoomView) *MessageView { TimestampWidth: len(messages.TimeFormat), ScrollOffset: 0, - messages: make([]messages.UIMessage, 0), - messageIDs: make(map[string]messages.UIMessage), - msgBuffer: make([]messages.UIMessage, 0), + messages: make([]*messages.UIMessage, 0), + messageIDs: make(map[string]*messages.UIMessage), + msgBuffer: make([]*messages.UIMessage, 0), _width: 80, _widestSender: 5, @@ -86,6 +89,23 @@ func NewMessageView(parent *RoomView) *MessageView { } } +func (view *MessageView) Unload() { + debug.Print("Unloading message view", view.parent.Room.ID) + view.messagesLock.Lock() + view.msgBufferLock.Lock() + view.messageIDLock.Lock() + view.messageIDs = make(map[string]*messages.UIMessage) + view.msgBuffer = make([]*messages.UIMessage, 0) + view.messages = make([]*messages.UIMessage, 0) + view.initialHistoryLoaded = false + view.ScrollOffset = 0 + view._widestSender = 5 + view.prevMsgCount = -1 + view.messagesLock.Unlock() + view.msgBufferLock.Unlock() + view.messageIDLock.Unlock() +} + func (view *MessageView) updateWidestSender(sender string) { if len(sender) > int(view._widestSender) { if len(sender) > view.MaxSenderWidth { @@ -108,20 +128,22 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir if ifcMessage == nil { return } - message, ok := ifcMessage.(messages.UIMessage) - if !ok { + message, ok := ifcMessage.(*messages.UIMessage) + if !ok || message == nil { debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().") debug.PrintStack() return } - var oldMsg messages.UIMessage - if oldMsg = view.getMessageByID(message.ID()); oldMsg != nil { + var oldMsg *messages.UIMessage + if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil { view.replaceMessage(oldMsg, message) direction = IgnoreMessage - } else if oldMsg = view.getMessageByID(message.TxnID()); oldMsg != nil { + } else if oldMsg = view.getMessageByID(message.TxnID); oldMsg != nil { view.replaceMessage(oldMsg, message) - view.deleteMessageID(message.TxnID()) + view.deleteMessageID(message.TxnID) + direction = IgnoreMessage + } else if oldMsg = view.getMessageByID(message.Relation.GetReplaceID()); oldMsg != nil { direction = IgnoreMessage } @@ -134,7 +156,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir } message.CalculateBuffer(view.config.Preferences, width) - makeDateChange := func() messages.UIMessage { + makeDateChange := func() *messages.UIMessage { dateChange := messages.NewDateChangeMessage( fmt.Sprintf("Date changed to %s", message.FormatDate())) dateChange.CalculateBuffer(view.config.Preferences, width) @@ -157,9 +179,9 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir } else if direction == PrependMessage { view.messagesLock.Lock() if len(view.messages) > 0 && !view.messages[0].SameDate(message) { - view.messages = append([]messages.UIMessage{message, makeDateChange()}, view.messages...) + view.messages = append([]*messages.UIMessage{message, makeDateChange()}, view.messages...) } else { - view.messages = append([]messages.UIMessage{message}, view.messages...) + view.messages = append([]*messages.UIMessage{message}, view.messages...) } view.messagesLock.Unlock() } else if oldMsg != nil { @@ -174,7 +196,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir } } -func (view *MessageView) replaceMessage(original messages.UIMessage, new messages.UIMessage) { +func (view *MessageView) replaceMessage(original *messages.UIMessage, new *messages.UIMessage) { if len(new.ID()) > 0 { view.setMessageID(new) } @@ -187,7 +209,10 @@ func (view *MessageView) replaceMessage(original messages.UIMessage, new message view.messagesLock.Unlock() } -func (view *MessageView) getMessageByID(id string) messages.UIMessage { +func (view *MessageView) getMessageByID(id string) *messages.UIMessage { + if id == "" { + return nil + } view.messageIDLock.RLock() defer view.messageIDLock.RUnlock() msg, ok := view.messageIDs[id] @@ -198,31 +223,37 @@ func (view *MessageView) getMessageByID(id string) messages.UIMessage { } func (view *MessageView) deleteMessageID(id string) { + if id == "" { + return + } view.messageIDLock.Lock() delete(view.messageIDs, id) view.messageIDLock.Unlock() } -func (view *MessageView) setMessageID(message messages.UIMessage) { +func (view *MessageView) setMessageID(message *messages.UIMessage) { + if message.ID() == "" { + return + } view.messageIDLock.Lock() view.messageIDs[message.ID()] = message view.messageIDLock.Unlock() } -func (view *MessageView) appendBuffer(message messages.UIMessage) { +func (view *MessageView) appendBuffer(message *messages.UIMessage) { view.msgBufferLock.Lock() view.appendBufferUnlocked(message) view.msgBufferLock.Unlock() } -func (view *MessageView) appendBufferUnlocked(message messages.UIMessage) { +func (view *MessageView) appendBufferUnlocked(message *messages.UIMessage) { for i := 0; i < message.Height(); i++ { view.msgBuffer = append(view.msgBuffer, message) } view.prevMsgCount++ } -func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages.UIMessage) { +func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messages.UIMessage) { start := -1 end := -1 view.msgBufferLock.RLock() @@ -240,7 +271,7 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages if start == -1 { debug.Print("Called replaceBuffer() with message that was not in the buffer:", original) - debug.PrintStack() + //debug.PrintStack() view.appendBuffer(new) return } @@ -280,7 +311,7 @@ func (view *MessageView) recalculateBuffers() { if !prefs.BareMessageView { width -= view.TimestampWidth + TimestampSenderGap + view.widestSender() + SenderMessageGap } - view.msgBuffer = []messages.UIMessage{} + view.msgBuffer = []*messages.UIMessage{} view.prevMsgCount = 0 for i, message := range view.messages { if message == nil { @@ -299,17 +330,17 @@ func (view *MessageView) recalculateBuffers() { view.prevPrefs = prefs } -func (view *MessageView) handleMessageClick(message messages.UIMessage) bool { - switch message := message.(type) { +func (view *MessageView) handleMessageClick(message *messages.UIMessage) bool { + switch msg := message.Renderer.(type) { case *messages.ImageMessage: - open.Open(message.Path()) - case messages.UIMessage: + open.Open(msg.Path()) + default: debug.Print("Message clicked:", message) } return false } -func (view *MessageView) handleUsernameClick(message messages.UIMessage, prevMessage messages.UIMessage) bool { +func (view *MessageView) handleUsernameClick(message *messages.UIMessage, prevMessage *messages.UIMessage) bool { if prevMessage != nil && prevMessage.Sender() == message.Sender() { return false } @@ -317,7 +348,7 @@ func (view *MessageView) handleUsernameClick(message messages.UIMessage, prevMes if len(message.Sender()) == 0 { return false } - sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", message.Sender(), message.SenderID()) + sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", message.Sender(), message.SenderID) cursorPos := view.parent.input.GetCursorOffset() text := view.parent.input.GetText() @@ -363,7 +394,7 @@ func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool { view.msgBufferLock.RLock() message := view.msgBuffer[line] - var prevMessage messages.UIMessage + var prevMessage *messages.UIMessage if y != 0 && line > 0 { prevMessage = view.msgBuffer[line-1] } @@ -496,7 +527,7 @@ func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX func (view *MessageView) CapturePlaintext(height int) string { var buf strings.Builder indexOffset := view.TotalHeight() - view.ScrollOffset - height - var prevMessage messages.UIMessage + var prevMessage *messages.UIMessage view.msgBufferLock.RLock() for line := 0; line < height; line++ { index := indexOffset + line @@ -504,14 +535,13 @@ func (view *MessageView) CapturePlaintext(height int) string { continue } - meta := view.msgBuffer[index] - message, ok := meta.(messages.UIMessage) - if ok && message != prevMessage { + message := view.msgBuffer[index] + if message != prevMessage { var sender string if len(message.Sender()) > 0 { sender = fmt.Sprintf(" <%s>", message.Sender()) - } else if message.Type() == "m.emote" { - sender = fmt.Sprintf(" * %s", message.RealSender()) + } else if message.Type == mautrix.MsgEmote { + sender = fmt.Sprintf(" * %s", message.SenderName) } fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText()) prevMessage = message @@ -561,7 +591,7 @@ func (view *MessageView) Draw(screen mauview.Screen) { } } - var prevMsg messages.UIMessage + var prevMsg *messages.UIMessage view.msgBufferLock.RLock() for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); line++ { index := indexOffset + line diff --git a/ui/messages/base.go b/ui/messages/base.go index 05df72b..ef495fb 100644 --- a/ui/messages/base.go +++ b/ui/messages/base.go @@ -27,44 +27,60 @@ import ( "maunium.net/go/tcell" "maunium.net/go/gomuks/interface" - "maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/widget" ) -type BaseMessage struct { - MsgID string - MsgTxnID string - MsgType mautrix.MessageType - MsgSenderID string - MsgSender string - MsgSenderColor tcell.Color - MsgTimestamp time.Time - MsgState mautrix.OutgoingEventState - MsgIsHighlight bool - MsgIsService bool - MsgSource json.RawMessage - ReplyTo UIMessage - buffer []tstring.TString -} - -func newBaseMessage(event *mautrix.Event, displayname string) BaseMessage { +type MessageRenderer interface { + Draw(screen mauview.Screen) + NotificationContent() string + PlainText() string + CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage) + RegisterMatrix(matrix ifc.MatrixContainer) + Height() int + Clone() MessageRenderer + String() string +} + +type UIMessage struct { + EventID string + TxnID string + Relation mautrix.RelatesTo + Type mautrix.MessageType + SenderID string + SenderName string + DefaultSenderColor tcell.Color + Timestamp time.Time + State mautrix.OutgoingEventState + IsHighlight bool + IsService bool + Source json.RawMessage + ReplyTo *UIMessage + Renderer MessageRenderer +} + +const DateFormat = "January _2, 2006" +const TimeFormat = "15:04:05" + +func newUIMessage(event *mautrix.Event, displayname string, renderer MessageRenderer) *UIMessage { msgtype := event.Content.MsgType if len(msgtype) == 0 { msgtype = mautrix.MessageType(event.Type.String()) } - return BaseMessage{ - MsgSenderID: event.Sender, - MsgSender: displayname, - MsgTimestamp: unixToTime(event.Timestamp), - MsgSenderColor: widget.GetHashColor(event.Sender), - MsgType: msgtype, - MsgID: event.ID, - MsgTxnID: event.Unsigned.TransactionID, - MsgState: event.Unsigned.OutgoingState, - MsgIsHighlight: false, - MsgIsService: false, - MsgSource: event.Content.VeryRaw, + return &UIMessage{ + SenderID: event.Sender, + SenderName: displayname, + Timestamp: unixToTime(event.Timestamp), + DefaultSenderColor: widget.GetHashColor(event.Sender), + Type: msgtype, + EventID: event.ID, + TxnID: event.Unsigned.TransactionID, + Relation: *event.Content.GetRelatesTo(), + State: event.Unsigned.OutgoingState, + IsHighlight: false, + IsService: false, + Source: event.Content.VeryRaw, + Renderer: renderer, } } @@ -76,44 +92,38 @@ func unixToTime(unix int64) time.Time { return timestamp } -func (msg *BaseMessage) RegisterMatrix(matrix ifc.MatrixContainer) {} - // Sender gets the string that should be displayed as the sender of this message. // // If the message is being sent, the sender is "Sending...". // If sending has failed, the sender is "Error". // If the message is an emote, the sender is blank. // In any other case, the sender is the display name of the user who sent the message. -func (msg *BaseMessage) Sender() string { - switch msg.MsgState { +func (msg *UIMessage) Sender() string { + switch msg.State { case mautrix.EventStateLocalEcho: return "Sending..." case mautrix.EventStateSendFail: return "Error" } - switch msg.MsgType { + switch msg.Type { case "m.emote": // Emotes don't show a separate sender, it's included in the buffer. return "" default: - return msg.MsgSender + return msg.SenderName } } -func (msg *BaseMessage) SenderID() string { - return msg.MsgSenderID +func (msg *UIMessage) NotificationSenderName() string { + return msg.SenderName } -func (msg *BaseMessage) RealSender() string { - return msg.MsgSender +func (msg *UIMessage) NotificationContent() string { + return msg.Renderer.NotificationContent() } -func (msg *BaseMessage) NotificationSenderName() string { - return msg.MsgSender -} - -func (msg *BaseMessage) getStateSpecificColor() tcell.Color { - switch msg.MsgState { +func (msg *UIMessage) getStateSpecificColor() tcell.Color { + switch msg.State { case mautrix.EventStateLocalEcho: return tcell.ColorGray case mautrix.EventStateSendFail: @@ -132,31 +142,31 @@ func (msg *BaseMessage) getStateSpecificColor() tcell.Color { // // In any other case, the color is whatever is specified in the Message struct. // Usually that means it is the hash-based color of the sender (see ui/widget/color.go) -func (msg *BaseMessage) SenderColor() tcell.Color { +func (msg *UIMessage) SenderColor() tcell.Color { stateColor := msg.getStateSpecificColor() switch { case stateColor != tcell.ColorDefault: return stateColor - case msg.MsgType == "m.room.member": - return widget.GetHashColor(msg.MsgSender) - case msg.MsgIsService: + case msg.Type == "m.room.member": + return widget.GetHashColor(msg.SenderName) + case msg.IsService: return tcell.ColorGray default: - return msg.MsgSenderColor + return msg.DefaultSenderColor } } // TextColor returns the color the actual content of the message should be shown in. -func (msg *BaseMessage) TextColor() tcell.Color { +func (msg *UIMessage) TextColor() tcell.Color { stateColor := msg.getStateSpecificColor() switch { case stateColor != tcell.ColorDefault: return stateColor - case msg.MsgIsService, msg.MsgType == "m.notice": + case msg.IsService, msg.Type == "m.notice": return tcell.ColorGray - case msg.MsgIsHighlight: + case msg.IsHighlight: return tcell.ColorYellow - case msg.MsgType == "m.room.member": + case msg.Type == "m.room.member": return tcell.ColorGreen default: return tcell.ColorDefault @@ -169,14 +179,14 @@ func (msg *BaseMessage) TextColor() tcell.Color { // gray and red respectively. // // However, other messages are the default color instead of a color stored in the struct. -func (msg *BaseMessage) TimestampColor() tcell.Color { - if msg.MsgIsService { +func (msg *UIMessage) TimestampColor() tcell.Color { + if msg.IsService { return tcell.ColorGray } return msg.getStateSpecificColor() } -func (msg *BaseMessage) ReplyHeight() int { +func (msg *UIMessage) ReplyHeight() int { if msg.ReplyTo != nil { return 1 + msg.ReplyTo.Height() } @@ -184,102 +194,78 @@ func (msg *BaseMessage) ReplyHeight() int { } // Height returns the number of rows in the computed buffer (see Buffer()). -func (msg *BaseMessage) Height() int { - return msg.ReplyHeight() + len(msg.buffer) +func (msg *UIMessage) Height() int { + return msg.ReplyHeight() + msg.Renderer.Height() } -// Timestamp returns the full timestamp when the message was sent. -func (msg *BaseMessage) Timestamp() time.Time { - return msg.MsgTimestamp +func (msg *UIMessage) Time() time.Time { + return msg.Timestamp } // FormatTime returns the formatted time when the message was sent. -func (msg *BaseMessage) FormatTime() string { - return msg.MsgTimestamp.Format(TimeFormat) +func (msg *UIMessage) FormatTime() string { + return msg.Timestamp.Format(TimeFormat) } // FormatDate returns the formatted date when the message was sent. -func (msg *BaseMessage) FormatDate() string { - return msg.MsgTimestamp.Format(DateFormat) +func (msg *UIMessage) FormatDate() string { + return msg.Timestamp.Format(DateFormat) } -func (msg *BaseMessage) SameDate(message UIMessage) bool { - year1, month1, day1 := msg.Timestamp().Date() - year2, month2, day2 := message.Timestamp().Date() +func (msg *UIMessage) SameDate(message *UIMessage) bool { + year1, month1, day1 := msg.Timestamp.Date() + year2, month2, day2 := message.Timestamp.Date() return day1 == day2 && month1 == month2 && year1 == year2 } -func (msg *BaseMessage) ID() string { - if len(msg.MsgID) == 0 { - return msg.MsgTxnID +func (msg *UIMessage) ID() string { + if len(msg.EventID) == 0 { + return msg.TxnID } - return msg.MsgID -} - -func (msg *BaseMessage) SetID(id string) { - msg.MsgID = id -} - -func (msg *BaseMessage) TxnID() string { - return msg.MsgTxnID -} - -func (msg *BaseMessage) Type() mautrix.MessageType { - return msg.MsgType -} - -func (msg *BaseMessage) State() mautrix.OutgoingEventState { - return msg.MsgState -} - -func (msg *BaseMessage) SetState(state mautrix.OutgoingEventState) { - msg.MsgState = state -} - -func (msg *BaseMessage) IsHighlight() bool { - return msg.MsgIsHighlight -} - -func (msg *BaseMessage) SetIsHighlight(isHighlight bool) { - msg.MsgIsHighlight = isHighlight + return msg.EventID } -func (msg *BaseMessage) Source() json.RawMessage { - return msg.MsgSource +func (msg *UIMessage) SetID(id string) { + msg.EventID = id } -func (msg *BaseMessage) SetReplyTo(event UIMessage) { - msg.ReplyTo = event +func (msg *UIMessage) SetIsHighlight(isHighlight bool) { + // TODO Textmessage cache needs to be cleared + msg.IsHighlight = isHighlight } -func (msg *BaseMessage) Draw(screen mauview.Screen) { +func (msg *UIMessage) Draw(screen mauview.Screen) { screen = msg.DrawReply(screen) - for y, line := range msg.buffer { - line.Draw(screen, 0, y) - } + msg.Renderer.Draw(screen) } -func (msg *BaseMessage) clone() BaseMessage { +func (msg *UIMessage) Clone() *UIMessage { clone := *msg - clone.buffer = nil - return clone + clone.ReplyTo = nil + clone.Renderer = clone.Renderer.Clone() + return &clone } -func (msg *BaseMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) { +func (msg *UIMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) { if msg.ReplyTo == nil { return } msg.ReplyTo.CalculateBuffer(preferences, width-1) } -func (msg *BaseMessage) DrawReply(screen mauview.Screen) mauview.Screen { +func (msg *UIMessage) CalculateBuffer(preferences config.UserPreferences, width int) { + msg.Renderer.CalculateBuffer(preferences, width, msg) + msg.CalculateReplyBuffer(preferences, width) +} + +func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen { if msg.ReplyTo == nil { return screen } width, height := screen.Size() replyHeight := msg.ReplyTo.Height() widget.WriteLineSimpleColor(screen, "In reply to", 1, 0, tcell.ColorGreen) - widget.WriteLineSimpleColor(screen, msg.ReplyTo.RealSender(), 13, 0, msg.ReplyTo.SenderColor()) + widget.WriteLineSimpleColor(screen, msg.ReplyTo.SenderName, 13, 0, msg.ReplyTo.SenderColor()) for y := 0; y < 1+replyHeight; y++ { screen.SetCell(0, y, tcell.StyleDefault, '▊') } @@ -288,16 +274,21 @@ func (msg *BaseMessage) DrawReply(screen mauview.Screen) mauview.Screen { return mauview.NewProxyScreen(screen, 0, replyHeight+1, width, height-replyHeight-1) } -func (msg *BaseMessage) String() string { - return fmt.Sprintf(`&messages.BaseMessage{ +func (msg *UIMessage) String() string { + return fmt.Sprintf(`&messages.UIMessage{ ID="%s", TxnID="%s", Type="%s", Timestamp=%s, Sender={ID="%s", Name="%s", Color=#%X}, IsService=%t, IsHighlight=%t, + Renderer=%s, }`, - msg.MsgID, msg.MsgTxnID, - msg.MsgType, msg.MsgTimestamp.String(), - msg.MsgSenderID, msg.MsgSender, msg.MsgSenderColor.Hex(), - msg.MsgIsService, msg.MsgIsHighlight, + msg.EventID, msg.TxnID, + msg.Type, msg.Timestamp.String(), + msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(), + msg.IsService, msg.IsHighlight, msg.Renderer.String(), ) } + +func (msg *UIMessage) PlainText() string { + return msg.Renderer.PlainText() +} diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go index cf71ba1..c9cbf0c 100644 --- a/ui/messages/expandedtextmessage.go +++ b/ui/messages/expandedtextmessage.go @@ -17,9 +17,12 @@ package messages import ( + "fmt" "time" + ifc "maunium.net/go/gomuks/interface" "maunium.net/go/mautrix" + "maunium.net/go/mauview" "maunium.net/go/tcell" "maunium.net/go/gomuks/config" @@ -27,55 +30,63 @@ import ( ) type ExpandedTextMessage struct { - BaseMessage - MsgText tstring.TString + Text tstring.TString + buffer []tstring.TString } // NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. -func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) UIMessage { - return &ExpandedTextMessage{ - BaseMessage: newBaseMessage(event, displayname), - MsgText: text, - } +func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) *UIMessage { + return newUIMessage(event, displayname, &ExpandedTextMessage{ + Text: text, + }) } -func NewDateChangeMessage(text string) UIMessage { +func NewDateChangeMessage(text string) *UIMessage { midnight := time.Now() midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(), 0, 0, 0, 0, midnight.Location()) - return &ExpandedTextMessage{ - BaseMessage: BaseMessage{ - MsgSenderID: "*", - MsgSender: "*", - MsgTimestamp: midnight, - MsgIsService: true, + return &UIMessage{ + SenderID: "*", + SenderName: "*", + Timestamp: midnight, + IsService: true, + Renderer: &ExpandedTextMessage{ + Text: tstring.NewColorTString(text, tcell.ColorGreen), }, - MsgText: tstring.NewColorTString(text, tcell.ColorGreen), } } - -func (msg *ExpandedTextMessage) Clone() UIMessage { +func (msg *ExpandedTextMessage) Clone() MessageRenderer { return &ExpandedTextMessage{ - BaseMessage: msg.BaseMessage.clone(), - MsgText: msg.MsgText.Clone(), + Text: msg.Text.Clone(), } } -func (msg *ExpandedTextMessage) GenerateText() tstring.TString { - return msg.MsgText -} - func (msg *ExpandedTextMessage) NotificationContent() string { - return msg.MsgText.String() + return msg.Text.String() } func (msg *ExpandedTextMessage) PlainText() string { - return msg.MsgText.String() + return msg.Text.String() +} + +func (msg *ExpandedTextMessage) String() string { + return fmt.Sprintf(`&messages.ExpandedTextMessage{Text="%s"}`, msg.Text.String()) +} + +func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) { + msg.buffer = calculateBufferWithText(prefs, msg.Text, width, uiMsg) } -func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { - msg.CalculateReplyBuffer(prefs, width) - msg.calculateBufferWithText(prefs, msg.MsgText, width) +func (msg *ExpandedTextMessage) Height() int { + return len(msg.buffer) } + +func (msg *ExpandedTextMessage) Draw(screen mauview.Screen) { + for y, line := range msg.buffer { + line.Draw(screen, 0, y) + } +} + +func (msg *ExpandedTextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {} diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go index 30b1588..5b95a82 100644 --- a/ui/messages/htmlmessage.go +++ b/ui/messages/htmlmessage.go @@ -17,9 +17,7 @@ package messages import ( - "fmt" - "strings" - + ifc "maunium.net/go/gomuks/interface" "maunium.net/go/mautrix" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -29,30 +27,27 @@ import ( ) type HTMLMessage struct { - BaseMessage - Root html.Entity FocusedBg tcell.Color focused bool } -func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) UIMessage { - return &HTMLMessage{ - BaseMessage: newBaseMessage(event, displayname), - Root: root, - } +func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) *UIMessage { + return newUIMessage(event, displayname, &HTMLMessage{ + Root: root, + }) } -func (hw *HTMLMessage) Clone() UIMessage { +func (hw *HTMLMessage) RegisterMatrix(matrix ifc.MatrixContainer) {} + +func (hw *HTMLMessage) Clone() MessageRenderer { return &HTMLMessage{ - BaseMessage: hw.BaseMessage.clone(), - Root: hw.Root.Clone(), - FocusedBg: hw.FocusedBg, + Root: hw.Root.Clone(), + FocusedBg: hw.FocusedBg, } } func (hw *HTMLMessage) Draw(screen mauview.Screen) { - screen = hw.DrawReply(screen) if hw.focused { screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg)) } @@ -80,18 +75,17 @@ func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool { return false } -func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int) { +func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int, msg *UIMessage) { if width < 2 { return } - hw.CalculateReplyBuffer(preferences, width) // TODO account for bare messages in initial startX startX := 0 hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView) } func (hw *HTMLMessage) Height() int { - return hw.ReplyHeight() + hw.Root.Height() + return hw.Root.Height() } func (hw *HTMLMessage) PlainText() string { @@ -103,8 +97,5 @@ func (hw *HTMLMessage) NotificationContent() string { } func (hw *HTMLMessage) String() string { - return fmt.Sprintf("&messages.HTMLMessage{\n" + - " Base=%s,\n" + - " Root=||\n%s\n" + - "}", strings.Replace(hw.BaseMessage.String(), "\n", "\n ", -1), hw.Root.String()) + return hw.Root.String() } diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go index c891687..399c941 100644 --- a/ui/messages/imagemessage.go +++ b/ui/messages/imagemessage.go @@ -23,6 +23,7 @@ import ( "image/color" "maunium.net/go/mautrix" + "maunium.net/go/mauview" "maunium.net/go/tcell" "maunium.net/go/gomuks/config" @@ -33,32 +34,30 @@ import ( ) type ImageMessage struct { - BaseMessage Body string Homeserver string FileID string data []byte + buffer []tstring.TString matrix ifc.MatrixContainer } // NewImageMessage creates a new ImageMessage object with the provided values and the default state. -func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) UIMessage { - return &ImageMessage{ - newBaseMessage(event, displayname), - body, - homeserver, - fileID, - data, - matrix, - } +func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) *UIMessage { + return newUIMessage(event, displayname, &ImageMessage{ + Body: body, + Homeserver: homeserver, + FileID: fileID, + data: data, + matrix: matrix, + }) } -func (msg *ImageMessage) Clone() UIMessage { +func (msg *ImageMessage) Clone() MessageRenderer { data := make([]byte, len(msg.data)) copy(data, msg.data) return &ImageMessage{ - BaseMessage: msg.BaseMessage.clone(), Body: msg.Body, Homeserver: msg.Homeserver, FileID: msg.FileID, @@ -83,6 +82,10 @@ func (msg *ImageMessage) PlainText() string { return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.Homeserver, msg.FileID)) } +func (msg *ImageMessage) String() string { + return fmt.Sprintf(`&messages.ImageMessage{Body="%s", Homeserver="%s", FileID="%s"}`, msg.Body, msg.Homeserver, msg.FileID) +} + func (msg *ImageMessage) updateData() { defer debug.Recover() debug.Print("Loading image:", msg.Homeserver, msg.FileID) @@ -103,15 +106,13 @@ func (msg *ImageMessage) Path() string { // of the text of this message split into lines at most as wide as the width // parameter. If the message width is larger than the width of the buffer // the message is scaled to one third the buffer width. -func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int) { +func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) { if width < 2 { return } - msg.CalculateReplyBuffer(prefs, width) - if prefs.BareMessageView || prefs.DisableImages { - msg.calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width) + msg.buffer = calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width, uiMsg) return } @@ -133,3 +134,13 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int msg.buffer = ansImage.Render() } + +func (msg *ImageMessage) Height() int { + return len(msg.buffer) +} + +func (msg *ImageMessage) Draw(screen mauview.Screen) { + for y, line := range msg.buffer { + line.Draw(screen, 0, y) + } +} diff --git a/ui/messages/message.go b/ui/messages/message.go deleted file mode 100644 index c990368..0000000 --- a/ui/messages/message.go +++ /dev/null @@ -1,53 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// 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 -// GNU Affero General Public License for more details. -// -// 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/>. - -package messages - -import ( - "maunium.net/go/gomuks/config" - "maunium.net/go/gomuks/interface" - "maunium.net/go/mautrix" - "maunium.net/go/mauview" - "maunium.net/go/tcell" -) - -// UIMessage is a wrapper for the content and metadata of a Matrix message intended to be displayed. -type UIMessage interface { - ifc.Message - - Type() mautrix.MessageType - Sender() string - SenderColor() tcell.Color - TextColor() tcell.Color - TimestampColor() tcell.Color - FormatTime() string - FormatDate() string - SameDate(message UIMessage) bool - - SetReplyTo(message UIMessage) - CalculateBuffer(preferences config.UserPreferences, width int) - Draw(screen mauview.Screen) - Height() int - PlainText() string - - Clone() UIMessage - - RealSender() string - RegisterMatrix(matrix ifc.MatrixContainer) -} - -const DateFormat = "January _2, 2006" -const TimeFormat = "15:04:05" diff --git a/ui/messages/parser.go b/ui/messages/parser.go index 0723257..29f078c 100644 --- a/ui/messages/parser.go +++ b/ui/messages/parser.go @@ -31,10 +31,10 @@ import ( "maunium.net/go/gomuks/ui/widget" ) -func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage { +func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage { if roomView := mainView.GetRoom(roomID); roomView != nil { if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil { - if replyToMsg, ok := replyToIfcMsg.(UIMessage); ok && replyToMsg != nil { + if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil { return replyToMsg } } @@ -42,24 +42,17 @@ func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage { return nil } -func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *mautrix.Event) UIMessage { +func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *mautrix.Event) *UIMessage { msg := directParseEvent(matrix, room, evt) if msg == nil { return nil } if len(evt.Content.GetReplyTo()) > 0 { - replyToRoom := room - if len(evt.Content.RelatesTo.InReplyTo.RoomID) > 0 { - replyToRoom = matrix.GetRoom(evt.Content.RelatesTo.InReplyTo.RoomID) - } - - if replyToMsg := getCachedEvent(mainView, replyToRoom.ID, evt.Content.GetReplyTo()); replyToMsg != nil { - replyToMsg = replyToMsg.Clone() - replyToMsg.SetReplyTo(nil) - msg.SetReplyTo(replyToMsg) - } else if replyToEvt, _ := matrix.GetEvent(replyToRoom, evt.Content.GetReplyTo()); replyToEvt != nil { - if replyToMsg := directParseEvent(matrix, replyToRoom, replyToEvt); replyToMsg != nil { - msg.SetReplyTo(replyToMsg) + if replyToMsg := getCachedEvent(mainView, room.ID, evt.Content.GetReplyTo()); replyToMsg != nil { + msg.ReplyTo = replyToMsg.Clone() + } else if replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()); replyToEvt != nil { + if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil { + msg.ReplyTo = replyToMsg } else { // TODO add unrenderable reply header } @@ -70,15 +63,22 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R return msg } -func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { +func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) *UIMessage { + displayname := evt.Sender + member := room.GetMember(evt.Sender) + if member != nil { + displayname = member.Displayname + } switch evt.Type { case mautrix.EventSticker: evt.Content.MsgType = mautrix.MsgImage fallthrough case mautrix.EventMessage: - return ParseMessage(matrix, room, evt) + return ParseMessage(matrix, room, evt, displayname) + case mautrix.EventEncrypted: + return NewExpandedTextMessage(evt, displayname, tstring.NewStyleTString("Encrypted messages are not yet supported", tcell.StyleDefault.Italic(true))) case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias: - return ParseStateEvent(matrix, room, evt) + return ParseStateEvent(evt, displayname) case mautrix.StateMember: return ParseMembershipEvent(room, evt) } @@ -86,12 +86,7 @@ func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix return nil } -func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { - displayname := evt.Sender - member := room.GetMember(evt.Sender) - if member != nil { - displayname = member.Displayname - } +func ParseStateEvent(evt *mautrix.Event, displayname string) *UIMessage { text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)) switch evt.Type { case mautrix.StateTopic: @@ -124,15 +119,13 @@ func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix. return NewExpandedTextMessage(evt, displayname, text) } -func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { - displayname := evt.Sender - member := room.GetMember(evt.Sender) - if member != nil { - displayname = member.Displayname - } +func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event, displayname string) *UIMessage { if len(evt.Content.GetReplyTo()) > 0 { evt.Content.RemoveReplyFallback() } + if evt.Content.GetRelatesTo().Type == mautrix.RelReplace && evt.Content.NewContent != nil { + evt.Content = *evt.Content.NewContent + } switch evt.Content.MsgType { case "m.text", "m.notice", "m.emote": if evt.Content.Format == mautrix.FormatHTML { @@ -224,7 +217,7 @@ func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender str return } -func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) UIMessage { +func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) *UIMessage { displayname, text := getMembershipEventContent(room, evt) if len(text) == 0 { return nil diff --git a/ui/messages/textbase.go b/ui/messages/textbase.go index 321d998..0d1cc3b 100644 --- a/ui/messages/textbase.go +++ b/ui/messages/textbase.go @@ -52,12 +52,12 @@ func matchBoundaryPattern(bare bool, extract tstring.TString) tstring.TString { // CalculateBuffer generates the internal buffer for this message that consists // of the text of this message split into lines at most as wide as the width // parameter. -func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int) { +func calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int, msg *UIMessage) []tstring.TString { if width < 2 { - return + return nil } - msg.buffer = []tstring.TString{} + var buffer []tstring.TString if prefs.BareMessageView { newText := tstring.NewTString(msg.FormatTime()) @@ -74,7 +74,7 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te newlines := 0 for _, str := range forcedLinebreaks { if len(str) == 0 && newlines < 1 { - msg.buffer = append(msg.buffer, tstring.TString{}) + buffer = append(buffer, tstring.TString{}) newlines++ } else { newlines = 0 @@ -88,8 +88,9 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te } extract = matchBoundaryPattern(prefs.BareMessageView, extract) } - msg.buffer = append(msg.buffer, extract) + buffer = append(buffer, extract) str = str[len(extract):] } } + return buffer } diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go index f8c4573..9ace201 100644 --- a/ui/messages/textmessage.go +++ b/ui/messages/textmessage.go @@ -20,72 +20,82 @@ import ( "fmt" "time" + ifc "maunium.net/go/gomuks/interface" "maunium.net/go/mautrix" + "maunium.net/go/mauview" "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/ui/messages/tstring" ) type TextMessage struct { - BaseMessage - cache tstring.TString - MsgText string + cache tstring.TString + buffer []tstring.TString + Text string } // NewTextMessage creates a new UITextMessage object with the provided values and the default state. -func NewTextMessage(event *mautrix.Event, displayname string, text string) UIMessage { - return &TextMessage{ - BaseMessage: newBaseMessage(event, displayname), - MsgText: text, - } +func NewTextMessage(event *mautrix.Event, displayname string, text string) *UIMessage { + return newUIMessage(event, displayname, &TextMessage{ + Text: text, + }) } -func NewServiceMessage(text string) UIMessage { - return &TextMessage{ - BaseMessage: BaseMessage{ - MsgSenderID: "*", - MsgSender: "*", - MsgTimestamp: time.Now(), - MsgIsService: true, +func NewServiceMessage(text string) *UIMessage { + return &UIMessage{ + SenderID: "*", + SenderName: "*", + Timestamp: time.Now(), + IsService: true, + Renderer: &TextMessage{ + Text: text, }, - MsgText: text, } } -func (msg *TextMessage) Clone() UIMessage { +func (msg *TextMessage) Clone() MessageRenderer { return &TextMessage{ - BaseMessage: msg.BaseMessage.clone(), - MsgText: msg.MsgText, + Text: msg.Text, } } -func (msg *TextMessage) getCache() tstring.TString { +func (msg *TextMessage) getCache(uiMsg *UIMessage) tstring.TString { if msg.cache == nil { - switch msg.MsgType { + switch uiMsg.Type { case "m.emote": - msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor()) - msg.cache.Colorize(0, len(msg.MsgSender)+2, msg.SenderColor()) + msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", uiMsg.SenderName, msg.Text), uiMsg.TextColor()) + msg.cache.Colorize(0, len(uiMsg.SenderName)+2, uiMsg.SenderColor()) default: - msg.cache = tstring.NewColorTString(msg.MsgText, msg.TextColor()) + msg.cache = tstring.NewColorTString(msg.Text, uiMsg.TextColor()) } } return msg.cache } -func (msg *TextMessage) SetIsHighlight(isHighlight bool) { - msg.BaseMessage.SetIsHighlight(isHighlight) - msg.cache = nil -} - func (msg *TextMessage) NotificationContent() string { - return msg.MsgText + return msg.Text } func (msg *TextMessage) PlainText() string { - return msg.MsgText + return msg.Text +} + +func (msg *TextMessage) String() string { + return fmt.Sprintf(`&messages.TextMessage{Text="%s"}`, msg.Text) } -func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { - msg.CalculateReplyBuffer(prefs, width) - msg.calculateBufferWithText(prefs, msg.getCache(), width) +func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) { + msg.buffer = calculateBufferWithText(prefs, msg.getCache(uiMsg), width, uiMsg) } + +func (msg *TextMessage) Height() int { + return len(msg.buffer) +} + +func (msg *TextMessage) Draw(screen mauview.Screen) { + for y, line := range msg.buffer { + line.Draw(screen, 0, y) + } +} + +func (msg *TextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {} diff --git a/ui/room-list.go b/ui/room-list.go index 6b22c8e..53337b6 100644 --- a/ui/room-list.go +++ b/ui/room-list.go @@ -236,6 +236,10 @@ func (list *RoomList) AddScrollOffset(offset int) { func (list *RoomList) First() (string, *rooms.Room) { list.RLock() defer list.RUnlock() + return list.first() +} + +func (list *RoomList) first() (string, *rooms.Room) { for _, tag := range list.tags { trl := list.items[tag] if trl.HasVisibleRooms() { @@ -248,6 +252,10 @@ func (list *RoomList) First() (string, *rooms.Room) { func (list *RoomList) Last() (string, *rooms.Room) { list.RLock() defer list.RUnlock() + return list.last() +} + +func (list *RoomList) last() (string, *rooms.Room) { for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- { tag := list.tags[tagIndex] trl := list.items[tag] @@ -273,7 +281,7 @@ func (list *RoomList) Previous() (string, *rooms.Room) { if len(list.items) == 0 { return "", nil } else if list.selected == nil { - return list.First() + return list.first() } trl := list.items[list.selectedTag] @@ -295,11 +303,11 @@ func (list *RoomList) Previous() (string, *rooms.Room) { return prevTag, prevTRL.LastVisible() } } - return list.Last() + return list.last() } else if index >= 0 { return list.selectedTag, trl.Visible()[index+1].Room } - return list.First() + return list.first() } func (list *RoomList) Next() (string, *rooms.Room) { @@ -308,7 +316,7 @@ func (list *RoomList) Next() (string, *rooms.Room) { if len(list.items) == 0 { return "", nil } else if list.selected == nil { - return list.First() + return list.first() } trl := list.items[list.selectedTag] @@ -330,11 +338,11 @@ func (list *RoomList) Next() (string, *rooms.Room) { return nextTag, nextTRL.FirstVisible() } } - return list.First() + return list.first() } else if index > 0 { return list.selectedTag, trl.Visible()[index-1].Room } - return list.Last() + return list.last() } // NextWithActivity Returns next room with activity. @@ -415,8 +423,10 @@ func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool { switch event.Buttons() { case tcell.WheelUp: list.AddScrollOffset(-WheelScrollOffsetDiff) + return true case tcell.WheelDown: list.AddScrollOffset(WheelScrollOffsetDiff) + return true case tcell.Button1: x, y := event.Position() return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl) @@ -478,7 +488,8 @@ func (list *RoomList) clickRoom(line, column int, mod bool) bool { if trl.maxShown < 10 { trl.maxShown = 10 } - break + list.RUnlock() + return true } } // Tag footer diff --git a/ui/room-view.go b/ui/room-view.go index a2e1bc1..00aa09a 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -57,6 +57,8 @@ type RoomView struct { ulBorderScreen *mauview.ProxyScreen ulScreen *mauview.ProxyScreen + userListLoaded bool + prevScreen mauview.Screen parent *MainView @@ -91,6 +93,14 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView { config: parent.config, } view.content = NewMessageView(view) + view.Room.SetPreUnload(func() bool { + if view.parent.currentRoom == view { + return false + } + view.content.Unload() + return true + }) + view.Room.SetPostLoad(view.loadTyping) view.input. SetBackgroundColor(tcell.ColorDefault). @@ -99,7 +109,6 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView { SetTabCompleteFunc(view.InputTabComplete) view.topic. - SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). SetTextColor(tcell.ColorWhite). SetBackgroundColor(tcell.ColorDarkGreen) @@ -269,14 +278,20 @@ func (view *RoomView) SetCompletions(completions []string) { view.completions.time = time.Now() } -func (view *RoomView) SetTyping(users []string) { - for index, user := range users { +func (view *RoomView) loadTyping() { + for index, user := range view.typing { member := view.Room.GetMember(user) if member != nil { - users[index] = member.Displayname + view.typing[index] = member.Displayname } } +} + +func (view *RoomView) SetTyping(users []string) { view.typing = users + if view.Room.Loaded() { + view.loadTyping() + } } type completion struct { @@ -385,11 +400,11 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { text = emoji.Sprint(text) } evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text) - msg := view.ParseEvent(evt) + msg := view.parseEvent(evt) view.AddMessage(msg) eventID, err := view.parent.matrix.SendEvent(evt) if err != nil { - msg.SetState(mautrix.EventStateSendFail) + msg.State = mautrix.EventStateSendFail // Show shorter version if available if httpErr, ok := err.(mautrix.HTTPError); ok { err = httpErr @@ -401,7 +416,10 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) { view.parent.parent.Render() } else { debug.Print("Event ID received:", eventID) - //view.MessageView().UpdateMessageID(msg, eventID) + msg.EventID = eventID + msg.State = mautrix.EventStateDefault + view.MessageView().setMessageID(msg) + view.parent.parent.Render() } } @@ -413,12 +431,20 @@ func (view *RoomView) MxRoom() *rooms.Room { return view.Room } +func (view *RoomView) Update() { + view.topic.SetText(strings.Replace(view.Room.GetTopic(), "\n", " ", -1)) + if !view.userListLoaded { + view.UpdateUserList() + } +} + func (view *RoomView) UpdateUserList() { pls := &mautrix.PowerLevels{} if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil { pls = plEvent.Content.GetPowerLevels() } view.userList.Update(view.Room.GetMembers(), pls) + view.userListLoaded = true } func (view *RoomView) AddServiceMessage(text string) { @@ -429,10 +455,18 @@ func (view *RoomView) AddMessage(message ifc.Message) { view.content.AddMessage(message, AppendMessage) } -func (view *RoomView) ParseEvent(evt *mautrix.Event) ifc.Message { +func (view *RoomView) parseEvent(evt *mautrix.Event) *messages.UIMessage { return messages.ParseEvent(view.parent.matrix, view.parent, view.Room, evt) } +func (view *RoomView) ParseEvent(evt *mautrix.Event) ifc.Message { + msg := view.parseEvent(evt) + if msg == nil { + return nil + } + return msg +} + func (view *RoomView) GetEvent(eventID string) ifc.Message { message, ok := view.content.messageIDs[eventID] if !ok { diff --git a/ui/view-login.go b/ui/view-login.go index b929d62..a65a77c 100644 --- a/ui/view-login.go +++ b/ui/view-login.go @@ -74,7 +74,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component { view.username.SetText(ui.gmx.Config().UserID) view.password.SetMaskCharacter('*') - view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorDarkCyan) + view.quitButton.SetOnClick(func() { ui.gmx.Stop(true) }).SetBackgroundColor(tcell.ColorDarkCyan) view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorDarkCyan) view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1}) diff --git a/ui/view-main.go b/ui/view-main.go index 48004c1..ea24644 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -26,6 +26,7 @@ import ( sync "github.com/sasha-s/go-deadlock" + "maunium.net/go/gomuks/ui/messages" "maunium.net/go/mauview" "maunium.net/go/tcell" @@ -256,6 +257,7 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) { if room == nil { return } + room.Load() roomView, ok := view.getRoomView(room.ID, lock) if !ok { @@ -263,12 +265,15 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) { debug.Print(tag, room) return } + roomView.Update() view.roomView.SetInnerComponent(roomView) view.currentRoom = roomView view.MarkRead(roomView) view.roomList.SetSelected(tag, room) view.parent.Render() - if len(roomView.MessageView().messages) == 0 { + + if msgView := roomView.MessageView(); len(msgView.messages) < 20 && !msgView.initialHistoryLoaded { + msgView.initialHistoryLoaded = true go view.LoadHistory(room.ID) } } @@ -278,12 +283,6 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView { roomView := NewRoomView(view, room). SetInputChangedFunc(view.InputChanged) view.rooms[room.ID] = roomView - roomView.UpdateUserList() - - // TODO make sure this works - if len(roomView.MessageView().messages) == 0 { - go view.LoadHistory(room.ID) - } return roomView } return nil @@ -292,7 +291,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView { func (view *MainView) GetRoom(roomID string) ifc.RoomView { room, ok := view.getRoomView(roomID, true) if !ok { - return view.addRoom(view.matrix.GetRoom(roomID)) + return view.addRoom(view.matrix.GetOrCreateRoom(roomID)) } return room } @@ -348,11 +347,11 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView { return roomView } -func (view *MainView) SetRooms(rooms map[string]*rooms.Room) { +func (view *MainView) SetRooms(rooms *rooms.RoomCache) { view.roomList.Clear() view.roomsLock.Lock() view.rooms = make(map[string]*RoomView) - for _, room := range rooms { + for _, room := range rooms.Map { if room.HasLeft { continue } @@ -388,9 +387,14 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo notification.Send(sender, text, critical, sound) } -func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) { +func (view *MainView) Bump(room *rooms.Room) { view.roomList.Bump(room) - if message.SenderID() == view.config.UserID { +} + +func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) { + view.Bump(room) + uiMsg, ok := message.(*messages.UIMessage) + if ok && uiMsg.SenderID == view.config.UserID { return } // Whether or not the room where the message came is the currently shown room. @@ -420,16 +424,6 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul message.SetIsHighlight(should.Highlight) } -func (view *MainView) InitialSyncDone() { - view.roomList.Clear() - view.roomsLock.RLock() - for _, room := range view.rooms { - view.roomList.Add(room.Room) - room.UpdateUserList() - } - view.roomsLock.RUnlock() -} - func (view *MainView) LoadHistory(roomID string) { defer debug.Recover() roomView, ok := view.getRoomView(roomID, true) |