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.out
coverage.html coverage.html
deb/usr deb/usr
*.prof

View File

@ -53,15 +53,19 @@ type Config struct {
AccessToken string `yaml:"access_token"` AccessToken string `yaml:"access_token"`
HS string `yaml:"homeserver"` HS string `yaml:"homeserver"`
Dir string `yaml:"-"` RoomCacheSize int `yaml:"room_cache_size"`
CacheDir string `yaml:"cache_dir"` RoomCacheAge int64 `yaml:"room_cache_age"`
HistoryPath string `yaml:"history_path"`
MediaDir string `yaml:"media_dir"` Dir string `yaml:"-"`
StateDir string `yaml:"state_dir"` 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:"-"` Preferences UserPreferences `yaml:"-"`
AuthCache AuthCache `yaml:"-"` AuthCache AuthCache `yaml:"-"`
Rooms map[string]*rooms.Room `yaml:"-"` Rooms *rooms.RoomCache `yaml:"-"`
PushRules *pushrules.PushRuleset `yaml:"-"` PushRules *pushrules.PushRuleset `yaml:"-"`
nosave bool nosave bool
@ -70,36 +74,39 @@ type Config struct {
// NewConfig creates a config that loads data from the given directory. // NewConfig creates a config that loads data from the given directory.
func NewConfig(configDir, cacheDir string) *Config { func NewConfig(configDir, cacheDir string) *Config {
return &Config{ return &Config{
Dir: configDir, Dir: configDir,
CacheDir: cacheDir, CacheDir: cacheDir,
HistoryPath: filepath.Join(cacheDir, "history.db"), HistoryPath: filepath.Join(cacheDir, "history.db"),
StateDir: filepath.Join(cacheDir, "state"), RoomListPath: filepath.Join(cacheDir, "rooms.gob.gz"),
MediaDir: filepath.Join(cacheDir, "media"), 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. // Clear clears the session cache and removes all history.
func (config *Config) Clear() { func (config *Config) Clear() {
os.Remove(config.HistoryPath) _ = os.Remove(config.HistoryPath)
os.RemoveAll(config.StateDir) _ = os.Remove(config.RoomListPath)
os.RemoveAll(config.MediaDir) _ = os.RemoveAll(config.StateDir)
os.RemoveAll(config.CacheDir) _ = os.RemoveAll(config.MediaDir)
_ = os.RemoveAll(config.CacheDir)
config.nosave = true config.nosave = true
} }
func (config *Config) CreateCacheDirs() { func (config *Config) CreateCacheDirs() {
os.MkdirAll(config.CacheDir, 0700) _ = os.MkdirAll(config.CacheDir, 0700)
os.MkdirAll(config.StateDir, 0700) _ = os.MkdirAll(config.StateDir, 0700)
os.MkdirAll(config.MediaDir, 0700) _ = os.MkdirAll(config.MediaDir, 0700)
} }
func (config *Config) DeleteSession() { func (config *Config) DeleteSession() {
config.AuthCache.NextBatch = "" config.AuthCache.NextBatch = ""
config.AuthCache.InitialSyncDone = false config.AuthCache.InitialSyncDone = false
config.AccessToken = "" 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.PushRules = nil
config.Clear() config.Clear()
@ -109,10 +116,14 @@ func (config *Config) DeleteSession() {
func (config *Config) LoadAll() { func (config *Config) LoadAll() {
config.Load() config.Load()
config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID)
config.LoadAuthCache() config.LoadAuthCache()
config.LoadPushRules() config.LoadPushRules()
config.LoadPreferences() 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. // 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.SaveAuthCache()
config.SavePushRules() config.SavePushRules()
config.SavePreferences() 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. // 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) config.save("push rules", config.CacheDir, "pushrules.json", &config.PushRules)
} }
func (config *Config) LoadRooms() { func (config *Config) load(name, dir, file string, target interface{}) {
os.MkdirAll(config.StateDir, 0700) err := os.MkdirAll(dir, 0700)
roomFiles, err := ioutil.ReadDir(config.StateDir)
if err != nil { if err != nil {
debug.Print("Failed to list rooms state caches in", config.StateDir) debug.Print("Failed to create", dir)
panic(err) 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) path := filepath.Join(dir, file)
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
@ -229,9 +209,12 @@ func (config *Config) save(name, dir, file string, source interface{}) {
return 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 data []byte
var err error
if strings.HasSuffix(file, ".yaml") { if strings.HasSuffix(file, ".yaml") {
data, err = yaml.Marshal(source) data, err = yaml.Marshal(source)
} else { } else {
@ -272,30 +255,14 @@ func (config *Config) LoadNextBatch(_ string) string {
return config.AuthCache.NextBatch 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) { func (config *Config) SaveRoom(room *mautrix.Room) {
gmxRoom := config.GetRoom(room.ID) panic("SaveRoom is not supported")
gmxRoom.Room = room
gmxRoom.Save(config.getRoomCachePath(gmxRoom))
} }
func (config *Config) LoadRoom(roomID string) *mautrix.Room { 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 module maunium.net/go/gomuks
go 1.12 go 1.11
require ( require (
github.com/alecthomas/chroma v0.6.3 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/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
github.com/disintegration/imaging v1.6.0 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/kyokomi/emoji v2.1.0+incompatible
github.com/lithammer/fuzzysearch v1.0.2 github.com/lithammer/fuzzysearch v1.0.2
github.com/lucasb-eyer/go-colorful 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.8 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mattn/go-runewidth v0.0.4 github.com/mattn/go-runewidth v0.0.4
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // 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/sasha-s/go-deadlock v0.2.0
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/testify v1.3.0
github.com/stretchr/objx v0.2.0 // indirect
go.etcd.io/bbolt v1.3.2 go.etcd.io/bbolt v1.3.2
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 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/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/russross/blackfriday.v2 v2.0.1 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 gopkg.in/yaml.v2 v2.2.2
maunium.net/go/mautrix v0.1.0-alpha.3.0.20190512142959-897a8c5be1d9 maunium.net/go/mautrix v0.1.0-alpha.3.0.20190616114735-e5bf3141e88e
maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125 maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d
maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed
) )
replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1 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/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/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 h1:8H1D0yddf0mvgvO4JDBKnzLd9ERmzzAijBxnZXGV/FA=
github.com/alecthomas/chroma v0.6.3/go.mod h1:quT2EpvJNqkuPi6DmBHB+E33FXBgBBPzyH5++Dn1LPc= 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/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.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-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/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 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 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.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/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 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= 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/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 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 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 h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/lithammer/fuzzysearch v1.0.2 h1:AjCE2iwc5y+8K+h2nXVc0Pmrpjvu+JVqMgiZ0oakXDM= 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 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 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.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.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.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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/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 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= 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/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-20190513083848-b9f5b9457d44 h1:XKCbzPvK4/BbMXoMJOkYP2ANxiAEO0HM1xn6psSbXxY=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE= 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 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y=
github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= 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/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 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.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.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/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 h1:Lhqt2eo+rgM8aswvM7nTtAMVm8ARPWzkE9n6eZDOccY=
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY= 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 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 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-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-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-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ=
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/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-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-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-20190606122018-79a91cf218c4 h1:3i7qG/aA9NUAzdnJHfhgxSKSmxbAebomYR5IZgFbC5Y=
golang.org/x/sys v0.0.0-20190425145619-16072639606e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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-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 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/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/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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.20190606153009-ca5d9535b6cc h1:G7Nse6r/XaCu+p7yc/3m/nfFuOFZZ87Hb3AOX4INOEk=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20190512142959-897a8c5be1d9/go.mod h1:cyZKXVQphK5gnbKMvRLXoxfKMySfAQJvn8ttR4x/23c= maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg=
maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125 h1:wSrf+ZYCavbnU21f3Q1fKytL2mJyCyi2+Dosbck780Q= maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac h1:r0X7mMPcc8eJaCaHdbW9ibfCLe3EruuqZIH2FM8oLIs=
maunium.net/go/mauview v0.0.0-20190426104003-3e5387b8a125/go.mod h1:TIbj5iET7pJSq4SpVxZ080mAe6dgoQxGN5oRHwnSXnI= maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg=
maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b h1:xncLOTadq4VzDKFUV7Jm3OSkehS2KAL88pfwowcAmRA= maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d h1:H4wZ4vMVnOh5QFsb4xZtssgpv3DDEkBRzQ8iyEg2fX0=
maunium.net/go/tcell v0.0.0-20190426103942-24a060c2189b/go.mod h1:V4YmSYrOlCtlTM188iXR8VwWSo+ksAVawxQLXibeAyQ= 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. // Save saves the active session and message history.
func (gmx *Gomuks) Save() { func (gmx *Gomuks) Save() {
gmx.config.SaveAll() gmx.config.SaveAll()
//debug.Print("Saving history...")
//gmx.ui.MainView().SaveAllHistory()
} }
// StartAutosave calls Save() every minute until it receives a stop signal // StartAutosave calls Save() every minute until it receives a stop signal
@ -70,7 +68,9 @@ func (gmx *Gomuks) StartAutosave() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
gmx.Save() if gmx.config.AuthCache.InitialSyncDone {
gmx.Save()
}
case val := <-gmx.stop: case val := <-gmx.stop:
if val { if val {
return return
@ -81,13 +81,15 @@ func (gmx *Gomuks) StartAutosave() {
// Stop stops the Matrix syncer, the tview app and the autosave goroutine, // Stop stops the Matrix syncer, the tview app and the autosave goroutine,
// then saves everything and calls os.Exit(0). // then saves everything and calls os.Exit(0).
func (gmx *Gomuks) Stop() { func (gmx *Gomuks) Stop(save bool) {
debug.Print("Disconnecting from Matrix...") debug.Print("Disconnecting from Matrix...")
gmx.matrix.Stop() gmx.matrix.Stop()
debug.Print("Cleaning up UI...") debug.Print("Cleaning up UI...")
gmx.ui.Stop() gmx.ui.Stop()
gmx.stop <- true gmx.stop <- true
gmx.Save() if save {
gmx.Save()
}
os.Exit(0) os.Exit(0)
} }
@ -102,7 +104,7 @@ func (gmx *Gomuks) Start() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
<-c <-c
gmx.Stop() gmx.Stop(true)
}() }()
go gmx.StartAutosave() go gmx.StartAutosave()

View File

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

View File

@ -45,6 +45,7 @@ type MatrixContainer interface {
GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, error) GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, error)
GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, error) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, error)
GetRoom(roomID string) *rooms.Room GetRoom(roomID string) *rooms.Room
GetOrCreateRoom(roomID string) *rooms.Room
Download(mxcURL string) ([]byte, string, string, error) Download(mxcURL string) ([]byte, string, string, error)
GetDownloadURL(homeserver, fileID string) string GetDownloadURL(homeserver, fileID string) string

View File

@ -43,14 +43,14 @@ type MainView interface {
GetRoom(roomID string) RoomView GetRoom(roomID string) RoomView
AddRoom(room *rooms.Room) AddRoom(room *rooms.Room)
RemoveRoom(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) UpdateTags(room *rooms.Room)
SetTyping(roomID string, users []string) SetTyping(roomID string, users []string)
NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould) NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
InitialSyncDone()
} }
type RoomView interface { type RoomView interface {
@ -68,13 +68,10 @@ type RoomView interface {
type Message interface { type Message interface {
ID() string ID() string
TxnID() string Time() time.Time
SenderID() string
Timestamp() time.Time
NotificationSenderName() string NotificationSenderName() string
NotificationContent() string NotificationContent() string
SetState(state mautrix.OutgoingEventState)
SetIsHighlight(highlight bool) SetIsHighlight(highlight bool)
SetID(id string) SetID(id string)
} }

View File

@ -18,6 +18,7 @@ package matrix
import ( import (
"bytes" "bytes"
"compress/gzip"
"encoding/binary" "encoding/binary"
"encoding/gob" "encoding/gob"
@ -28,6 +29,10 @@ import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
) )
func init() {
gob.Register(&mautrix.Event{})
}
type HistoryManager struct { type HistoryManager struct {
sync.Mutex sync.Mutex
@ -226,13 +231,27 @@ func btoi(b []byte) uint64 {
func marshalEvent(event *mautrix.Event) ([]byte, error) { func marshalEvent(event *mautrix.Event) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(event) enc := gzip.NewWriter(&buf)
return buf.Bytes(), err 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) { func unmarshalEvent(data []byte) (*mautrix.Event, error) {
event := &mautrix.Event{} 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 { func put(streams, eventIDs *bolt.Bucket, event *mautrix.Event, key uint64) error {

View File

@ -29,7 +29,9 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"time" "time"
dbg "runtime/debug"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@ -204,6 +206,9 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer") debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config) c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage) 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.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage) c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage) c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage)
@ -218,9 +223,22 @@ func (c *Container) OnLogin() {
c.syncer.InitDoneCallback = func() { c.syncer.InitDoneCallback = func() {
debug.Print("Initial sync done") debug.Print("Initial sync done")
c.config.AuthCache.InitialSyncDone = true c.config.AuthCache.InitialSyncDone = true
c.config.SaveAuthCache() debug.Print("Updating title caches")
c.ui.MainView().InitialSyncDone() 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() 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 c.client.Syncer = c.syncer
@ -274,7 +292,9 @@ func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) {
return return
} }
debug.Print("Updated preferences:", orig, "->", c.config.Preferences) debug.Print("Updated preferences:", orig, "->", c.config.Preferences)
c.ui.HandleNewPreferences() if c.config.AuthCache.InitialSyncDone {
c.ui.HandleNewPreferences()
}
} }
func (c *Container) SendPreferencesToMatrix() { func (c *Container) SendPreferencesToMatrix() {
@ -289,9 +309,24 @@ func (c *Container) SendPreferencesToMatrix() {
// HandleMessage is the event handler for the m.room.message timeline event. // HandleMessage is the event handler for the m.room.message timeline event.
func (c *Container) HandleMessage(source EventSource, evt *mautrix.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 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() mainView := c.ui.MainView()
roomView := mainView.GetRoom(evt.RoomID) roomView := mainView.GetRoom(evt.RoomID)
@ -300,23 +335,29 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) {
return return
} }
err := c.history.Append(roomView.MxRoom(), []*mautrix.Event{evt}) if !room.Loaded() {
if err != nil { pushRules := c.PushRules().GetActions(room, evt).Should()
debug.Printf("Failed to add event %s to history: %v", evt.ID, err) 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 // TODO switch to roomView.AddEvent
message := roomView.ParseEvent(evt) message := roomView.ParseEvent(evt)
if message != nil { if message != nil {
roomView.AddMessage(message) roomView.AddMessage(message)
roomView.MxRoom().LastReceivedMessage = message.Timestamp() roomView.MxRoom().LastReceivedMessage = message.Time()
if c.syncer.FirstSyncDone { if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should() pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
mainView.NotifyMessage(roomView.MxRoom(), message, pushRules) mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
c.ui.Render() c.ui.Render()
} }
} else { } 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) { func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
isLeave := source&EventSourceLeave != 0 isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0 isTimeline := source&EventSourceTimeline != 0
if isLeave {
c.GetOrCreateRoom(evt.RoomID).HasLeft = true
}
isNonTimelineLeave := isLeave && !isTimeline isNonTimelineLeave := isLeave && !isTimeline
if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
return return
@ -350,11 +394,16 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
room := c.GetRoom(evt.RoomID) room := c.GetRoom(evt.RoomID)
switch membership { switch membership {
case "join": case "join":
c.ui.MainView().AddRoom(room) if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().AddRoom(room)
}
room.HasLeft = false room.HasLeft = false
case "leave": case "leave":
c.ui.MainView().RemoveRoom(room) if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().RemoveRoom(room)
}
room.HasLeft = true room.HasLeft = true
room.Unload()
case "invite": case "invite":
// TODO handle // TODO handle
debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID) 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 := c.GetRoom(evt.RoomID)
room.MarkRead(lastReadEvent) if room != nil {
c.ui.Render() room.MarkRead(lastReadEvent)
if c.config.AuthCache.InitialSyncDone {
c.ui.Render()
}
}
} }
func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool { 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 continue
} }
room := c.GetRoom(roomID) room := c.GetOrCreateRoom(roomID)
if room != nil && !room.HasLeft { if room != nil && !room.HasLeft {
directChats[room] = true 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) { func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) {
directChats := c.parseDirectChatInfo(evt) directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms { for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room] shouldBeDirect := directChats[room]
if shouldBeDirect != room.IsDirect { if shouldBeDirect != room.IsDirect {
room.IsDirect = shouldBeDirect 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. // HandleTag is the event handler for the m.tag account data event.
func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) { func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
room := c.config.GetRoom(evt.RoomID) room := c.GetOrCreateRoom(evt.RoomID)
newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags)) newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags))
index := 0 index := 0
@ -466,14 +521,19 @@ func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
} }
index++ index++
} }
mainView := c.ui.MainView()
room.RawTags = newTags 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. // HandleTyping is the event handler for the m.typing event.
func (c *Container) HandleTyping(source EventSource, evt *mautrix.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) 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 { if err != nil {
return nil, err return nil, err
} }
room := c.GetRoom(resp.RoomID) room := c.GetOrCreateRoom(resp.RoomID)
return room, nil return room, nil
} }
@ -557,7 +617,6 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) {
room := c.GetRoom(resp.RoomID) room := c.GetRoom(resp.RoomID)
room.HasLeft = false room.HasLeft = false
return room, nil return room, nil
} }
@ -568,8 +627,9 @@ func (c *Container) LeaveRoom(roomID string) error {
return err return err
} }
room := c.GetRoom(roomID) node := c.GetOrCreateRoom(roomID)
room.HasLeft = true node.HasLeft = true
node.Unload()
return nil return nil
} }
@ -593,9 +653,9 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, e
return nil, err 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) 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 return resp.Chunk, nil
} }
@ -613,9 +673,14 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event,
return event, nil 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. // GetRoom gets the room instance stored in the session.
func (c *Container) GetRoom(roomID string) *rooms.Room { func (c *Container) GetRoom(roomID string) *rooms.Room {
return c.config.GetRoom(roomID) return c.config.Rooms.Get(roomID)
} }
var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)") 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 return
} }

View File

@ -17,6 +17,7 @@
package rooms package rooms
import ( import (
"compress/gzip"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"os" "os"
@ -31,17 +32,18 @@ import (
) )
func init() { func init() {
gob.Register([]interface{}{})
gob.Register(map[string]interface{}{}) gob.Register(map[string]interface{}{})
gob.Register([]interface{}{})
} }
type RoomNameSource int type RoomNameSource int
const ( const (
ExplicitRoomName RoomNameSource = iota UnknownRoomName RoomNameSource = iota
CanonicalAliasRoomName
AliasRoomName
MemberRoomName MemberRoomName
AliasRoomName
CanonicalAliasRoomName
ExplicitRoomName
) )
// RoomTag is a tag given to a specific room. // RoomTag is a tag given to a specific room.
@ -60,7 +62,8 @@ type UnreadMessage struct {
// Room represents a single Matrix room. // Room represents a single Matrix room.
type Room struct { type Room struct {
*mautrix.Room // The room ID.
ID string
// Whether or not the user has left the room. // Whether or not the user has left the room.
HasLeft bool HasLeft bool
@ -70,6 +73,7 @@ type Room struct {
PrevBatch string PrevBatch string
// The MXID of the user whose session this room was created for. // The MXID of the user whose session this room was created for.
SessionUserID string SessionUserID string
SessionMember *mautrix.Member
// The number of unread messages that were notified about. // The number of unread messages that were notified about.
UnreadMessages []UnreadMessage UnreadMessages []UnreadMessage
@ -79,19 +83,22 @@ type Room struct {
// Whether or not this room is marked as a direct chat. // Whether or not this room is marked as a direct chat.
IsDirect bool IsDirect bool
// List of tags given to this room // List of tags given to this room.
RawTags []RoomTag RawTags []RoomTag
// Timestamp of previously received actual message. // Timestamp of previously received actual message.
LastReceivedMessage time.Time LastReceivedMessage time.Time
// Room state cache.
state map[mautrix.EventType]map[string]*mautrix.Event
// MXID -> Member cache calculated from membership events. // MXID -> Member cache calculated from membership events.
memberCache map[string]*mautrix.Member memberCache map[string]*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. // 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, // The name of the room. Calculated from the state event name,
// canonical_alias or alias or the member cache. // canonical_alias or alias or the member cache.
nameCache string NameCache string
// The event type from which the name cache was calculated from. // The event type from which the name cache was calculated from.
nameCacheSource RoomNameSource nameCacheSource RoomNameSource
// The topic of the room. Directly fetched from the m.room.topic state event. // 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. // The list of aliases. Directly fetched from the m.room.aliases state event.
aliasesCache []string aliasesCache []string
// Path for state store file.
path string
// Room cache object
cache *RoomCache
// Lock for state and other room stuff.
lock sync.RWMutex 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 { func debugPrintError(fn func() error, message string) {
file, err := os.OpenFile(path, os.O_RDONLY, 0600) if err := fn(); err != nil {
if err != nil { debug.Printf("%s: %v", message, err)
return 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() room.lock.Lock()
defer room.lock.Unlock() room.load()
return dec.Decode(room) room.lock.Unlock()
if room.postLoad != nil {
room.postLoad()
}
} }
func (room *Room) Save(path string) error { func (room *Room) load() {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600) if room.Loaded() {
if err != nil { return
return err
} }
defer file.Close() debug.Print("Loading state for room", room.ID, "from disk")
enc := gob.NewEncoder(file) 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() room.lock.RLock()
defer room.lock.RUnlock() 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. // 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 // UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination. // on the type/state_key combination.
func (room *Room) UpdateState(event *mautrix.Event) { func (room *Room) UpdateState(event *mautrix.Event) {
room.Load()
room.lock.Lock() room.lock.Lock()
defer room.lock.Unlock() defer room.lock.Unlock()
_, exists := room.State[event.Type] room.changed = true
_, exists := room.state[event.Type]
if !exists { if !exists {
room.State[event.Type] = make(map[string]*mautrix.Event) room.state[event.Type] = make(map[string]*mautrix.Event)
} }
switch event.Type { switch event.Type {
case mautrix.StateRoomName: case mautrix.StateRoomName:
room.nameCache = "" room.NameCache = event.Content.Name
room.nameCacheSource = ExplicitRoomName
case mautrix.StateCanonicalAlias: case mautrix.StateCanonicalAlias:
if room.nameCacheSource >= CanonicalAliasRoomName { if room.nameCacheSource <= CanonicalAliasRoomName {
room.nameCache = "" room.NameCache = event.Content.Alias
room.nameCacheSource = CanonicalAliasRoomName
} }
room.canonicalAliasCache = "" room.canonicalAliasCache = event.Content.Alias
case mautrix.StateAliases: case mautrix.StateAliases:
if room.nameCacheSource >= AliasRoomName { if room.nameCacheSource <= AliasRoomName {
room.nameCache = "" room.NameCache = ""
} }
room.aliasesCache = nil room.aliasesCache = nil
case mautrix.StateMember: case mautrix.StateMember:
room.memberCache = nil userID := event.GetStateKey()
room.firstMemberCache = nil if userID == room.SessionUserID {
if room.nameCacheSource >= MemberRoomName { room.SessionMember = room.eventToMember(userID, &event.Content)
room.nameCache = "" }
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: case mautrix.StateTopic:
room.topicCache = "" room.topicCache = event.Content.Topic
} }
stateKey := ""
if event.StateKey != nil {
stateKey = *event.StateKey
}
if event.Type != mautrix.StateMember { 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 { if event.StateKey == nil {
room.State[event.Type][""] = event room.state[event.Type][""] = event
} else { } 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. // 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 { func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event {
room.Load()
room.lock.RLock() room.lock.RLock()
defer room.lock.RUnlock() defer room.lock.RUnlock()
stateEventMap, _ := room.State[eventType] stateEventMap, _ := room.state[eventType]
event, _ := stateEventMap[stateKey] event, _ := stateEventMap[stateKey]
return event return event
} }
// getStateEvents returns the state events for the given type. // getStateEvents returns the state events for the given type.
func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event { func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event {
stateEventMap, _ := room.State[eventType] stateEventMap, _ := room.state[eventType]
return stateEventMap return stateEventMap
} }
@ -323,7 +459,7 @@ func (room *Room) GetAliases() []string {
func (room *Room) updateNameFromNameEvent() { func (room *Room) updateNameFromNameEvent() {
nameEvt := room.GetStateEvent(mautrix.StateRoomName, "") nameEvt := room.GetStateEvent(mautrix.StateRoomName, "")
if nameEvt != nil { if nameEvt != nil {
room.nameCache = nameEvt.Content.Name room.NameCache = nameEvt.Content.Name
} }
} }
@ -336,7 +472,7 @@ func (room *Room) updateNameFromAliases() {
aliases := room.GetAliases() aliases := room.GetAliases()
if len(aliases) > 0 { if len(aliases) > 0 {
sort.Sort(sort.StringSlice(aliases)) 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() { func (room *Room) updateNameFromMembers() {
members := room.GetMembers() members := room.GetMembers()
if len(members) <= 1 { if len(members) <= 1 {
room.nameCache = "Empty room" room.NameCache = "Empty room"
} else if room.firstMemberCache == nil { } else if room.firstMemberCache == nil {
room.nameCache = "Room" room.NameCache = "Room"
} else if len(members) == 2 { } 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 { } else {
firstMember := room.firstMemberCache.Displayname members := room.firstMemberCache.Displayname
room.nameCache = fmt.Sprintf("%s and %d others", firstMember, len(members)-2) 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 // updateNameCache updates the room display name based on the room state in the order
// specified in spec section 11.2.2.5. // specified in spec section 11.2.2.5.
func (room *Room) updateNameCache() { func (room *Room) updateNameCache() {
if len(room.nameCache) == 0 { if len(room.NameCache) == 0 {
room.updateNameFromNameEvent() room.updateNameFromNameEvent()
room.nameCacheSource = ExplicitRoomName room.nameCacheSource = ExplicitRoomName
} }
if len(room.nameCache) == 0 { if len(room.NameCache) == 0 {
room.nameCache = room.GetCanonicalAlias() room.NameCache = room.GetCanonicalAlias()
room.nameCacheSource = CanonicalAliasRoomName room.nameCacheSource = CanonicalAliasRoomName
} }
if len(room.nameCache) == 0 { if len(room.NameCache) == 0 {
room.updateNameFromAliases() room.updateNameFromAliases()
room.nameCacheSource = AliasRoomName room.nameCacheSource = AliasRoomName
} }
if len(room.nameCache) == 0 { if len(room.NameCache) == 0 {
room.updateNameFromMembers() room.updateNameFromMembers()
room.nameCacheSource = MemberRoomName room.nameCacheSource = MemberRoomName
} }
@ -389,27 +532,47 @@ func (room *Room) updateNameCache() {
// If the cache is empty, it is updated first. // If the cache is empty, it is updated first.
func (room *Room) GetTitle() string { func (room *Room) GetTitle() string {
room.updateNameCache() 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. // createMemberCache caches all member events into a easily processable MXID -> *Member map.
func (room *Room) createMemberCache() map[string]*mautrix.Member { func (room *Room) createMemberCache() map[string]*mautrix.Member {
if len(room.memberCache) > 0 {
return room.memberCache
}
cache := make(map[string]*mautrix.Member) cache := make(map[string]*mautrix.Member)
room.lock.RLock() room.lock.RLock()
events := room.getStateEvents(mautrix.StateMember) events := room.getStateEvents(mautrix.StateMember)
room.firstMemberCache = nil room.firstMemberCache = nil
room.secondMemberCache = nil
if events != nil { if events != nil {
for userID, event := range events { for userID, event := range events {
member := &event.Content.Member member := room.eventToMember(userID, &event.Content)
member.Membership = event.Content.Membership
if len(member.Displayname) == 0 {
member.Displayname = userID
}
if room.firstMemberCache == nil && userID != room.SessionUserID {
room.firstMemberCache = member
}
if member.Membership == mautrix.MembershipJoin || member.Membership == mautrix.MembershipInvite { if member.Membership == mautrix.MembershipJoin || member.Membership == mautrix.MembershipInvite {
cache[userID] = member 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. // The members are returned from the cache.
// If the cache is empty, it is updated first. // If the cache is empty, it is updated first.
func (room *Room) GetMembers() map[string]*mautrix.Member { func (room *Room) GetMembers() map[string]*mautrix.Member {
if len(room.memberCache) == 0 || room.firstMemberCache == nil { room.Load()
room.createMemberCache() room.createMemberCache()
}
return room.memberCache return room.memberCache
} }
// GetMember returns the member with the given MXID. // GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned. // If the member doesn't exist, nil is returned.
func (room *Room) GetMember(userID string) *mautrix.Member { func (room *Room) GetMember(userID string) *mautrix.Member {
if len(room.memberCache) == 0 { if userID == room.SessionUserID && room.SessionMember != nil {
room.createMemberCache() return room.SessionMember
} }
room.Load()
room.createMemberCache()
room.lock.RLock() room.lock.RLock()
member, _ := room.memberCache[userID] member, _ := room.memberCache[userID]
room.lock.RUnlock() room.lock.RUnlock()
@ -449,9 +613,13 @@ func (room *Room) GetSessionOwner() string {
} }
// NewRoom creates a new Room with the given ID // NewRoom creates a new Room with the given ID
func NewRoom(roomID, owner string) *Room { func NewRoom(roomID string, cache *RoomCache) *Room {
return &Room{ return &Room{
Room: mautrix.NewRoom(roomID), ID: roomID,
SessionUserID: owner, 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. // ProcessResponse processes a Matrix sync response.
func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) { func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) {
debug.Print("Received sync response") debug.Print("Received sync response")
// dat, _ := json.MarshalIndent(res, "", " ")
// debug.Print(string(dat))
s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence) s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence)
s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData) s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData)
@ -215,6 +213,10 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
Timeline: mautrix.FilterPart{ Timeline: mautrix.FilterPart{
Types: []string{ Types: []string{
"m.room.message", "m.room.message",
"m.room.encrypted",
"m.sticker",
"m.reaction",
"m.room.member", "m.room.member",
"m.room.name", "m.room.name",
"m.room.topic", "m.room.topic",

View File

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

View File

@ -19,10 +19,15 @@ package ui
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"runtime" "runtime"
dbg "runtime/debug"
"runtime/pprof" "runtime/pprof"
"runtime/trace"
"strconv"
"strings" "strings"
"time"
"unicode" "unicode"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
@ -72,40 +77,80 @@ var rainbow = GradientTable{
func cmdHeapProfile(cmd *Command) { func cmdHeapProfile(cmd *Command) {
runtime.GC() runtime.GC()
memProfile, err := os.Create("gomuks.prof") dbg.FreeOSMemory()
memProfile, err := os.Create("gomuks.heap.prof")
if err != nil { 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 { 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. // TODO this command definitely belongs in a plugin once we have a plugin system.
func cmdRainbow(cmd *Command) { func cmdRainbow(cmd *Command) {
text := strings.Join(cmd.Args, " ") text := strings.Join(cmd.Args, " ")
var html strings.Builder var html strings.Builder
fmt.Fprint(&html, "**🌈** ") _, _ = fmt.Fprint(&html, "**🌈** ")
for i, char := range text { for i, char := range text {
if unicode.IsSpace(char) { if unicode.IsSpace(char) {
html.WriteRune(char) html.WriteRune(char)
continue continue
} }
color := rainbow.GetInterpolatedColorFor(float64(i) / float64(len(text))).Hex() 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()) go cmd.Room.SendMessage("m.text", html.String())
cmd.UI.Render() cmd.UI.Render()
} }
func cmdQuit(cmd *Command) { func cmdQuit(cmd *Command) {
cmd.Gomuks.Stop() cmd.Gomuks.Stop(true)
} }
func cmdClearCache(cmd *Command) { func cmdClearCache(cmd *Command) {
cmd.Config.Clear() cmd.Config.Clear()
cmd.Gomuks.Stop() cmd.Gomuks.Stop(false)
} }
func cmdUnknownCommand(cmd *Command) { func cmdUnknownCommand(cmd *Command) {

View File

@ -25,6 +25,7 @@ import (
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
sync "github.com/sasha-s/go-deadlock" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -58,11 +59,13 @@ type MessageView struct {
prevPrefs config.UserPreferences prevPrefs config.UserPreferences
messageIDLock sync.RWMutex messageIDLock sync.RWMutex
messageIDs map[string]messages.UIMessage messageIDs map[string]*messages.UIMessage
messagesLock sync.RWMutex messagesLock sync.RWMutex
messages []messages.UIMessage messages []*messages.UIMessage
msgBufferLock sync.RWMutex msgBufferLock sync.RWMutex
msgBuffer []messages.UIMessage msgBuffer []*messages.UIMessage
initialHistoryLoaded bool
} }
func NewMessageView(parent *RoomView) *MessageView { func NewMessageView(parent *RoomView) *MessageView {
@ -74,9 +77,9 @@ func NewMessageView(parent *RoomView) *MessageView {
TimestampWidth: len(messages.TimeFormat), TimestampWidth: len(messages.TimeFormat),
ScrollOffset: 0, ScrollOffset: 0,
messages: make([]messages.UIMessage, 0), messages: make([]*messages.UIMessage, 0),
messageIDs: make(map[string]messages.UIMessage), messageIDs: make(map[string]*messages.UIMessage),
msgBuffer: make([]messages.UIMessage, 0), msgBuffer: make([]*messages.UIMessage, 0),
_width: 80, _width: 80,
_widestSender: 5, _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) { func (view *MessageView) updateWidestSender(sender string) {
if len(sender) > int(view._widestSender) { if len(sender) > int(view._widestSender) {
if len(sender) > view.MaxSenderWidth { if len(sender) > view.MaxSenderWidth {
@ -108,20 +128,22 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
if ifcMessage == nil { if ifcMessage == nil {
return return
} }
message, ok := ifcMessage.(messages.UIMessage) message, ok := ifcMessage.(*messages.UIMessage)
if !ok { if !ok || message == nil {
debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().") debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().")
debug.PrintStack() debug.PrintStack()
return return
} }
var oldMsg messages.UIMessage var oldMsg *messages.UIMessage
if oldMsg = view.getMessageByID(message.ID()); oldMsg != nil { if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil {
view.replaceMessage(oldMsg, message) view.replaceMessage(oldMsg, message)
direction = IgnoreMessage 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.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 direction = IgnoreMessage
} }
@ -134,7 +156,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
} }
message.CalculateBuffer(view.config.Preferences, width) message.CalculateBuffer(view.config.Preferences, width)
makeDateChange := func() messages.UIMessage { makeDateChange := func() *messages.UIMessage {
dateChange := messages.NewDateChangeMessage( dateChange := messages.NewDateChangeMessage(
fmt.Sprintf("Date changed to %s", message.FormatDate())) fmt.Sprintf("Date changed to %s", message.FormatDate()))
dateChange.CalculateBuffer(view.config.Preferences, width) dateChange.CalculateBuffer(view.config.Preferences, width)
@ -157,9 +179,9 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
} else if direction == PrependMessage { } else if direction == PrependMessage {
view.messagesLock.Lock() view.messagesLock.Lock()
if len(view.messages) > 0 && !view.messages[0].SameDate(message) { 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 { } else {
view.messages = append([]messages.UIMessage{message}, view.messages...) view.messages = append([]*messages.UIMessage{message}, view.messages...)
} }
view.messagesLock.Unlock() view.messagesLock.Unlock()
} else if oldMsg != nil { } 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 { if len(new.ID()) > 0 {
view.setMessageID(new) view.setMessageID(new)
} }
@ -187,7 +209,10 @@ func (view *MessageView) replaceMessage(original messages.UIMessage, new message
view.messagesLock.Unlock() 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() view.messageIDLock.RLock()
defer view.messageIDLock.RUnlock() defer view.messageIDLock.RUnlock()
msg, ok := view.messageIDs[id] msg, ok := view.messageIDs[id]
@ -198,31 +223,37 @@ func (view *MessageView) getMessageByID(id string) messages.UIMessage {
} }
func (view *MessageView) deleteMessageID(id string) { func (view *MessageView) deleteMessageID(id string) {
if id == "" {
return
}
view.messageIDLock.Lock() view.messageIDLock.Lock()
delete(view.messageIDs, id) delete(view.messageIDs, id)
view.messageIDLock.Unlock() 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.messageIDLock.Lock()
view.messageIDs[message.ID()] = message view.messageIDs[message.ID()] = message
view.messageIDLock.Unlock() view.messageIDLock.Unlock()
} }
func (view *MessageView) appendBuffer(message messages.UIMessage) { func (view *MessageView) appendBuffer(message *messages.UIMessage) {
view.msgBufferLock.Lock() view.msgBufferLock.Lock()
view.appendBufferUnlocked(message) view.appendBufferUnlocked(message)
view.msgBufferLock.Unlock() view.msgBufferLock.Unlock()
} }
func (view *MessageView) appendBufferUnlocked(message messages.UIMessage) { func (view *MessageView) appendBufferUnlocked(message *messages.UIMessage) {
for i := 0; i < message.Height(); i++ { for i := 0; i < message.Height(); i++ {
view.msgBuffer = append(view.msgBuffer, message) view.msgBuffer = append(view.msgBuffer, message)
} }
view.prevMsgCount++ view.prevMsgCount++
} }
func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages.UIMessage) { func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messages.UIMessage) {
start := -1 start := -1
end := -1 end := -1
view.msgBufferLock.RLock() view.msgBufferLock.RLock()
@ -240,7 +271,7 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
if start == -1 { if start == -1 {
debug.Print("Called replaceBuffer() with message that was not in the buffer:", original) debug.Print("Called replaceBuffer() with message that was not in the buffer:", original)
debug.PrintStack() //debug.PrintStack()
view.appendBuffer(new) view.appendBuffer(new)
return return
} }
@ -280,7 +311,7 @@ func (view *MessageView) recalculateBuffers() {
if !prefs.BareMessageView { if !prefs.BareMessageView {
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender() + SenderMessageGap width -= view.TimestampWidth + TimestampSenderGap + view.widestSender() + SenderMessageGap
} }
view.msgBuffer = []messages.UIMessage{} view.msgBuffer = []*messages.UIMessage{}
view.prevMsgCount = 0 view.prevMsgCount = 0
for i, message := range view.messages { for i, message := range view.messages {
if message == nil { if message == nil {
@ -299,17 +330,17 @@ func (view *MessageView) recalculateBuffers() {
view.prevPrefs = prefs view.prevPrefs = prefs
} }
func (view *MessageView) handleMessageClick(message messages.UIMessage) bool { func (view *MessageView) handleMessageClick(message *messages.UIMessage) bool {
switch message := message.(type) { switch msg := message.Renderer.(type) {
case *messages.ImageMessage: case *messages.ImageMessage:
open.Open(message.Path()) open.Open(msg.Path())
case messages.UIMessage: default:
debug.Print("Message clicked:", message) debug.Print("Message clicked:", message)
} }
return false 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() { if prevMessage != nil && prevMessage.Sender() == message.Sender() {
return false return false
} }
@ -317,7 +348,7 @@ func (view *MessageView) handleUsernameClick(message messages.UIMessage, prevMes
if len(message.Sender()) == 0 { if len(message.Sender()) == 0 {
return false 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() cursorPos := view.parent.input.GetCursorOffset()
text := view.parent.input.GetText() text := view.parent.input.GetText()
@ -363,7 +394,7 @@ func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
view.msgBufferLock.RLock() view.msgBufferLock.RLock()
message := view.msgBuffer[line] message := view.msgBuffer[line]
var prevMessage messages.UIMessage var prevMessage *messages.UIMessage
if y != 0 && line > 0 { if y != 0 && line > 0 {
prevMessage = view.msgBuffer[line-1] 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 { func (view *MessageView) CapturePlaintext(height int) string {
var buf strings.Builder var buf strings.Builder
indexOffset := view.TotalHeight() - view.ScrollOffset - height indexOffset := view.TotalHeight() - view.ScrollOffset - height
var prevMessage messages.UIMessage var prevMessage *messages.UIMessage
view.msgBufferLock.RLock() view.msgBufferLock.RLock()
for line := 0; line < height; line++ { for line := 0; line < height; line++ {
index := indexOffset + line index := indexOffset + line
@ -504,14 +535,13 @@ func (view *MessageView) CapturePlaintext(height int) string {
continue continue
} }
meta := view.msgBuffer[index] message := view.msgBuffer[index]
message, ok := meta.(messages.UIMessage) if message != prevMessage {
if ok && message != prevMessage {
var sender string var sender string
if len(message.Sender()) > 0 { if len(message.Sender()) > 0 {
sender = fmt.Sprintf(" <%s>", message.Sender()) sender = fmt.Sprintf(" <%s>", message.Sender())
} else if message.Type() == "m.emote" { } else if message.Type == mautrix.MsgEmote {
sender = fmt.Sprintf(" * %s", message.RealSender()) sender = fmt.Sprintf(" * %s", message.SenderName)
} }
fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText()) fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText())
prevMessage = message prevMessage = message
@ -561,7 +591,7 @@ func (view *MessageView) Draw(screen mauview.Screen) {
} }
} }
var prevMsg messages.UIMessage var prevMsg *messages.UIMessage
view.msgBufferLock.RLock() view.msgBufferLock.RLock()
for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); line++ { for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); line++ {
index := indexOffset + line index := indexOffset + line

View File

@ -27,44 +27,60 @@ import (
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
) )
type BaseMessage struct { type MessageRenderer interface {
MsgID string Draw(screen mauview.Screen)
MsgTxnID string NotificationContent() string
MsgType mautrix.MessageType PlainText() string
MsgSenderID string CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage)
MsgSender string RegisterMatrix(matrix ifc.MatrixContainer)
MsgSenderColor tcell.Color Height() int
MsgTimestamp time.Time Clone() MessageRenderer
MsgState mautrix.OutgoingEventState String() string
MsgIsHighlight bool
MsgIsService bool
MsgSource json.RawMessage
ReplyTo UIMessage
buffer []tstring.TString
} }
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 msgtype := event.Content.MsgType
if len(msgtype) == 0 { if len(msgtype) == 0 {
msgtype = mautrix.MessageType(event.Type.String()) msgtype = mautrix.MessageType(event.Type.String())
} }
return BaseMessage{ return &UIMessage{
MsgSenderID: event.Sender, SenderID: event.Sender,
MsgSender: displayname, SenderName: displayname,
MsgTimestamp: unixToTime(event.Timestamp), Timestamp: unixToTime(event.Timestamp),
MsgSenderColor: widget.GetHashColor(event.Sender), DefaultSenderColor: widget.GetHashColor(event.Sender),
MsgType: msgtype, Type: msgtype,
MsgID: event.ID, EventID: event.ID,
MsgTxnID: event.Unsigned.TransactionID, TxnID: event.Unsigned.TransactionID,
MsgState: event.Unsigned.OutgoingState, Relation: *event.Content.GetRelatesTo(),
MsgIsHighlight: false, State: event.Unsigned.OutgoingState,
MsgIsService: false, IsHighlight: false,
MsgSource: event.Content.VeryRaw, IsService: false,
Source: event.Content.VeryRaw,
Renderer: renderer,
} }
} }
@ -76,44 +92,38 @@ func unixToTime(unix int64) time.Time {
return timestamp return timestamp
} }
func (msg *BaseMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
// Sender gets the string that should be displayed as the sender of this message. // 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 the message is being sent, the sender is "Sending...".
// If sending has failed, the sender is "Error". // If sending has failed, the sender is "Error".
// If the message is an emote, the sender is blank. // 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. // In any other case, the sender is the display name of the user who sent the message.
func (msg *BaseMessage) Sender() string { func (msg *UIMessage) Sender() string {
switch msg.MsgState { switch msg.State {
case mautrix.EventStateLocalEcho: case mautrix.EventStateLocalEcho:
return "Sending..." return "Sending..."
case mautrix.EventStateSendFail: case mautrix.EventStateSendFail:
return "Error" return "Error"
} }
switch msg.MsgType { switch msg.Type {
case "m.emote": case "m.emote":
// Emotes don't show a separate sender, it's included in the buffer. // Emotes don't show a separate sender, it's included in the buffer.
return "" return ""
default: default:
return msg.MsgSender return msg.SenderName
} }
} }
func (msg *BaseMessage) SenderID() string { func (msg *UIMessage) NotificationSenderName() string {
return msg.MsgSenderID return msg.SenderName
} }
func (msg *BaseMessage) RealSender() string { func (msg *UIMessage) NotificationContent() string {
return msg.MsgSender return msg.Renderer.NotificationContent()
} }
func (msg *BaseMessage) NotificationSenderName() string { func (msg *UIMessage) getStateSpecificColor() tcell.Color {
return msg.MsgSender switch msg.State {
}
func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
switch msg.MsgState {
case mautrix.EventStateLocalEcho: case mautrix.EventStateLocalEcho:
return tcell.ColorGray return tcell.ColorGray
case mautrix.EventStateSendFail: 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. // 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) // 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() stateColor := msg.getStateSpecificColor()
switch { switch {
case stateColor != tcell.ColorDefault: case stateColor != tcell.ColorDefault:
return stateColor return stateColor
case msg.MsgType == "m.room.member": case msg.Type == "m.room.member":
return widget.GetHashColor(msg.MsgSender) return widget.GetHashColor(msg.SenderName)
case msg.MsgIsService: case msg.IsService:
return tcell.ColorGray return tcell.ColorGray
default: default:
return msg.MsgSenderColor return msg.DefaultSenderColor
} }
} }
// TextColor returns the color the actual content of the message should be shown in. // 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() stateColor := msg.getStateSpecificColor()
switch { switch {
case stateColor != tcell.ColorDefault: case stateColor != tcell.ColorDefault:
return stateColor return stateColor
case msg.MsgIsService, msg.MsgType == "m.notice": case msg.IsService, msg.Type == "m.notice":
return tcell.ColorGray return tcell.ColorGray
case msg.MsgIsHighlight: case msg.IsHighlight:
return tcell.ColorYellow return tcell.ColorYellow
case msg.MsgType == "m.room.member": case msg.Type == "m.room.member":
return tcell.ColorGreen return tcell.ColorGreen
default: default:
return tcell.ColorDefault return tcell.ColorDefault
@ -169,14 +179,14 @@ func (msg *BaseMessage) TextColor() tcell.Color {
// gray and red respectively. // gray and red respectively.
// //
// However, other messages are the default color instead of a color stored in the struct. // However, other messages are the default color instead of a color stored in the struct.
func (msg *BaseMessage) TimestampColor() tcell.Color { func (msg *UIMessage) TimestampColor() tcell.Color {
if msg.MsgIsService { if msg.IsService {
return tcell.ColorGray return tcell.ColorGray
} }
return msg.getStateSpecificColor() return msg.getStateSpecificColor()
} }
func (msg *BaseMessage) ReplyHeight() int { func (msg *UIMessage) ReplyHeight() int {
if msg.ReplyTo != nil { if msg.ReplyTo != nil {
return 1 + msg.ReplyTo.Height() 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()). // Height returns the number of rows in the computed buffer (see Buffer()).
func (msg *BaseMessage) Height() int { func (msg *UIMessage) Height() int {
return msg.ReplyHeight() + len(msg.buffer) return msg.ReplyHeight() + msg.Renderer.Height()
} }
// Timestamp returns the full timestamp when the message was sent. func (msg *UIMessage) Time() time.Time {
func (msg *BaseMessage) Timestamp() time.Time { return msg.Timestamp
return msg.MsgTimestamp
} }
// FormatTime returns the formatted time when the message was sent. // FormatTime returns the formatted time when the message was sent.
func (msg *BaseMessage) FormatTime() string { func (msg *UIMessage) FormatTime() string {
return msg.MsgTimestamp.Format(TimeFormat) return msg.Timestamp.Format(TimeFormat)
} }
// FormatDate returns the formatted date when the message was sent. // FormatDate returns the formatted date when the message was sent.
func (msg *BaseMessage) FormatDate() string { func (msg *UIMessage) FormatDate() string {
return msg.MsgTimestamp.Format(DateFormat) return msg.Timestamp.Format(DateFormat)
} }
func (msg *BaseMessage) SameDate(message UIMessage) bool { func (msg *UIMessage) SameDate(message *UIMessage) bool {
year1, month1, day1 := msg.Timestamp().Date() year1, month1, day1 := msg.Timestamp.Date()
year2, month2, day2 := message.Timestamp().Date() year2, month2, day2 := message.Timestamp.Date()
return day1 == day2 && month1 == month2 && year1 == year2 return day1 == day2 && month1 == month2 && year1 == year2
} }
func (msg *BaseMessage) ID() string { func (msg *UIMessage) ID() string {
if len(msg.MsgID) == 0 { if len(msg.EventID) == 0 {
return msg.MsgTxnID return msg.TxnID
} }
return msg.MsgID return msg.EventID
} }
func (msg *BaseMessage) SetID(id string) { func (msg *UIMessage) SetID(id string) {
msg.MsgID = id msg.EventID = id
} }
func (msg *BaseMessage) TxnID() string { func (msg *UIMessage) SetIsHighlight(isHighlight bool) {
return msg.MsgTxnID // TODO Textmessage cache needs to be cleared
msg.IsHighlight = isHighlight
} }
func (msg *BaseMessage) Type() mautrix.MessageType { func (msg *UIMessage) Draw(screen mauview.Screen) {
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) {
screen = msg.DrawReply(screen) screen = msg.DrawReply(screen)
for y, line := range msg.buffer { msg.Renderer.Draw(screen)
line.Draw(screen, 0, y)
}
} }
func (msg *BaseMessage) clone() BaseMessage { func (msg *UIMessage) Clone() *UIMessage {
clone := *msg clone := *msg
clone.buffer = nil clone.ReplyTo = nil
return clone 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 { if msg.ReplyTo == nil {
return return
} }
msg.ReplyTo.CalculateBuffer(preferences, width-1) 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 { if msg.ReplyTo == nil {
return screen return screen
} }
width, height := screen.Size() width, height := screen.Size()
replyHeight := msg.ReplyTo.Height() replyHeight := msg.ReplyTo.Height()
widget.WriteLineSimpleColor(screen, "In reply to", 1, 0, tcell.ColorGreen) 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++ { for y := 0; y < 1+replyHeight; y++ {
screen.SetCell(0, y, tcell.StyleDefault, '▊') 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) return mauview.NewProxyScreen(screen, 0, replyHeight+1, width, height-replyHeight-1)
} }
func (msg *BaseMessage) String() string { func (msg *UIMessage) String() string {
return fmt.Sprintf(`&messages.BaseMessage{ return fmt.Sprintf(`&messages.UIMessage{
ID="%s", TxnID="%s", ID="%s", TxnID="%s",
Type="%s", Timestamp=%s, Type="%s", Timestamp=%s,
Sender={ID="%s", Name="%s", Color=#%X}, Sender={ID="%s", Name="%s", Color=#%X},
IsService=%t, IsHighlight=%t, IsService=%t, IsHighlight=%t,
Renderer=%s,
}`, }`,
msg.MsgID, msg.MsgTxnID, msg.EventID, msg.TxnID,
msg.MsgType, msg.MsgTimestamp.String(), msg.Type, msg.Timestamp.String(),
msg.MsgSenderID, msg.MsgSender, msg.MsgSenderColor.Hex(), msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(),
msg.MsgIsService, msg.MsgIsHighlight, msg.IsService, msg.IsHighlight, msg.Renderer.String(),
) )
} }
func (msg *UIMessage) PlainText() string {
return msg.Renderer.PlainText()
}

View File

@ -17,9 +17,12 @@
package messages package messages
import ( import (
"fmt"
"time" "time"
ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -27,55 +30,63 @@ import (
) )
type ExpandedTextMessage struct { type ExpandedTextMessage struct {
BaseMessage Text tstring.TString
MsgText tstring.TString buffer []tstring.TString
} }
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. // 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 { func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) *UIMessage {
return &ExpandedTextMessage{ return newUIMessage(event, displayname, &ExpandedTextMessage{
BaseMessage: newBaseMessage(event, displayname), Text: text,
MsgText: text, })
}
} }
func NewDateChangeMessage(text string) UIMessage { func NewDateChangeMessage(text string) *UIMessage {
midnight := time.Now() midnight := time.Now()
midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(), midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(),
0, 0, 0, 0, 0, 0, 0, 0,
midnight.Location()) midnight.Location())
return &ExpandedTextMessage{ return &UIMessage{
BaseMessage: BaseMessage{ SenderID: "*",
MsgSenderID: "*", SenderName: "*",
MsgSender: "*", Timestamp: midnight,
MsgTimestamp: midnight, IsService: true,
MsgIsService: true, Renderer: &ExpandedTextMessage{
Text: tstring.NewColorTString(text, tcell.ColorGreen),
}, },
MsgText: tstring.NewColorTString(text, tcell.ColorGreen),
} }
} }
func (msg *ExpandedTextMessage) Clone() MessageRenderer {
func (msg *ExpandedTextMessage) Clone() UIMessage {
return &ExpandedTextMessage{ return &ExpandedTextMessage{
BaseMessage: msg.BaseMessage.clone(), Text: msg.Text.Clone(),
MsgText: msg.MsgText.Clone(),
} }
} }
func (msg *ExpandedTextMessage) GenerateText() tstring.TString {
return msg.MsgText
}
func (msg *ExpandedTextMessage) NotificationContent() string { func (msg *ExpandedTextMessage) NotificationContent() string {
return msg.MsgText.String() return msg.Text.String()
} }
func (msg *ExpandedTextMessage) PlainText() string { func (msg *ExpandedTextMessage) PlainText() string {
return msg.MsgText.String() return msg.Text.String()
} }
func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { func (msg *ExpandedTextMessage) String() string {
msg.CalculateReplyBuffer(prefs, width) return fmt.Sprintf(`&messages.ExpandedTextMessage{Text="%s"}`, msg.Text.String())
msg.calculateBufferWithText(prefs, msg.MsgText, width)
} }
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 package messages
import ( import (
"fmt" ifc "maunium.net/go/gomuks/interface"
"strings"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -29,30 +27,27 @@ import (
) )
type HTMLMessage struct { type HTMLMessage struct {
BaseMessage
Root html.Entity Root html.Entity
FocusedBg tcell.Color FocusedBg tcell.Color
focused bool focused bool
} }
func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) UIMessage { func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) *UIMessage {
return &HTMLMessage{ return newUIMessage(event, displayname, &HTMLMessage{
BaseMessage: newBaseMessage(event, displayname), Root: root,
Root: root, })
}
} }
func (hw *HTMLMessage) Clone() UIMessage { func (hw *HTMLMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
func (hw *HTMLMessage) Clone() MessageRenderer {
return &HTMLMessage{ return &HTMLMessage{
BaseMessage: hw.BaseMessage.clone(), Root: hw.Root.Clone(),
Root: hw.Root.Clone(), FocusedBg: hw.FocusedBg,
FocusedBg: hw.FocusedBg,
} }
} }
func (hw *HTMLMessage) Draw(screen mauview.Screen) { func (hw *HTMLMessage) Draw(screen mauview.Screen) {
screen = hw.DrawReply(screen)
if hw.focused { if hw.focused {
screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg)) screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg))
} }
@ -80,18 +75,17 @@ func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool {
return false 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 { if width < 2 {
return return
} }
hw.CalculateReplyBuffer(preferences, width)
// TODO account for bare messages in initial startX // TODO account for bare messages in initial startX
startX := 0 startX := 0
hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView) hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView)
} }
func (hw *HTMLMessage) Height() int { func (hw *HTMLMessage) Height() int {
return hw.ReplyHeight() + hw.Root.Height() return hw.Root.Height()
} }
func (hw *HTMLMessage) PlainText() string { func (hw *HTMLMessage) PlainText() string {
@ -103,8 +97,5 @@ func (hw *HTMLMessage) NotificationContent() string {
} }
func (hw *HTMLMessage) String() string { func (hw *HTMLMessage) String() string {
return fmt.Sprintf("&messages.HTMLMessage{\n" + return hw.Root.String()
" Base=%s,\n" +
" Root=||\n%s\n" +
"}", strings.Replace(hw.BaseMessage.String(), "\n", "\n ", -1), hw.Root.String())
} }

View File

@ -22,6 +22,7 @@ import (
"image/color" "image/color"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
@ -32,32 +33,30 @@ import (
) )
type ImageMessage struct { type ImageMessage struct {
BaseMessage
Body string Body string
Homeserver string Homeserver string
FileID string FileID string
data []byte data []byte
buffer []tstring.TString
matrix ifc.MatrixContainer matrix ifc.MatrixContainer
} }
// NewImageMessage creates a new ImageMessage object with the provided values and the default state. // 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 { func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) *UIMessage {
return &ImageMessage{ return newUIMessage(event, displayname, &ImageMessage{
newBaseMessage(event, displayname), Body: body,
body, Homeserver: homeserver,
homeserver, FileID: fileID,
fileID, data: data,
data, matrix: matrix,
matrix, })
}
} }
func (msg *ImageMessage) Clone() UIMessage { func (msg *ImageMessage) Clone() MessageRenderer {
data := make([]byte, len(msg.data)) data := make([]byte, len(msg.data))
copy(data, msg.data) copy(data, msg.data)
return &ImageMessage{ return &ImageMessage{
BaseMessage: msg.BaseMessage.clone(),
Body: msg.Body, Body: msg.Body,
Homeserver: msg.Homeserver, Homeserver: msg.Homeserver,
FileID: msg.FileID, FileID: msg.FileID,
@ -70,7 +69,7 @@ func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) {
msg.matrix = matrix msg.matrix = matrix
if len(msg.data) == 0 { 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)) 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() { func (msg *ImageMessage) updateData() {
defer debug.Recover() defer debug.Recover()
debug.Print("Loading image:", msg.Homeserver, msg.FileID) 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 // 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 // of the text of this message split into lines at most as wide as the width
// parameter. // parameter.
func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int) { func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
if width < 2 { if width < 2 {
return return
} }
msg.CalculateReplyBuffer(prefs, width)
if prefs.BareMessageView || prefs.DisableImages { if prefs.BareMessageView || prefs.DisableImages {
msg.calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width) msg.buffer = calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width, uiMsg)
return return
} }
@ -121,3 +123,13 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int
msg.buffer = image.Render() 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" "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 roomView := mainView.GetRoom(roomID); roomView != nil {
if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != 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 return replyToMsg
} }
} }
@ -42,24 +42,17 @@ func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage {
return nil 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) msg := directParseEvent(matrix, room, evt)
if msg == nil { if msg == nil {
return nil return nil
} }
if len(evt.Content.GetReplyTo()) > 0 { if len(evt.Content.GetReplyTo()) > 0 {
replyToRoom := room if replyToMsg := getCachedEvent(mainView, room.ID, evt.Content.GetReplyTo()); replyToMsg != nil {
if len(evt.Content.RelatesTo.InReplyTo.RoomID) > 0 { msg.ReplyTo = replyToMsg.Clone()
replyToRoom = matrix.GetRoom(evt.Content.RelatesTo.InReplyTo.RoomID) } else if replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()); replyToEvt != nil {
} if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil {
msg.ReplyTo = replyToMsg
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)
} else { } else {
// TODO add unrenderable reply header // TODO add unrenderable reply header
} }
@ -70,15 +63,22 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R
return msg 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 { switch evt.Type {
case mautrix.EventSticker: case mautrix.EventSticker:
evt.Content.MsgType = mautrix.MsgImage evt.Content.MsgType = mautrix.MsgImage
fallthrough fallthrough
case mautrix.EventMessage: 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: case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias:
return ParseStateEvent(matrix, room, evt) return ParseStateEvent(evt, displayname)
case mautrix.StateMember: case mautrix.StateMember:
return ParseMembershipEvent(room, evt) return ParseMembershipEvent(room, evt)
} }
@ -86,12 +86,7 @@ func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix
return nil return nil
} }
func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { func ParseStateEvent(evt *mautrix.Event, displayname string) *UIMessage {
displayname := evt.Sender
member := room.GetMember(evt.Sender)
if member != nil {
displayname = member.Displayname
}
text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender)) text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender))
switch evt.Type { switch evt.Type {
case mautrix.StateTopic: case mautrix.StateTopic:
@ -124,15 +119,13 @@ func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.
return NewExpandedTextMessage(evt, displayname, text) return NewExpandedTextMessage(evt, displayname, text)
} }
func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage { func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event, displayname string) *UIMessage {
displayname := evt.Sender
member := room.GetMember(evt.Sender)
if member != nil {
displayname = member.Displayname
}
if len(evt.Content.GetReplyTo()) > 0 { if len(evt.Content.GetReplyTo()) > 0 {
evt.Content.RemoveReplyFallback() evt.Content.RemoveReplyFallback()
} }
if evt.Content.GetRelatesTo().Type == mautrix.RelReplace && evt.Content.NewContent != nil {
evt.Content = *evt.Content.NewContent
}
switch evt.Content.MsgType { switch evt.Content.MsgType {
case "m.text", "m.notice", "m.emote": case "m.text", "m.notice", "m.emote":
if evt.Content.Format == mautrix.FormatHTML { if evt.Content.Format == mautrix.FormatHTML {
@ -224,7 +217,7 @@ func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender str
return return
} }
func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) UIMessage { func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) *UIMessage {
displayname, text := getMembershipEventContent(room, evt) displayname, text := getMembershipEventContent(room, evt)
if len(text) == 0 { if len(text) == 0 {
return nil 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 // 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 // of the text of this message split into lines at most as wide as the width
// parameter. // 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 { if width < 2 {
return return nil
} }
msg.buffer = []tstring.TString{} var buffer []tstring.TString
if prefs.BareMessageView { if prefs.BareMessageView {
newText := tstring.NewTString(msg.FormatTime()) newText := tstring.NewTString(msg.FormatTime())
@ -74,7 +74,7 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
newlines := 0 newlines := 0
for _, str := range forcedLinebreaks { for _, str := range forcedLinebreaks {
if len(str) == 0 && newlines < 1 { if len(str) == 0 && newlines < 1 {
msg.buffer = append(msg.buffer, tstring.TString{}) buffer = append(buffer, tstring.TString{})
newlines++ newlines++
} else { } else {
newlines = 0 newlines = 0
@ -88,8 +88,9 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
} }
extract = matchBoundaryPattern(prefs.BareMessageView, extract) extract = matchBoundaryPattern(prefs.BareMessageView, extract)
} }
msg.buffer = append(msg.buffer, extract) buffer = append(buffer, extract)
str = str[len(extract):] str = str[len(extract):]
} }
} }
return buffer
} }

View File

@ -20,72 +20,82 @@ import (
"fmt" "fmt"
"time" "time"
ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/ui/messages/tstring" "maunium.net/go/gomuks/ui/messages/tstring"
) )
type TextMessage struct { type TextMessage struct {
BaseMessage cache tstring.TString
cache tstring.TString buffer []tstring.TString
MsgText string Text string
} }
// NewTextMessage creates a new UITextMessage object with the provided values and the default state. // NewTextMessage creates a new UITextMessage object with the provided values and the default state.
func NewTextMessage(event *mautrix.Event, displayname string, text string) UIMessage { func NewTextMessage(event *mautrix.Event, displayname string, text string) *UIMessage {
return &TextMessage{ return newUIMessage(event, displayname, &TextMessage{
BaseMessage: newBaseMessage(event, displayname), Text: text,
MsgText: text, })
}
} }
func NewServiceMessage(text string) UIMessage { func NewServiceMessage(text string) *UIMessage {
return &TextMessage{ return &UIMessage{
BaseMessage: BaseMessage{ SenderID: "*",
MsgSenderID: "*", SenderName: "*",
MsgSender: "*", Timestamp: time.Now(),
MsgTimestamp: time.Now(), IsService: true,
MsgIsService: true, Renderer: &TextMessage{
Text: text,
}, },
MsgText: text,
} }
} }
func (msg *TextMessage) Clone() UIMessage { func (msg *TextMessage) Clone() MessageRenderer {
return &TextMessage{ return &TextMessage{
BaseMessage: msg.BaseMessage.clone(), Text: msg.Text,
MsgText: msg.MsgText,
} }
} }
func (msg *TextMessage) getCache() tstring.TString { func (msg *TextMessage) getCache(uiMsg *UIMessage) tstring.TString {
if msg.cache == nil { if msg.cache == nil {
switch msg.MsgType { switch uiMsg.Type {
case "m.emote": case "m.emote":
msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor()) msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", uiMsg.SenderName, msg.Text), uiMsg.TextColor())
msg.cache.Colorize(0, len(msg.MsgSender)+2, msg.SenderColor()) msg.cache.Colorize(0, len(uiMsg.SenderName)+2, uiMsg.SenderColor())
default: default:
msg.cache = tstring.NewColorTString(msg.MsgText, msg.TextColor()) msg.cache = tstring.NewColorTString(msg.Text, uiMsg.TextColor())
} }
} }
return msg.cache return msg.cache
} }
func (msg *TextMessage) SetIsHighlight(isHighlight bool) {
msg.BaseMessage.SetIsHighlight(isHighlight)
msg.cache = nil
}
func (msg *TextMessage) NotificationContent() string { func (msg *TextMessage) NotificationContent() string {
return msg.MsgText return msg.Text
} }
func (msg *TextMessage) PlainText() string { func (msg *TextMessage) PlainText() string {
return msg.MsgText return msg.Text
} }
func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int) { func (msg *TextMessage) String() string {
msg.CalculateReplyBuffer(prefs, width) return fmt.Sprintf(`&messages.TextMessage{Text="%s"}`, msg.Text)
msg.calculateBufferWithText(prefs, msg.getCache(), width)
} }
func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
msg.buffer = calculateBufferWithText(prefs, msg.getCache(uiMsg), width, uiMsg)
}
func (msg *TextMessage) Height() int {
return len(msg.buffer)
}
func (msg *TextMessage) Draw(screen mauview.Screen) {
for y, line := range msg.buffer {
line.Draw(screen, 0, y)
}
}
func (msg *TextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}

View File

@ -236,6 +236,10 @@ func (list *RoomList) AddScrollOffset(offset int) {
func (list *RoomList) First() (string, *rooms.Room) { func (list *RoomList) First() (string, *rooms.Room) {
list.RLock() list.RLock()
defer list.RUnlock() defer list.RUnlock()
return list.first()
}
func (list *RoomList) first() (string, *rooms.Room) {
for _, tag := range list.tags { for _, tag := range list.tags {
trl := list.items[tag] trl := list.items[tag]
if trl.HasVisibleRooms() { if trl.HasVisibleRooms() {
@ -248,6 +252,10 @@ func (list *RoomList) First() (string, *rooms.Room) {
func (list *RoomList) Last() (string, *rooms.Room) { func (list *RoomList) Last() (string, *rooms.Room) {
list.RLock() list.RLock()
defer list.RUnlock() defer list.RUnlock()
return list.last()
}
func (list *RoomList) last() (string, *rooms.Room) {
for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- { for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- {
tag := list.tags[tagIndex] tag := list.tags[tagIndex]
trl := list.items[tag] trl := list.items[tag]
@ -273,7 +281,7 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
if len(list.items) == 0 { if len(list.items) == 0 {
return "", nil return "", nil
} else if list.selected == nil { } else if list.selected == nil {
return list.First() return list.first()
} }
trl := list.items[list.selectedTag] trl := list.items[list.selectedTag]
@ -295,11 +303,11 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
return prevTag, prevTRL.LastVisible() return prevTag, prevTRL.LastVisible()
} }
} }
return list.Last() return list.last()
} else if index >= 0 { } else if index >= 0 {
return list.selectedTag, trl.Visible()[index+1].Room return list.selectedTag, trl.Visible()[index+1].Room
} }
return list.First() return list.first()
} }
func (list *RoomList) Next() (string, *rooms.Room) { func (list *RoomList) Next() (string, *rooms.Room) {
@ -308,7 +316,7 @@ func (list *RoomList) Next() (string, *rooms.Room) {
if len(list.items) == 0 { if len(list.items) == 0 {
return "", nil return "", nil
} else if list.selected == nil { } else if list.selected == nil {
return list.First() return list.first()
} }
trl := list.items[list.selectedTag] trl := list.items[list.selectedTag]
@ -330,11 +338,11 @@ func (list *RoomList) Next() (string, *rooms.Room) {
return nextTag, nextTRL.FirstVisible() return nextTag, nextTRL.FirstVisible()
} }
} }
return list.First() return list.first()
} else if index > 0 { } else if index > 0 {
return list.selectedTag, trl.Visible()[index-1].Room return list.selectedTag, trl.Visible()[index-1].Room
} }
return list.Last() return list.last()
} }
// NextWithActivity Returns next room with activity. // NextWithActivity Returns next room with activity.
@ -415,8 +423,10 @@ func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool {
switch event.Buttons() { switch event.Buttons() {
case tcell.WheelUp: case tcell.WheelUp:
list.AddScrollOffset(-WheelScrollOffsetDiff) list.AddScrollOffset(-WheelScrollOffsetDiff)
return true
case tcell.WheelDown: case tcell.WheelDown:
list.AddScrollOffset(WheelScrollOffsetDiff) list.AddScrollOffset(WheelScrollOffsetDiff)
return true
case tcell.Button1: case tcell.Button1:
x, y := event.Position() x, y := event.Position()
return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl) 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 { if trl.maxShown < 10 {
trl.maxShown = 10 trl.maxShown = 10
} }
break list.RUnlock()
return true
} }
} }
// Tag footer // Tag footer

View File

@ -57,6 +57,8 @@ type RoomView struct {
ulBorderScreen *mauview.ProxyScreen ulBorderScreen *mauview.ProxyScreen
ulScreen *mauview.ProxyScreen ulScreen *mauview.ProxyScreen
userListLoaded bool
prevScreen mauview.Screen prevScreen mauview.Screen
parent *MainView parent *MainView
@ -91,6 +93,14 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
config: parent.config, config: parent.config,
} }
view.content = NewMessageView(view) 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. view.input.
SetBackgroundColor(tcell.ColorDefault). SetBackgroundColor(tcell.ColorDefault).
@ -99,7 +109,6 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
SetTabCompleteFunc(view.InputTabComplete) SetTabCompleteFunc(view.InputTabComplete)
view.topic. view.topic.
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetBackgroundColor(tcell.ColorDarkGreen) SetBackgroundColor(tcell.ColorDarkGreen)
@ -269,14 +278,20 @@ func (view *RoomView) SetCompletions(completions []string) {
view.completions.time = time.Now() view.completions.time = time.Now()
} }
func (view *RoomView) SetTyping(users []string) { func (view *RoomView) loadTyping() {
for index, user := range users { for index, user := range view.typing {
member := view.Room.GetMember(user) member := view.Room.GetMember(user)
if member != nil { if member != nil {
users[index] = member.Displayname view.typing[index] = member.Displayname
} }
} }
}
func (view *RoomView) SetTyping(users []string) {
view.typing = users view.typing = users
if view.Room.Loaded() {
view.loadTyping()
}
} }
type completion struct { type completion struct {
@ -385,11 +400,11 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) {
text = emoji.Sprint(text) text = emoji.Sprint(text)
} }
evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text) evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text)
msg := view.ParseEvent(evt) msg := view.parseEvent(evt)
view.AddMessage(msg) view.AddMessage(msg)
eventID, err := view.parent.matrix.SendEvent(evt) eventID, err := view.parent.matrix.SendEvent(evt)
if err != nil { if err != nil {
msg.SetState(mautrix.EventStateSendFail) msg.State = mautrix.EventStateSendFail
// Show shorter version if available // Show shorter version if available
if httpErr, ok := err.(mautrix.HTTPError); ok { if httpErr, ok := err.(mautrix.HTTPError); ok {
err = httpErr err = httpErr
@ -401,7 +416,10 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) {
view.parent.parent.Render() view.parent.parent.Render()
} else { } else {
debug.Print("Event ID received:", eventID) 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 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() { func (view *RoomView) UpdateUserList() {
pls := &mautrix.PowerLevels{} pls := &mautrix.PowerLevels{}
if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil { if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil {
pls = plEvent.Content.GetPowerLevels() pls = plEvent.Content.GetPowerLevels()
} }
view.userList.Update(view.Room.GetMembers(), pls) view.userList.Update(view.Room.GetMembers(), pls)
view.userListLoaded = true
} }
func (view *RoomView) AddServiceMessage(text string) { func (view *RoomView) AddServiceMessage(text string) {
@ -429,10 +455,18 @@ func (view *RoomView) AddMessage(message ifc.Message) {
view.content.AddMessage(message, AppendMessage) 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) 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 { func (view *RoomView) GetEvent(eventID string) ifc.Message {
message, ok := view.content.messageIDs[eventID] message, ok := view.content.messageIDs[eventID]
if !ok { if !ok {

View File

@ -74,7 +74,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
view.username.SetText(ui.gmx.Config().UserID) view.username.SetText(ui.gmx.Config().UserID)
view.password.SetMaskCharacter('*') 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.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorDarkCyan)
view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1}) 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" sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/gomuks/ui/messages"
"maunium.net/go/mauview" "maunium.net/go/mauview"
"maunium.net/go/tcell" "maunium.net/go/tcell"
@ -256,6 +257,7 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
if room == nil { if room == nil {
return return
} }
room.Load()
roomView, ok := view.getRoomView(room.ID, lock) roomView, ok := view.getRoomView(room.ID, lock)
if !ok { if !ok {
@ -263,12 +265,15 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
debug.Print(tag, room) debug.Print(tag, room)
return return
} }
roomView.Update()
view.roomView.SetInnerComponent(roomView) view.roomView.SetInnerComponent(roomView)
view.currentRoom = roomView view.currentRoom = roomView
view.MarkRead(roomView) view.MarkRead(roomView)
view.roomList.SetSelected(tag, room) view.roomList.SetSelected(tag, room)
view.parent.Render() 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) go view.LoadHistory(room.ID)
} }
} }
@ -278,12 +283,6 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
roomView := NewRoomView(view, room). roomView := NewRoomView(view, room).
SetInputChangedFunc(view.InputChanged) SetInputChangedFunc(view.InputChanged)
view.rooms[room.ID] = roomView 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 roomView
} }
return nil return nil
@ -292,7 +291,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
func (view *MainView) GetRoom(roomID string) ifc.RoomView { func (view *MainView) GetRoom(roomID string) ifc.RoomView {
room, ok := view.getRoomView(roomID, true) room, ok := view.getRoomView(roomID, true)
if !ok { if !ok {
return view.addRoom(view.matrix.GetRoom(roomID)) return view.addRoom(view.matrix.GetOrCreateRoom(roomID))
} }
return room return room
} }
@ -348,11 +347,11 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView {
return roomView return roomView
} }
func (view *MainView) SetRooms(rooms map[string]*rooms.Room) { func (view *MainView) SetRooms(rooms *rooms.RoomCache) {
view.roomList.Clear() view.roomList.Clear()
view.roomsLock.Lock() view.roomsLock.Lock()
view.rooms = make(map[string]*RoomView) view.rooms = make(map[string]*RoomView)
for _, room := range rooms { for _, room := range rooms.Map {
if room.HasLeft { if room.HasLeft {
continue continue
} }
@ -388,9 +387,14 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo
notification.Send(sender, text, critical, sound) 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) 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 return
} }
// Whether or not the room where the message came is the currently shown room. // 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) 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) { func (view *MainView) LoadHistory(roomID string) {
defer debug.Recover() defer debug.Recover()
roomView, ok := view.getRoomView(roomID, true) roomView, ok := view.getRoomView(roomID, true)