Merge branch 'break-things-again'

This commit is contained in:
Tulir Asokan 2019-06-16 15:18:25 +03:00
commit 8b87809ac1
28 changed files with 1268 additions and 637 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ gomuks
coverage.out
coverage.html
deb/usr
*.prof

View File

@ -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"),
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"),
Rooms: make(map[string]*rooms.Room),
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)
}

30
go.mod
View File

@ -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
)

68
go.sum
View File

@ -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=

View File

@ -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()

View File

@ -27,5 +27,5 @@ type Gomuks interface {
Config() *config.Config
Start()
Stop()
Stop(save bool)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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://(.+)/(.+)")
@ -642,7 +707,7 @@ func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err err
}
}
data, err = c.download(hs, id, cacheFile)
//FIXME data, err = c.download(hs, id, cacheFile)
return
}

View File

@ -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) Save(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
func (room *Room) load() {
if room.Loaded() {
return
}
defer file.Close()
enc := gob.NewEncoder(file)
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) 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 {
debug.Print("Failed to open room state file for writing:", err)
return
}
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(),
}
}

319
matrix/rooms/roomcache.go Normal file
View File

@ -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
}

View File

@ -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",

View File

@ -108,6 +108,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"rainbow": cmdRainbow,
"invite": cmdInvite,
"hprof": cmdHeapProfile,
"cprof": cmdCPUProfile,
"trace": cmdTrace,
},
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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
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
}
func newBaseMessage(event *mautrix.Event, displayname string) BaseMessage {
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
return msg.EventID
}
func (msg *BaseMessage) SetID(id string) {
msg.MsgID = id
func (msg *UIMessage) SetID(id string) {
msg.EventID = id
}
func (msg *BaseMessage) TxnID() string {
return msg.MsgTxnID
func (msg *UIMessage) SetIsHighlight(isHighlight bool) {
// TODO Textmessage cache needs to be cleared
msg.IsHighlight = isHighlight
}
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
}
func (msg *BaseMessage) Source() json.RawMessage {
return msg.MsgSource
}
func (msg *BaseMessage) SetReplyTo(event UIMessage) {
msg.ReplyTo = event
}
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()
}

View File

@ -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) CalculateBuffer(prefs config.UserPreferences, width int) {
msg.CalculateReplyBuffer(prefs, width)
msg.calculateBufferWithText(prefs, msg.MsgText, width)
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) 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) {}

View File

@ -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()
}

View File

@ -22,6 +22,7 @@ import (
"image/color"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
@ -32,32 +33,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,
@ -70,7 +69,7 @@ func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) {
msg.matrix = matrix
if len(msg.data) == 0 {
go msg.updateData()
//FIXME go msg.updateData()
}
}
@ -82,6 +81,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)
@ -101,14 +104,13 @@ func (msg *ImageMessage) Path() string {
// 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 *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
}
@ -121,3 +123,13 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int
msg.buffer = image.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)
}
}

View File

@ -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"

View File

@ -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

View File

@ -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
}

View File

@ -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) CalculateBuffer(prefs config.UserPreferences, width int) {
msg.CalculateReplyBuffer(prefs, width)
msg.calculateBufferWithText(prefs, msg.getCache(), width)
func (msg *TextMessage) String() string {
return fmt.Sprintf(`&messages.TextMessage{Text="%s"}`, msg.Text)
}
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) {}

View File

@ -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

View File

@ -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 {

View File

@ -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})

View File

@ -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)