diff --git a/config/config.go b/config/config.go
index f77275f..d2d8ff7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -53,15 +53,19 @@ type Config struct {
AccessToken string `yaml:"access_token"`
HS string `yaml:"homeserver"`
- Dir string `yaml:"-"`
- CacheDir string `yaml:"cache_dir"`
- HistoryPath string `yaml:"history_path"`
- MediaDir string `yaml:"media_dir"`
- StateDir string `yaml:"state_dir"`
+ RoomCacheSize int `yaml:"room_cache_size"`
+ RoomCacheAge int64 `yaml:"room_cache_age"`
+
+ Dir string `yaml:"-"`
+ CacheDir string `yaml:"cache_dir"`
+ HistoryPath string `yaml:"history_path"`
+ RoomListPath string `yaml:"room_list_path"`
+ MediaDir string `yaml:"media_dir"`
+ StateDir string `yaml:"state_dir"`
Preferences UserPreferences `yaml:"-"`
AuthCache AuthCache `yaml:"-"`
- Rooms map[string]*rooms.Room `yaml:"-"`
+ Rooms *rooms.RoomCache `yaml:"-"`
PushRules *pushrules.PushRuleset `yaml:"-"`
nosave bool
@@ -70,36 +74,39 @@ type Config struct {
// NewConfig creates a config that loads data from the given directory.
func NewConfig(configDir, cacheDir string) *Config {
return &Config{
- Dir: configDir,
- CacheDir: cacheDir,
- HistoryPath: filepath.Join(cacheDir, "history.db"),
- StateDir: filepath.Join(cacheDir, "state"),
- MediaDir: filepath.Join(cacheDir, "media"),
+ Dir: configDir,
+ CacheDir: cacheDir,
+ HistoryPath: filepath.Join(cacheDir, "history.db"),
+ RoomListPath: filepath.Join(cacheDir, "rooms.gob.gz"),
+ StateDir: filepath.Join(cacheDir, "state"),
+ MediaDir: filepath.Join(cacheDir, "media"),
- Rooms: make(map[string]*rooms.Room),
+ RoomCacheSize: 32,
+ RoomCacheAge: 1 * 60,
}
}
// Clear clears the session cache and removes all history.
func (config *Config) Clear() {
- os.Remove(config.HistoryPath)
- os.RemoveAll(config.StateDir)
- os.RemoveAll(config.MediaDir)
- os.RemoveAll(config.CacheDir)
+ _ = os.Remove(config.HistoryPath)
+ _ = os.Remove(config.RoomListPath)
+ _ = os.RemoveAll(config.StateDir)
+ _ = os.RemoveAll(config.MediaDir)
+ _ = os.RemoveAll(config.CacheDir)
config.nosave = true
}
func (config *Config) CreateCacheDirs() {
- os.MkdirAll(config.CacheDir, 0700)
- os.MkdirAll(config.StateDir, 0700)
- os.MkdirAll(config.MediaDir, 0700)
+ _ = os.MkdirAll(config.CacheDir, 0700)
+ _ = os.MkdirAll(config.StateDir, 0700)
+ _ = os.MkdirAll(config.MediaDir, 0700)
}
func (config *Config) DeleteSession() {
config.AuthCache.NextBatch = ""
config.AuthCache.InitialSyncDone = false
config.AccessToken = ""
- config.Rooms = make(map[string]*rooms.Room)
+ config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID)
config.PushRules = nil
config.Clear()
@@ -109,10 +116,14 @@ func (config *Config) DeleteSession() {
func (config *Config) LoadAll() {
config.Load()
+ config.Rooms = rooms.NewRoomCache(config.RoomListPath, config.StateDir, config.RoomCacheSize, config.RoomCacheAge, config.GetUserID)
config.LoadAuthCache()
config.LoadPushRules()
config.LoadPreferences()
- config.LoadRooms()
+ err := config.Rooms.LoadList()
+ if err != nil {
+ panic(err)
+ }
}
// Load loads the config from config.yaml in the directory given to the config struct.
@@ -126,7 +137,11 @@ func (config *Config) SaveAll() {
config.SaveAuthCache()
config.SavePushRules()
config.SavePreferences()
- config.SaveRooms()
+ err := config.Rooms.SaveList()
+ if err != nil {
+ panic(err)
+ }
+ config.Rooms.SaveLoadedRooms()
}
// Save saves this config to config.yaml in the directory given to the config struct.
@@ -161,48 +176,13 @@ func (config *Config) SavePushRules() {
config.save("push rules", config.CacheDir, "pushrules.json", &config.PushRules)
}
-func (config *Config) LoadRooms() {
- os.MkdirAll(config.StateDir, 0700)
-
- roomFiles, err := ioutil.ReadDir(config.StateDir)
+func (config *Config) load(name, dir, file string, target interface{}) {
+ err := os.MkdirAll(dir, 0700)
if err != nil {
- debug.Print("Failed to list rooms state caches in", config.StateDir)
+ debug.Print("Failed to create", dir)
panic(err)
}
- for _, roomFile := range roomFiles {
- if roomFile.IsDir() || !strings.HasSuffix(roomFile.Name(), ".gmxstate") {
- continue
- }
- path := filepath.Join(config.StateDir, roomFile.Name())
- room := &rooms.Room{}
- err = room.Load(path)
- if err != nil {
- debug.Printf("Failed to load room state cache from %s: %v", path, err)
- continue
- }
- config.Rooms[room.ID] = room
- }
-}
-
-func (config *Config) SaveRooms() {
- if config.nosave {
- return
- }
-
- os.MkdirAll(config.StateDir, 0700)
- for _, room := range config.Rooms {
- path := config.getRoomCachePath(room)
- err := room.Save(path)
- if err != nil {
- debug.Printf("Failed to save room state cache to file %s: %v", path, err)
- }
- }
-}
-
-func (config *Config) load(name, dir, file string, target interface{}) {
- os.MkdirAll(dir, 0700)
-
path := filepath.Join(dir, file)
data, err := ioutil.ReadFile(path)
if err != nil {
@@ -229,9 +209,12 @@ func (config *Config) save(name, dir, file string, source interface{}) {
return
}
- os.MkdirAll(dir, 0700)
+ err := os.MkdirAll(dir, 0700)
+ if err != nil {
+ debug.Print("Failed to create", dir)
+ panic(err)
+ }
var data []byte
- var err error
if strings.HasSuffix(file, ".yaml") {
data, err = yaml.Marshal(source)
} else {
@@ -272,30 +255,14 @@ func (config *Config) LoadNextBatch(_ string) string {
return config.AuthCache.NextBatch
}
-func (config *Config) GetRoom(roomID string) *rooms.Room {
- room, _ := config.Rooms[roomID]
- if room == nil {
- room = rooms.NewRoom(roomID, config.UserID)
- config.Rooms[room.ID] = room
- }
- return room
-}
-
-func (config *Config) getRoomCachePath(room *rooms.Room) string {
- return filepath.Join(config.StateDir, room.ID+".gmxstate")
-}
-
-func (config *Config) PutRoom(room *rooms.Room) {
- config.Rooms[room.ID] = room
- room.Save(config.getRoomCachePath(room))
-}
-
func (config *Config) SaveRoom(room *mautrix.Room) {
- gmxRoom := config.GetRoom(room.ID)
- gmxRoom.Room = room
- gmxRoom.Save(config.getRoomCachePath(gmxRoom))
+ panic("SaveRoom is not supported")
}
func (config *Config) LoadRoom(roomID string) *mautrix.Room {
- return config.GetRoom(roomID).Room
+ panic("LoadRoom is not supported")
+}
+
+func (config *Config) GetRoom(roomID string) *rooms.Room {
+ return config.Rooms.GetOrCreate(roomID)
}
diff --git a/go.mod b/go.mod
index c413db3..c4cd3e1 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/mattn/go-runewidth v0.0.4
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
+ github.com/pkg/errors v0.8.1
github.com/sasha-s/go-deadlock v0.2.0
github.com/stretchr/testify v1.3.0
go.etcd.io/bbolt v1.3.2
@@ -24,9 +25,15 @@ require (
gopkg.in/russross/blackfriday.v2 v2.0.1
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
gopkg.in/yaml.v2 v2.2.2
- maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc
+ maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac
maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d
maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed
)
replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1
+
+replace (
+ maunium.net/go/mautrix => ../mautrix-go
+ maunium.net/go/mauview => ../../Go/mauview
+ maunium.net/go/tcell => ../../Go/tcell
+)
diff --git a/go.sum b/go.sum
index 87e99d0..016b79e 100644
--- a/go.sum
+++ b/go.sum
@@ -44,6 +44,8 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44 h1:XKCbzPvK4/BbMXoMJOkYP2ANxiAEO0HM1xn6psSbXxY=
@@ -90,6 +92,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc h1:G7Nse6r/XaCu+p7yc/3m/nfFuOFZZ87Hb3AOX4INOEk=
maunium.net/go/mautrix v0.1.0-alpha.3.0.20190606153009-ca5d9535b6cc/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg=
+maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac h1:r0X7mMPcc8eJaCaHdbW9ibfCLe3EruuqZIH2FM8oLIs=
+maunium.net/go/mautrix v0.1.0-alpha.3.0.20190607192515-d505052a02ac/go.mod h1:O+QWJP3H7BZEzIBSrECKpnpRnEKBwaoWVEu/yZwVwxg=
maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d h1:H4wZ4vMVnOh5QFsb4xZtssgpv3DDEkBRzQ8iyEg2fX0=
maunium.net/go/mauview v0.0.0-20190606152754-de9e0a754a5d/go.mod h1:GL+akv58wNFzzX4IKLvryKx0F/AcYKHql35DiBzBc/w=
maunium.net/go/tcell v0.0.0-20190606152714-9a88fc07b3ed h1:sAcUrUZG2LFWBTkTtLKPQvHPHFM5d6huAhr5ZZuxtbQ=
diff --git a/gomuks.go b/gomuks.go
index 7c307b9..5b6ed92 100644
--- a/gomuks.go
+++ b/gomuks.go
@@ -81,13 +81,15 @@ func (gmx *Gomuks) StartAutosave() {
// Stop stops the Matrix syncer, the tview app and the autosave goroutine,
// then saves everything and calls os.Exit(0).
-func (gmx *Gomuks) Stop() {
+func (gmx *Gomuks) Stop(save bool) {
debug.Print("Disconnecting from Matrix...")
gmx.matrix.Stop()
debug.Print("Cleaning up UI...")
gmx.ui.Stop()
gmx.stop <- true
- gmx.Save()
+ if save {
+ gmx.Save()
+ }
os.Exit(0)
}
@@ -102,7 +104,7 @@ func (gmx *Gomuks) Start() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
- gmx.Stop()
+ gmx.Stop(true)
}()
go gmx.StartAutosave()
diff --git a/interface/gomuks.go b/interface/gomuks.go
index f2565fb..5937a6b 100644
--- a/interface/gomuks.go
+++ b/interface/gomuks.go
@@ -27,5 +27,5 @@ type Gomuks interface {
Config() *config.Config
Start()
- Stop()
+ Stop(save bool)
}
diff --git a/interface/matrix.go b/interface/matrix.go
index f312df1..6a1a977 100644
--- a/interface/matrix.go
+++ b/interface/matrix.go
@@ -45,6 +45,7 @@ type MatrixContainer interface {
GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, error)
GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, error)
GetRoom(roomID string) *rooms.Room
+ GetOrCreateRoom(roomID string) *rooms.Room
Download(mxcURL string) ([]byte, string, string, error)
GetDownloadURL(homeserver, fileID string) string
diff --git a/interface/ui.go b/interface/ui.go
index 781f803..48a6007 100644
--- a/interface/ui.go
+++ b/interface/ui.go
@@ -43,14 +43,13 @@ type MainView interface {
GetRoom(roomID string) RoomView
AddRoom(room *rooms.Room)
RemoveRoom(room *rooms.Room)
- SetRooms(rooms map[string]*rooms.Room)
+ SetRooms(rooms *rooms.RoomCache)
UpdateTags(room *rooms.Room)
SetTyping(roomID string, users []string)
NotifyMessage(room *rooms.Room, message Message, should pushrules.PushActionArrayShould)
- InitialSyncDone()
}
type RoomView interface {
@@ -68,13 +67,10 @@ type RoomView interface {
type Message interface {
ID() string
- TxnID() string
- SenderID() string
- Timestamp() time.Time
+ Time() time.Time
NotificationSenderName() string
NotificationContent() string
- SetState(state mautrix.OutgoingEventState)
SetIsHighlight(highlight bool)
SetID(id string)
}
diff --git a/matrix/history.go b/matrix/history.go
index 767cace..7275c15 100644
--- a/matrix/history.go
+++ b/matrix/history.go
@@ -18,6 +18,7 @@ package matrix
import (
"bytes"
+ "compress/gzip"
"encoding/binary"
"encoding/gob"
@@ -28,6 +29,10 @@ import (
"maunium.net/go/mautrix"
)
+func init() {
+ gob.Register(&mautrix.Event{})
+}
+
type HistoryManager struct {
sync.Mutex
@@ -226,13 +231,27 @@ func btoi(b []byte) uint64 {
func marshalEvent(event *mautrix.Event) ([]byte, error) {
var buf bytes.Buffer
- err := gob.NewEncoder(&buf).Encode(event)
- return buf.Bytes(), err
+ enc := gzip.NewWriter(&buf)
+ if err := gob.NewEncoder(enc).Encode(event); err != nil {
+ _ = enc.Close()
+ return nil, err
+ } else if err := enc.Close(); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
}
func unmarshalEvent(data []byte) (*mautrix.Event, error) {
event := &mautrix.Event{}
- return event, gob.NewDecoder(bytes.NewReader(data)).Decode(event)
+ if cmpReader, err := gzip.NewReader(bytes.NewReader(data)); err != nil {
+ return nil, err
+ } else if err := gob.NewDecoder(cmpReader).Decode(event); err != nil {
+ _ = cmpReader.Close()
+ return nil, err
+ } else if err := cmpReader.Close(); err != nil {
+ return nil, err
+ }
+ return event, nil
}
func put(streams, eventIDs *bolt.Bucket, event *mautrix.Event, key uint64) error {
diff --git a/matrix/matrix.go b/matrix/matrix.go
index ef272b0..de99801 100644
--- a/matrix/matrix.go
+++ b/matrix/matrix.go
@@ -204,6 +204,9 @@ func (c *Container) OnLogin() {
debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config)
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage)
+ // Just pass encrypted events as messages, they'll show up with an encryption unsupported message.
+ c.syncer.OnEventType(mautrix.EventEncrypted, c.HandleMessage)
+ c.syncer.OnEventType(mautrix.EventSticker, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage)
@@ -219,7 +222,7 @@ func (c *Container) OnLogin() {
debug.Print("Initial sync done")
c.config.AuthCache.InitialSyncDone = true
c.config.SaveAuthCache()
- c.ui.MainView().InitialSyncDone()
+ c.ui.MainView().SetRooms(c.config.Rooms)
c.ui.Render()
}
c.client.Syncer = c.syncer
@@ -274,7 +277,9 @@ func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) {
return
}
debug.Print("Updated preferences:", orig, "->", c.config.Preferences)
- c.ui.HandleNewPreferences()
+ if c.config.AuthCache.InitialSyncDone {
+ c.ui.HandleNewPreferences()
+ }
}
func (c *Container) SendPreferencesToMatrix() {
@@ -292,6 +297,17 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) {
if source&EventSourceLeave != 0 || source&EventSourceState != 0 {
return
}
+ room := c.GetOrCreateRoom(evt.RoomID)
+
+ 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 {
+ return
+ }
+
mainView := c.ui.MainView()
roomView := mainView.GetRoom(evt.RoomID)
@@ -300,16 +316,11 @@ func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) {
return
}
- err := c.history.Append(roomView.MxRoom(), []*mautrix.Event{evt})
- if err != nil {
- debug.Printf("Failed to add event %s to history: %v", evt.ID, err)
- }
-
// TODO switch to roomView.AddEvent
message := roomView.ParseEvent(evt)
if message != nil {
roomView.AddMessage(message)
- roomView.MxRoom().LastReceivedMessage = message.Timestamp()
+ roomView.MxRoom().LastReceivedMessage = message.Time()
if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
@@ -350,11 +361,16 @@ func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
room := c.GetRoom(evt.RoomID)
switch membership {
case "join":
- c.ui.MainView().AddRoom(room)
+ if c.config.AuthCache.InitialSyncDone {
+ c.ui.MainView().AddRoom(room)
+ }
room.HasLeft = false
case "leave":
- c.ui.MainView().RemoveRoom(room)
+ if c.config.AuthCache.InitialSyncDone {
+ c.ui.MainView().RemoveRoom(room)
+ }
room.HasLeft = true
+ room.Unload()
case "invite":
// TODO handle
debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID)
@@ -399,8 +415,12 @@ func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) {
}
room := c.GetRoom(evt.RoomID)
- room.MarkRead(lastReadEvent)
- c.ui.Render()
+ if room != nil {
+ room.MarkRead(lastReadEvent)
+ if c.config.AuthCache.InitialSyncDone {
+ c.ui.Render()
+ }
+ }
}
func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool {
@@ -428,11 +448,13 @@ func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool
func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) {
directChats := c.parseDirectChatInfo(evt)
- for _, room := range c.config.Rooms {
+ for _, room := range c.config.Rooms.Map {
shouldBeDirect := directChats[room]
if shouldBeDirect != room.IsDirect {
room.IsDirect = shouldBeDirect
- c.ui.MainView().UpdateTags(room)
+ if c.config.AuthCache.InitialSyncDone {
+ c.ui.MainView().UpdateTags(room)
+ }
}
}
}
@@ -466,14 +488,19 @@ func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
}
index++
}
-
- mainView := c.ui.MainView()
room.RawTags = newTags
- mainView.UpdateTags(room)
+
+ if c.config.AuthCache.InitialSyncDone {
+ mainView := c.ui.MainView()
+ mainView.UpdateTags(room)
+ }
}
// HandleTyping is the event handler for the m.typing event.
func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) {
+ if !c.config.AuthCache.InitialSyncDone {
+ return
+ }
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs)
}
@@ -544,7 +571,7 @@ func (c *Container) CreateRoom(req *mautrix.ReqCreateRoom) (*rooms.Room, error)
if err != nil {
return nil, err
}
- room := c.GetRoom(resp.RoomID)
+ room := c.GetOrCreateRoom(resp.RoomID)
return room, nil
}
@@ -557,7 +584,6 @@ func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) {
room := c.GetRoom(resp.RoomID)
room.HasLeft = false
-
return room, nil
}
@@ -568,8 +594,9 @@ func (c *Container) LeaveRoom(roomID string) error {
return err
}
- room := c.GetRoom(roomID)
- room.HasLeft = true
+ node := c.GetOrCreateRoom(roomID)
+ node.HasLeft = true
+ node.Unload()
return nil
}
@@ -593,9 +620,9 @@ func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, e
return nil, err
}
}
- room.PrevBatch = resp.End
- c.config.PutRoom(room)
debug.Printf("Loaded %d events for %s from server from %s to %s", len(resp.Chunk), room.ID, resp.Start, resp.End)
+ room.PrevBatch = resp.End
+ c.config.Rooms.Put(room)
return resp.Chunk, nil
}
@@ -613,9 +640,14 @@ func (c *Container) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event,
return event, nil
}
+// GetOrCreateRoom gets the room instance stored in the session.
+func (c *Container) GetOrCreateRoom(roomID string) *rooms.Room {
+ return c.config.Rooms.GetOrCreate(roomID)
+}
+
// GetRoom gets the room instance stored in the session.
func (c *Container) GetRoom(roomID string) *rooms.Room {
- return c.config.GetRoom(roomID)
+ return c.config.Rooms.Get(roomID)
}
var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)")
@@ -642,7 +674,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
}
diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go
index 717636b..87c3780 100644
--- a/matrix/rooms/room.go
+++ b/matrix/rooms/room.go
@@ -17,6 +17,7 @@
package rooms
import (
+ "compress/gzip"
"encoding/gob"
"fmt"
"os"
@@ -31,17 +32,20 @@ import (
)
func init() {
- gob.Register([]interface{}{})
gob.Register(map[string]interface{}{})
+ gob.Register([]interface{}{})
+ gob.Register(&Room{})
+ gob.Register(0)
}
type RoomNameSource int
const (
- ExplicitRoomName RoomNameSource = iota
- CanonicalAliasRoomName
- AliasRoomName
+ UnknownRoomName RoomNameSource = iota
MemberRoomName
+ AliasRoomName
+ CanonicalAliasRoomName
+ ExplicitRoomName
)
// RoomTag is a tag given to a specific room.
@@ -60,7 +64,8 @@ type UnreadMessage struct {
// Room represents a single Matrix room.
type Room struct {
- *mautrix.Room
+ // The room ID.
+ ID string
// Whether or not the user has left the room.
HasLeft bool
@@ -79,19 +84,22 @@ type Room struct {
// Whether or not this room is marked as a direct chat.
IsDirect bool
- // List of tags given to this room
+ // List of tags given to this room.
RawTags []RoomTag
// Timestamp of previously received actual message.
LastReceivedMessage time.Time
+ // Room state cache.
+ state map[mautrix.EventType]map[string]*mautrix.Event
// MXID -> Member cache calculated from membership events.
memberCache map[string]*mautrix.Member
- // The first non-SessionUserID member in the room. Calculated at
+ // The first two non-SessionUserID members in the room. Calculated at
// the same time as memberCache.
- firstMemberCache *mautrix.Member
+ firstMemberCache *mautrix.Member
+ secondMemberCache *mautrix.Member
// The name of the room. Calculated from the state event name,
// canonical_alias or alias or the member cache.
- nameCache string
+ NameCache string
// The event type from which the name cache was calculated from.
nameCacheSource RoomNameSource
// The topic of the room. Directly fetched from the m.room.topic state event.
@@ -101,31 +109,98 @@ type Room struct {
// The list of aliases. Directly fetched from the m.room.aliases state event.
aliasesCache []string
+ // Path for state store file.
+ path string
+ // Room cache object
+ cache *RoomCache
+ // Lock for state and other room stuff.
lock sync.RWMutex
+
+ // Room state cache linked list.
+ prev *Room
+ next *Room
+ touch int64
}
-func (room *Room) Load(path string) error {
- file, err := os.OpenFile(path, os.O_RDONLY, 0600)
- if err != nil {
- return err
+func debugPrintError(fn func() error, message string) {
+ if err := fn(); err != nil {
+ debug.Printf("%s: %v", message, err)
}
- defer file.Close()
- dec := gob.NewDecoder(file)
+}
+
+func (room *Room) Loaded() bool {
+ return room.state != nil
+}
+
+func (room *Room) Load() {
+ if room.Loaded() {
+ return
+ }
+ room.cache.TouchNode(room)
room.lock.Lock()
- defer room.lock.Unlock()
- return dec.Decode(room)
+ room.load()
+ room.lock.Unlock()
}
-func (room *Room) Save(path string) error {
- file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
- if err != nil {
- return err
+func (room *Room) load() {
+ if room.Loaded() {
+ return
}
- defer file.Close()
- enc := gob.NewEncoder(file)
+ debug.Print("Loading state for room", room.ID)
+ 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)
+ }
+}
+
+func (room *Room) Unload() {
+ debug.Print("Unloading", room.ID)
+ room.Save()
+ room.state = nil
+ room.aliasesCache = nil
+ room.topicCache = ""
+ room.canonicalAliasCache = ""
+ room.firstMemberCache = nil
+ room.secondMemberCache = nil
+}
+
+func (room *Room) Save() {
+ if !room.Loaded() {
+ debug.Print("Failed to save room state: room not loaded")
+ return
+ }
+ debug.Print("Saving state for room", room.ID)
+ file, err := os.OpenFile(room.path, os.O_WRONLY|os.O_CREATE, 0600)
+ if err != nil {
+ debug.Print("Failed to open room state file for writing:", err)
+ return
+ }
+ defer debugPrintError(file.Close, "Failed to close room state file after writing")
+ cmpWriter := gzip.NewWriter(file)
+ defer debugPrintError(cmpWriter.Close, "Failed to close room state gzip writer")
+ enc := gob.NewEncoder(cmpWriter)
room.lock.RLock()
defer room.lock.RUnlock()
- return enc.Encode(room)
+ if err := enc.Encode(&room.state); err != nil {
+ debug.Print("Failed to encode room state:", err)
+ }
}
// MarkRead clears the new message statuses on this room.
@@ -220,30 +295,32 @@ func (room *Room) Tags() []RoomTag {
// UpdateState updates the room's current state with the given Event. This will clobber events based
// on the type/state_key combination.
func (room *Room) UpdateState(event *mautrix.Event) {
+ room.Load()
room.lock.Lock()
defer room.lock.Unlock()
- _, exists := room.State[event.Type]
+ _, exists := room.state[event.Type]
if !exists {
- room.State[event.Type] = make(map[string]*mautrix.Event)
+ room.state[event.Type] = make(map[string]*mautrix.Event)
}
switch event.Type {
case mautrix.StateRoomName:
- room.nameCache = ""
+ room.NameCache = ""
case mautrix.StateCanonicalAlias:
- if room.nameCacheSource >= CanonicalAliasRoomName {
- room.nameCache = ""
+ if room.nameCacheSource <= CanonicalAliasRoomName {
+ room.NameCache = ""
}
room.canonicalAliasCache = ""
case mautrix.StateAliases:
- if room.nameCacheSource >= AliasRoomName {
- room.nameCache = ""
+ if room.nameCacheSource <= AliasRoomName {
+ room.NameCache = ""
}
room.aliasesCache = nil
case mautrix.StateMember:
room.memberCache = nil
room.firstMemberCache = nil
- if room.nameCacheSource >= MemberRoomName {
- room.nameCache = ""
+ room.secondMemberCache = nil
+ if room.nameCacheSource <= MemberRoomName {
+ room.NameCache = ""
}
case mautrix.StateTopic:
room.topicCache = ""
@@ -258,24 +335,25 @@ func (room *Room) UpdateState(event *mautrix.Event) {
}
if event.StateKey == nil {
- room.State[event.Type][""] = event
+ room.state[event.Type][""] = event
} else {
- room.State[event.Type][*event.StateKey] = event
+ room.state[event.Type][*event.StateKey] = event
}
}
// GetStateEvent returns the state event for the given type/state_key combo, or nil.
func (room *Room) GetStateEvent(eventType mautrix.EventType, stateKey string) *mautrix.Event {
+ room.Load()
room.lock.RLock()
defer room.lock.RUnlock()
- stateEventMap, _ := room.State[eventType]
+ stateEventMap, _ := room.state[eventType]
event, _ := stateEventMap[stateKey]
return event
}
// getStateEvents returns the state events for the given type.
func (room *Room) getStateEvents(eventType mautrix.EventType) map[string]*mautrix.Event {
- stateEventMap, _ := room.State[eventType]
+ stateEventMap, _ := room.state[eventType]
return stateEventMap
}
@@ -323,7 +401,7 @@ func (room *Room) GetAliases() []string {
func (room *Room) updateNameFromNameEvent() {
nameEvt := room.GetStateEvent(mautrix.StateRoomName, "")
if nameEvt != nil {
- room.nameCache = nameEvt.Content.Name
+ room.NameCache = nameEvt.Content.Name
}
}
@@ -336,7 +414,7 @@ func (room *Room) updateNameFromAliases() {
aliases := room.GetAliases()
if len(aliases) > 0 {
sort.Sort(sort.StringSlice(aliases))
- room.nameCache = aliases[0]
+ room.NameCache = aliases[0]
}
}
@@ -351,33 +429,40 @@ func (room *Room) updateNameFromAliases() {
func (room *Room) updateNameFromMembers() {
members := room.GetMembers()
if len(members) <= 1 {
- room.nameCache = "Empty room"
+ room.NameCache = "Empty room"
} else if room.firstMemberCache == nil {
- room.nameCache = "Room"
+ room.NameCache = "Room"
} else if len(members) == 2 {
- room.nameCache = room.firstMemberCache.Displayname
+ room.NameCache = room.firstMemberCache.Displayname
+ } else if len(members) == 3 && room.secondMemberCache != nil {
+ room.NameCache = fmt.Sprintf("%s and %s", room.firstMemberCache.Displayname, room.secondMemberCache.Displayname)
} else {
- firstMember := room.firstMemberCache.Displayname
- room.nameCache = fmt.Sprintf("%s and %d others", firstMember, len(members)-2)
+ members := room.firstMemberCache.Displayname
+ count := len(members) - 2
+ if room.secondMemberCache != nil {
+ members += ", " + room.secondMemberCache.Displayname
+ count--
+ }
+ room.NameCache = fmt.Sprintf("%s and %d others", members, count)
}
}
// updateNameCache updates the room display name based on the room state in the order
// specified in spec section 11.2.2.5.
func (room *Room) updateNameCache() {
- if len(room.nameCache) == 0 {
+ if len(room.NameCache) == 0 {
room.updateNameFromNameEvent()
room.nameCacheSource = ExplicitRoomName
}
- if len(room.nameCache) == 0 {
- room.nameCache = room.GetCanonicalAlias()
+ if len(room.NameCache) == 0 {
+ room.NameCache = room.GetCanonicalAlias()
room.nameCacheSource = CanonicalAliasRoomName
}
- if len(room.nameCache) == 0 {
+ if len(room.NameCache) == 0 {
room.updateNameFromAliases()
room.nameCacheSource = AliasRoomName
}
- if len(room.nameCache) == 0 {
+ if len(room.NameCache) == 0 {
room.updateNameFromMembers()
room.nameCacheSource = MemberRoomName
}
@@ -389,15 +474,19 @@ func (room *Room) updateNameCache() {
// If the cache is empty, it is updated first.
func (room *Room) GetTitle() string {
room.updateNameCache()
- return room.nameCache
+ return room.NameCache
}
// createMemberCache caches all member events into a easily processable MXID -> *Member map.
func (room *Room) createMemberCache() map[string]*mautrix.Member {
+ if len(room.memberCache) > 0 {
+ return room.memberCache
+ }
cache := make(map[string]*mautrix.Member)
room.lock.RLock()
events := room.getStateEvents(mautrix.StateMember)
room.firstMemberCache = nil
+ room.secondMemberCache = nil
if events != nil {
for userID, event := range events {
member := &event.Content.Member
@@ -405,8 +494,12 @@ func (room *Room) createMemberCache() map[string]*mautrix.Member {
if len(member.Displayname) == 0 {
member.Displayname = userID
}
- if room.firstMemberCache == nil && userID != room.SessionUserID {
- room.firstMemberCache = member
+ if userID != room.SessionUserID {
+ if room.firstMemberCache == nil {
+ room.firstMemberCache = member
+ } else if room.secondMemberCache == nil {
+ room.secondMemberCache = member
+ }
}
if member.Membership == mautrix.MembershipJoin || member.Membership == mautrix.MembershipInvite {
cache[userID] = member
@@ -425,18 +518,16 @@ func (room *Room) createMemberCache() map[string]*mautrix.Member {
// The members are returned from the cache.
// If the cache is empty, it is updated first.
func (room *Room) GetMembers() map[string]*mautrix.Member {
- if len(room.memberCache) == 0 || room.firstMemberCache == nil {
- room.createMemberCache()
- }
+ room.Load()
+ room.createMemberCache()
return room.memberCache
}
// GetMember returns the member with the given MXID.
// If the member doesn't exist, nil is returned.
func (room *Room) GetMember(userID string) *mautrix.Member {
- if len(room.memberCache) == 0 {
- room.createMemberCache()
- }
+ room.Load()
+ room.createMemberCache()
room.lock.RLock()
member, _ := room.memberCache[userID]
room.lock.RUnlock()
@@ -449,9 +540,13 @@ func (room *Room) GetSessionOwner() string {
}
// NewRoom creates a new Room with the given ID
-func NewRoom(roomID, owner string) *Room {
+func NewRoom(roomID string, cache *RoomCache) *Room {
return &Room{
- Room: mautrix.NewRoom(roomID),
- SessionUserID: owner,
+ ID: roomID,
+ state: make(map[mautrix.EventType]map[string]*mautrix.Event),
+ path: cache.roomPath(roomID),
+ cache: cache,
+
+ SessionUserID: cache.getOwner(),
}
}
diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go
new file mode 100644
index 0000000..03e3ad8
--- /dev/null
+++ b/matrix/rooms/roomcache.go
@@ -0,0 +1,305 @@
+// 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 .
+
+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()
+ 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 && node.Loaded() {
+ cache.touch(node)
+ 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()
+}
+
+func (cache *RoomCache) clean() {
+ origSize := cache.size
+ maxTS := time.Now().Unix() - cache.maxAge
+ for cache.size > cache.maxSize {
+ if cache.tail.touch > maxTS {
+ break
+ }
+ cache.tail.Unload()
+ cache.llPop(cache.tail)
+ }
+ 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)
+ node.Unload()
+}
+
+func (cache *RoomCache) newRoom(roomID string) *Room {
+ node := NewRoom(roomID, cache)
+ cache.Map[node.ID] = node
+ return node
+}
diff --git a/matrix/sync.go b/matrix/sync.go
index 7f9c902..ab0e9f8 100644
--- a/matrix/sync.go
+++ b/matrix/sync.go
@@ -107,8 +107,6 @@ func NewGomuksSyncer(session SyncerSession) *GomuksSyncer {
// ProcessResponse processes a Matrix sync response.
func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) {
debug.Print("Received sync response")
-// dat, _ := json.MarshalIndent(res, "", " ")
-// debug.Print(string(dat))
s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence)
s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData)
@@ -215,6 +213,10 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
Timeline: mautrix.FilterPart{
Types: []string{
"m.room.message",
+ "m.room.encrypted",
+ "m.sticker",
+ "m.reaction",
+
"m.room.member",
"m.room.name",
"m.room.topic",
diff --git a/ui/commands.go b/ui/commands.go
index 5bcba98..cd770b6 100644
--- a/ui/commands.go
+++ b/ui/commands.go
@@ -86,26 +86,26 @@ func cmdHeapProfile(cmd *Command) {
func cmdRainbow(cmd *Command) {
text := strings.Join(cmd.Args, " ")
var html strings.Builder
- fmt.Fprint(&html, "**🌈** ")
+ _, _ = fmt.Fprint(&html, "**🌈** ")
for i, char := range text {
if unicode.IsSpace(char) {
html.WriteRune(char)
continue
}
color := rainbow.GetInterpolatedColorFor(float64(i) / float64(len(text))).Hex()
- fmt.Fprintf(&html, "%c", color, char)
+ _, _ = fmt.Fprintf(&html, "%c", color, char)
}
go cmd.Room.SendMessage("m.text", html.String())
cmd.UI.Render()
}
func cmdQuit(cmd *Command) {
- cmd.Gomuks.Stop()
+ cmd.Gomuks.Stop(true)
}
func cmdClearCache(cmd *Command) {
cmd.Config.Clear()
- cmd.Gomuks.Stop()
+ cmd.Gomuks.Stop(false)
}
func cmdUnknownCommand(cmd *Command) {
diff --git a/ui/message-view.go b/ui/message-view.go
index 75cd022..eb7098c 100644
--- a/ui/message-view.go
+++ b/ui/message-view.go
@@ -25,6 +25,7 @@ import (
"github.com/mattn/go-runewidth"
sync "github.com/sasha-s/go-deadlock"
+ "maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@@ -58,11 +59,13 @@ type MessageView struct {
prevPrefs config.UserPreferences
messageIDLock sync.RWMutex
- messageIDs map[string]messages.UIMessage
+ messageIDs map[string]*messages.UIMessage
messagesLock sync.RWMutex
- messages []messages.UIMessage
+ messages []*messages.UIMessage
msgBufferLock sync.RWMutex
- msgBuffer []messages.UIMessage
+ msgBuffer []*messages.UIMessage
+
+ initialHistoryLoaded bool
}
func NewMessageView(parent *RoomView) *MessageView {
@@ -74,9 +77,9 @@ func NewMessageView(parent *RoomView) *MessageView {
TimestampWidth: len(messages.TimeFormat),
ScrollOffset: 0,
- messages: make([]messages.UIMessage, 0),
- messageIDs: make(map[string]messages.UIMessage),
- msgBuffer: make([]messages.UIMessage, 0),
+ messages: make([]*messages.UIMessage, 0),
+ messageIDs: make(map[string]*messages.UIMessage),
+ msgBuffer: make([]*messages.UIMessage, 0),
_width: 80,
_widestSender: 5,
@@ -108,20 +111,22 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
if ifcMessage == nil {
return
}
- message, ok := ifcMessage.(messages.UIMessage)
- if !ok {
+ message, ok := ifcMessage.(*messages.UIMessage)
+ if !ok || message == nil {
debug.Print("[Warning] Passed non-UIMessage ifc.Message object to AddMessage().")
debug.PrintStack()
return
}
- var oldMsg messages.UIMessage
- if oldMsg = view.getMessageByID(message.ID()); oldMsg != nil {
+ var oldMsg *messages.UIMessage
+ if oldMsg = view.getMessageByID(message.EventID); oldMsg != nil {
view.replaceMessage(oldMsg, message)
direction = IgnoreMessage
- } else if oldMsg = view.getMessageByID(message.TxnID()); oldMsg != nil {
+ } else if oldMsg = view.getMessageByID(message.TxnID); oldMsg != nil {
view.replaceMessage(oldMsg, message)
- view.deleteMessageID(message.TxnID())
+ view.deleteMessageID(message.TxnID)
+ direction = IgnoreMessage
+ } else if oldMsg = view.getMessageByID(message.Relation.EventID); oldMsg != nil {
direction = IgnoreMessage
}
@@ -134,7 +139,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
}
message.CalculateBuffer(view.config.Preferences, width)
- makeDateChange := func() messages.UIMessage {
+ makeDateChange := func() *messages.UIMessage {
dateChange := messages.NewDateChangeMessage(
fmt.Sprintf("Date changed to %s", message.FormatDate()))
dateChange.CalculateBuffer(view.config.Preferences, width)
@@ -157,9 +162,9 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
} else if direction == PrependMessage {
view.messagesLock.Lock()
if len(view.messages) > 0 && !view.messages[0].SameDate(message) {
- view.messages = append([]messages.UIMessage{message, makeDateChange()}, view.messages...)
+ view.messages = append([]*messages.UIMessage{message, makeDateChange()}, view.messages...)
} else {
- view.messages = append([]messages.UIMessage{message}, view.messages...)
+ view.messages = append([]*messages.UIMessage{message}, view.messages...)
}
view.messagesLock.Unlock()
} else if oldMsg != nil {
@@ -174,7 +179,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction MessageDir
}
}
-func (view *MessageView) replaceMessage(original messages.UIMessage, new messages.UIMessage) {
+func (view *MessageView) replaceMessage(original *messages.UIMessage, new *messages.UIMessage) {
if len(new.ID()) > 0 {
view.setMessageID(new)
}
@@ -187,7 +192,10 @@ func (view *MessageView) replaceMessage(original messages.UIMessage, new message
view.messagesLock.Unlock()
}
-func (view *MessageView) getMessageByID(id string) messages.UIMessage {
+func (view *MessageView) getMessageByID(id string) *messages.UIMessage {
+ if id == "" {
+ return nil
+ }
view.messageIDLock.RLock()
defer view.messageIDLock.RUnlock()
msg, ok := view.messageIDs[id]
@@ -198,31 +206,37 @@ func (view *MessageView) getMessageByID(id string) messages.UIMessage {
}
func (view *MessageView) deleteMessageID(id string) {
+ if id == "" {
+ return
+ }
view.messageIDLock.Lock()
delete(view.messageIDs, id)
view.messageIDLock.Unlock()
}
-func (view *MessageView) setMessageID(message messages.UIMessage) {
+func (view *MessageView) setMessageID(message *messages.UIMessage) {
+ if message.ID() == "" {
+ return
+ }
view.messageIDLock.Lock()
view.messageIDs[message.ID()] = message
view.messageIDLock.Unlock()
}
-func (view *MessageView) appendBuffer(message messages.UIMessage) {
+func (view *MessageView) appendBuffer(message *messages.UIMessage) {
view.msgBufferLock.Lock()
view.appendBufferUnlocked(message)
view.msgBufferLock.Unlock()
}
-func (view *MessageView) appendBufferUnlocked(message messages.UIMessage) {
+func (view *MessageView) appendBufferUnlocked(message *messages.UIMessage) {
for i := 0; i < message.Height(); i++ {
view.msgBuffer = append(view.msgBuffer, message)
}
view.prevMsgCount++
}
-func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages.UIMessage) {
+func (view *MessageView) replaceBuffer(original *messages.UIMessage, new *messages.UIMessage) {
start := -1
end := -1
view.msgBufferLock.RLock()
@@ -240,7 +254,7 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
if start == -1 {
debug.Print("Called replaceBuffer() with message that was not in the buffer:", original)
- debug.PrintStack()
+ //debug.PrintStack()
view.appendBuffer(new)
return
}
@@ -280,7 +294,7 @@ func (view *MessageView) recalculateBuffers() {
if !prefs.BareMessageView {
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender() + SenderMessageGap
}
- view.msgBuffer = []messages.UIMessage{}
+ view.msgBuffer = []*messages.UIMessage{}
view.prevMsgCount = 0
for i, message := range view.messages {
if message == nil {
@@ -299,17 +313,17 @@ func (view *MessageView) recalculateBuffers() {
view.prevPrefs = prefs
}
-func (view *MessageView) handleMessageClick(message messages.UIMessage) bool {
- switch message := message.(type) {
+func (view *MessageView) handleMessageClick(message *messages.UIMessage) bool {
+ switch msg := message.Renderer.(type) {
case *messages.ImageMessage:
- open.Open(message.Path())
- case messages.UIMessage:
+ open.Open(msg.Path())
+ default:
debug.Print("Message clicked:", message)
}
return false
}
-func (view *MessageView) handleUsernameClick(message messages.UIMessage, prevMessage messages.UIMessage) bool {
+func (view *MessageView) handleUsernameClick(message *messages.UIMessage, prevMessage *messages.UIMessage) bool {
if prevMessage != nil && prevMessage.Sender() == message.Sender() {
return false
}
@@ -317,7 +331,7 @@ func (view *MessageView) handleUsernameClick(message messages.UIMessage, prevMes
if len(message.Sender()) == 0 {
return false
}
- sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", message.Sender(), message.SenderID())
+ sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", message.Sender(), message.SenderID)
cursorPos := view.parent.input.GetCursorOffset()
text := view.parent.input.GetText()
@@ -363,7 +377,7 @@ func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
view.msgBufferLock.RLock()
message := view.msgBuffer[line]
- var prevMessage messages.UIMessage
+ var prevMessage *messages.UIMessage
if y != 0 && line > 0 {
prevMessage = view.msgBuffer[line-1]
}
@@ -496,7 +510,7 @@ func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX
func (view *MessageView) CapturePlaintext(height int) string {
var buf strings.Builder
indexOffset := view.TotalHeight() - view.ScrollOffset - height
- var prevMessage messages.UIMessage
+ var prevMessage *messages.UIMessage
view.msgBufferLock.RLock()
for line := 0; line < height; line++ {
index := indexOffset + line
@@ -504,14 +518,13 @@ func (view *MessageView) CapturePlaintext(height int) string {
continue
}
- meta := view.msgBuffer[index]
- message, ok := meta.(messages.UIMessage)
- if ok && message != prevMessage {
+ message := view.msgBuffer[index]
+ if message != prevMessage {
var sender string
if len(message.Sender()) > 0 {
sender = fmt.Sprintf(" <%s>", message.Sender())
- } else if message.Type() == "m.emote" {
- sender = fmt.Sprintf(" * %s", message.RealSender())
+ } else if message.Type == mautrix.MsgEmote {
+ sender = fmt.Sprintf(" * %s", message.SenderName)
}
fmt.Fprintf(&buf, "%s%s %s\n", message.FormatTime(), sender, message.PlainText())
prevMessage = message
@@ -561,7 +574,7 @@ func (view *MessageView) Draw(screen mauview.Screen) {
}
}
- var prevMsg messages.UIMessage
+ var prevMsg *messages.UIMessage
view.msgBufferLock.RLock()
for line := viewStart; line < height && indexOffset+line < len(view.msgBuffer); line++ {
index := indexOffset + line
diff --git a/ui/messages/base.go b/ui/messages/base.go
index 05df72b..123c5c1 100644
--- a/ui/messages/base.go
+++ b/ui/messages/base.go
@@ -27,44 +27,60 @@ import (
"maunium.net/go/tcell"
"maunium.net/go/gomuks/interface"
- "maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget"
)
-type BaseMessage struct {
- MsgID string
- MsgTxnID string
- MsgType mautrix.MessageType
- MsgSenderID string
- MsgSender string
- MsgSenderColor tcell.Color
- MsgTimestamp time.Time
- MsgState mautrix.OutgoingEventState
- MsgIsHighlight bool
- MsgIsService bool
- MsgSource json.RawMessage
- ReplyTo UIMessage
- buffer []tstring.TString
+type MessageRenderer interface {
+ Draw(screen mauview.Screen)
+ NotificationContent() string
+ PlainText() string
+ CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage)
+ RegisterMatrix(matrix ifc.MatrixContainer)
+ Height() int
+ Clone() MessageRenderer
+ String() string
}
-func newBaseMessage(event *mautrix.Event, displayname string) BaseMessage {
+type UIMessage struct {
+ EventID string
+ TxnID string
+ Relation mautrix.RelatesTo
+ Type mautrix.MessageType
+ SenderID string
+ SenderName string
+ DefaultSenderColor tcell.Color
+ Timestamp time.Time
+ State mautrix.OutgoingEventState
+ IsHighlight bool
+ IsService bool
+ Source json.RawMessage
+ ReplyTo *UIMessage
+ Renderer MessageRenderer
+}
+
+const DateFormat = "January _2, 2006"
+const TimeFormat = "15:04:05"
+
+func newUIMessage(event *mautrix.Event, displayname string, renderer MessageRenderer) *UIMessage {
msgtype := event.Content.MsgType
if len(msgtype) == 0 {
msgtype = mautrix.MessageType(event.Type.String())
}
- return BaseMessage{
- MsgSenderID: event.Sender,
- MsgSender: displayname,
- MsgTimestamp: unixToTime(event.Timestamp),
- MsgSenderColor: widget.GetHashColor(event.Sender),
- MsgType: msgtype,
- MsgID: event.ID,
- MsgTxnID: event.Unsigned.TransactionID,
- MsgState: event.Unsigned.OutgoingState,
- MsgIsHighlight: false,
- MsgIsService: false,
- MsgSource: event.Content.VeryRaw,
+ return &UIMessage{
+ SenderID: event.Sender,
+ SenderName: displayname,
+ Timestamp: unixToTime(event.Timestamp),
+ DefaultSenderColor: widget.GetHashColor(event.Sender),
+ Type: msgtype,
+ EventID: event.ID,
+ TxnID: event.Unsigned.TransactionID,
+ Relation: *event.Content.GetRelatesTo(),
+ State: event.Unsigned.OutgoingState,
+ IsHighlight: false,
+ IsService: false,
+ Source: event.Content.VeryRaw,
+ Renderer: renderer,
}
}
@@ -76,44 +92,38 @@ func unixToTime(unix int64) time.Time {
return timestamp
}
-func (msg *BaseMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
-
// Sender gets the string that should be displayed as the sender of this message.
//
// If the message is being sent, the sender is "Sending...".
// If sending has failed, the sender is "Error".
// If the message is an emote, the sender is blank.
// In any other case, the sender is the display name of the user who sent the message.
-func (msg *BaseMessage) Sender() string {
- switch msg.MsgState {
+func (msg *UIMessage) Sender() string {
+ switch msg.State {
case mautrix.EventStateLocalEcho:
return "Sending..."
case mautrix.EventStateSendFail:
return "Error"
}
- switch msg.MsgType {
+ switch msg.Type {
case "m.emote":
// Emotes don't show a separate sender, it's included in the buffer.
return ""
default:
- return msg.MsgSender
+ return msg.SenderName
}
}
-func (msg *BaseMessage) SenderID() string {
- return msg.MsgSenderID
+func (msg *UIMessage) NotificationSenderName() string {
+ return msg.SenderName
}
-func (msg *BaseMessage) RealSender() string {
- return msg.MsgSender
+func (msg *UIMessage) NotificationContent() string {
+ return msg.Renderer.NotificationContent()
}
-func (msg *BaseMessage) NotificationSenderName() string {
- return msg.MsgSender
-}
-
-func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
- switch msg.MsgState {
+func (msg *UIMessage) getStateSpecificColor() tcell.Color {
+ switch msg.State {
case mautrix.EventStateLocalEcho:
return tcell.ColorGray
case mautrix.EventStateSendFail:
@@ -132,31 +142,31 @@ func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
//
// In any other case, the color is whatever is specified in the Message struct.
// Usually that means it is the hash-based color of the sender (see ui/widget/color.go)
-func (msg *BaseMessage) SenderColor() tcell.Color {
+func (msg *UIMessage) SenderColor() tcell.Color {
stateColor := msg.getStateSpecificColor()
switch {
case stateColor != tcell.ColorDefault:
return stateColor
- case msg.MsgType == "m.room.member":
- return widget.GetHashColor(msg.MsgSender)
- case msg.MsgIsService:
+ case msg.Type == "m.room.member":
+ return widget.GetHashColor(msg.SenderName)
+ case msg.IsService:
return tcell.ColorGray
default:
- return msg.MsgSenderColor
+ return msg.DefaultSenderColor
}
}
// TextColor returns the color the actual content of the message should be shown in.
-func (msg *BaseMessage) TextColor() tcell.Color {
+func (msg *UIMessage) TextColor() tcell.Color {
stateColor := msg.getStateSpecificColor()
switch {
case stateColor != tcell.ColorDefault:
return stateColor
- case msg.MsgIsService, msg.MsgType == "m.notice":
+ case msg.IsService, msg.Type == "m.notice":
return tcell.ColorGray
- case msg.MsgIsHighlight:
+ case msg.IsHighlight:
return tcell.ColorYellow
- case msg.MsgType == "m.room.member":
+ case msg.Type == "m.room.member":
return tcell.ColorGreen
default:
return tcell.ColorDefault
@@ -169,14 +179,14 @@ func (msg *BaseMessage) TextColor() tcell.Color {
// gray and red respectively.
//
// However, other messages are the default color instead of a color stored in the struct.
-func (msg *BaseMessage) TimestampColor() tcell.Color {
- if msg.MsgIsService {
+func (msg *UIMessage) TimestampColor() tcell.Color {
+ if msg.IsService {
return tcell.ColorGray
}
return msg.getStateSpecificColor()
}
-func (msg *BaseMessage) ReplyHeight() int {
+func (msg *UIMessage) ReplyHeight() int {
if msg.ReplyTo != nil {
return 1 + msg.ReplyTo.Height()
}
@@ -184,102 +194,76 @@ func (msg *BaseMessage) ReplyHeight() int {
}
// Height returns the number of rows in the computed buffer (see Buffer()).
-func (msg *BaseMessage) Height() int {
- return msg.ReplyHeight() + len(msg.buffer)
+func (msg *UIMessage) Height() int {
+ return msg.ReplyHeight() + msg.Renderer.Height()
}
-// Timestamp returns the full timestamp when the message was sent.
-func (msg *BaseMessage) Timestamp() time.Time {
- return msg.MsgTimestamp
+func (msg *UIMessage) Time() time.Time {
+ return msg.Timestamp
}
// FormatTime returns the formatted time when the message was sent.
-func (msg *BaseMessage) FormatTime() string {
- return msg.MsgTimestamp.Format(TimeFormat)
+func (msg *UIMessage) FormatTime() string {
+ return msg.Timestamp.Format(TimeFormat)
}
// FormatDate returns the formatted date when the message was sent.
-func (msg *BaseMessage) FormatDate() string {
- return msg.MsgTimestamp.Format(DateFormat)
+func (msg *UIMessage) FormatDate() string {
+ return msg.Timestamp.Format(DateFormat)
}
-func (msg *BaseMessage) SameDate(message UIMessage) bool {
- year1, month1, day1 := msg.Timestamp().Date()
- year2, month2, day2 := message.Timestamp().Date()
+func (msg *UIMessage) SameDate(message *UIMessage) bool {
+ year1, month1, day1 := msg.Timestamp.Date()
+ year2, month2, day2 := message.Timestamp.Date()
return day1 == day2 && month1 == month2 && year1 == year2
}
-func (msg *BaseMessage) ID() string {
- if len(msg.MsgID) == 0 {
- return msg.MsgTxnID
+func (msg *UIMessage) ID() string {
+ if len(msg.EventID) == 0 {
+ return msg.TxnID
}
- return msg.MsgID
+ return msg.EventID
}
-func (msg *BaseMessage) SetID(id string) {
- msg.MsgID = id
+func (msg *UIMessage) SetID(id string) {
+ msg.EventID = id
}
-func (msg *BaseMessage) TxnID() string {
- return msg.MsgTxnID
+func (msg *UIMessage) SetIsHighlight(isHighlight bool) {
+ // TODO Textmessage cache needs to be cleared
+ msg.IsHighlight = isHighlight
}
-func (msg *BaseMessage) Type() mautrix.MessageType {
- return msg.MsgType
-}
-
-func (msg *BaseMessage) State() mautrix.OutgoingEventState {
- return msg.MsgState
-}
-
-func (msg *BaseMessage) SetState(state mautrix.OutgoingEventState) {
- msg.MsgState = state
-}
-
-func (msg *BaseMessage) IsHighlight() bool {
- return msg.MsgIsHighlight
-}
-
-func (msg *BaseMessage) SetIsHighlight(isHighlight bool) {
- msg.MsgIsHighlight = isHighlight
-}
-
-func (msg *BaseMessage) Source() json.RawMessage {
- return msg.MsgSource
-}
-
-func (msg *BaseMessage) SetReplyTo(event UIMessage) {
- msg.ReplyTo = event
-}
-
-func (msg *BaseMessage) Draw(screen mauview.Screen) {
+func (msg *UIMessage) Draw(screen mauview.Screen) {
screen = msg.DrawReply(screen)
- for y, line := range msg.buffer {
- line.Draw(screen, 0, y)
- }
+ msg.Renderer.Draw(screen)
}
-func (msg *BaseMessage) clone() BaseMessage {
+func (msg *UIMessage) Clone() *UIMessage {
clone := *msg
- clone.buffer = nil
- return clone
+ clone.Renderer = clone.Renderer.Clone()
+ return &clone
}
-func (msg *BaseMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) {
+func (msg *UIMessage) CalculateReplyBuffer(preferences config.UserPreferences, width int) {
if msg.ReplyTo == nil {
return
}
msg.ReplyTo.CalculateBuffer(preferences, width-1)
}
-func (msg *BaseMessage) DrawReply(screen mauview.Screen) mauview.Screen {
+func (msg *UIMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
+ msg.Renderer.CalculateBuffer(preferences, width-1, msg)
+}
+
+func (msg *UIMessage) DrawReply(screen mauview.Screen) mauview.Screen {
if msg.ReplyTo == nil {
return screen
}
width, height := screen.Size()
replyHeight := msg.ReplyTo.Height()
widget.WriteLineSimpleColor(screen, "In reply to", 1, 0, tcell.ColorGreen)
- widget.WriteLineSimpleColor(screen, msg.ReplyTo.RealSender(), 13, 0, msg.ReplyTo.SenderColor())
+ widget.WriteLineSimpleColor(screen, msg.ReplyTo.SenderName, 13, 0, msg.ReplyTo.SenderColor())
for y := 0; y < 1+replyHeight; y++ {
screen.SetCell(0, y, tcell.StyleDefault, 'â–Š')
}
@@ -288,16 +272,21 @@ func (msg *BaseMessage) DrawReply(screen mauview.Screen) mauview.Screen {
return mauview.NewProxyScreen(screen, 0, replyHeight+1, width, height-replyHeight-1)
}
-func (msg *BaseMessage) String() string {
- return fmt.Sprintf(`&messages.BaseMessage{
+func (msg *UIMessage) String() string {
+ return fmt.Sprintf(`&messages.UIMessage{
ID="%s", TxnID="%s",
Type="%s", Timestamp=%s,
Sender={ID="%s", Name="%s", Color=#%X},
IsService=%t, IsHighlight=%t,
+ Renderer=%s,
}`,
- msg.MsgID, msg.MsgTxnID,
- msg.MsgType, msg.MsgTimestamp.String(),
- msg.MsgSenderID, msg.MsgSender, msg.MsgSenderColor.Hex(),
- msg.MsgIsService, msg.MsgIsHighlight,
+ msg.EventID, msg.TxnID,
+ msg.Type, msg.Timestamp.String(),
+ msg.SenderID, msg.SenderName, msg.DefaultSenderColor.Hex(),
+ msg.IsService, msg.IsHighlight, msg.Renderer.String(),
)
}
+
+func (msg *UIMessage) PlainText() string {
+ return msg.Renderer.PlainText()
+}
diff --git a/ui/messages/expandedtextmessage.go b/ui/messages/expandedtextmessage.go
index cf71ba1..c9cbf0c 100644
--- a/ui/messages/expandedtextmessage.go
+++ b/ui/messages/expandedtextmessage.go
@@ -17,9 +17,12 @@
package messages
import (
+ "fmt"
"time"
+ ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
+ "maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
@@ -27,55 +30,63 @@ import (
)
type ExpandedTextMessage struct {
- BaseMessage
- MsgText tstring.TString
+ Text tstring.TString
+ buffer []tstring.TString
}
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
-func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) UIMessage {
- return &ExpandedTextMessage{
- BaseMessage: newBaseMessage(event, displayname),
- MsgText: text,
- }
+func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) *UIMessage {
+ return newUIMessage(event, displayname, &ExpandedTextMessage{
+ Text: text,
+ })
}
-func NewDateChangeMessage(text string) UIMessage {
+func NewDateChangeMessage(text string) *UIMessage {
midnight := time.Now()
midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(),
0, 0, 0, 0,
midnight.Location())
- return &ExpandedTextMessage{
- BaseMessage: BaseMessage{
- MsgSenderID: "*",
- MsgSender: "*",
- MsgTimestamp: midnight,
- MsgIsService: true,
+ return &UIMessage{
+ SenderID: "*",
+ SenderName: "*",
+ Timestamp: midnight,
+ IsService: true,
+ Renderer: &ExpandedTextMessage{
+ Text: tstring.NewColorTString(text, tcell.ColorGreen),
},
- MsgText: tstring.NewColorTString(text, tcell.ColorGreen),
}
}
-
-func (msg *ExpandedTextMessage) Clone() UIMessage {
+func (msg *ExpandedTextMessage) Clone() MessageRenderer {
return &ExpandedTextMessage{
- BaseMessage: msg.BaseMessage.clone(),
- MsgText: msg.MsgText.Clone(),
+ Text: msg.Text.Clone(),
}
}
-func (msg *ExpandedTextMessage) GenerateText() tstring.TString {
- return msg.MsgText
-}
-
func (msg *ExpandedTextMessage) NotificationContent() string {
- return msg.MsgText.String()
+ return msg.Text.String()
}
func (msg *ExpandedTextMessage) PlainText() string {
- return msg.MsgText.String()
+ return msg.Text.String()
}
-func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
- msg.CalculateReplyBuffer(prefs, width)
- msg.calculateBufferWithText(prefs, msg.MsgText, width)
+func (msg *ExpandedTextMessage) String() string {
+ return fmt.Sprintf(`&messages.ExpandedTextMessage{Text="%s"}`, msg.Text.String())
}
+
+func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
+ msg.buffer = calculateBufferWithText(prefs, msg.Text, width, uiMsg)
+}
+
+func (msg *ExpandedTextMessage) Height() int {
+ return len(msg.buffer)
+}
+
+func (msg *ExpandedTextMessage) Draw(screen mauview.Screen) {
+ for y, line := range msg.buffer {
+ line.Draw(screen, 0, y)
+ }
+}
+
+func (msg *ExpandedTextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
diff --git a/ui/messages/htmlmessage.go b/ui/messages/htmlmessage.go
index 30b1588..5b95a82 100644
--- a/ui/messages/htmlmessage.go
+++ b/ui/messages/htmlmessage.go
@@ -17,9 +17,7 @@
package messages
import (
- "fmt"
- "strings"
-
+ ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@@ -29,30 +27,27 @@ import (
)
type HTMLMessage struct {
- BaseMessage
-
Root html.Entity
FocusedBg tcell.Color
focused bool
}
-func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) UIMessage {
- return &HTMLMessage{
- BaseMessage: newBaseMessage(event, displayname),
- Root: root,
- }
+func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) *UIMessage {
+ return newUIMessage(event, displayname, &HTMLMessage{
+ Root: root,
+ })
}
-func (hw *HTMLMessage) Clone() UIMessage {
+func (hw *HTMLMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
+
+func (hw *HTMLMessage) Clone() MessageRenderer {
return &HTMLMessage{
- BaseMessage: hw.BaseMessage.clone(),
- Root: hw.Root.Clone(),
- FocusedBg: hw.FocusedBg,
+ Root: hw.Root.Clone(),
+ FocusedBg: hw.FocusedBg,
}
}
func (hw *HTMLMessage) Draw(screen mauview.Screen) {
- screen = hw.DrawReply(screen)
if hw.focused {
screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg))
}
@@ -80,18 +75,17 @@ func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool {
return false
}
-func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
+func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int, msg *UIMessage) {
if width < 2 {
return
}
- hw.CalculateReplyBuffer(preferences, width)
// TODO account for bare messages in initial startX
startX := 0
hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView)
}
func (hw *HTMLMessage) Height() int {
- return hw.ReplyHeight() + hw.Root.Height()
+ return hw.Root.Height()
}
func (hw *HTMLMessage) PlainText() string {
@@ -103,8 +97,5 @@ func (hw *HTMLMessage) NotificationContent() string {
}
func (hw *HTMLMessage) String() string {
- return fmt.Sprintf("&messages.HTMLMessage{\n" +
- " Base=%s,\n" +
- " Root=||\n%s\n" +
- "}", strings.Replace(hw.BaseMessage.String(), "\n", "\n ", -1), hw.Root.String())
+ return hw.Root.String()
}
diff --git a/ui/messages/imagemessage.go b/ui/messages/imagemessage.go
index 01a6500..6c31327 100644
--- a/ui/messages/imagemessage.go
+++ b/ui/messages/imagemessage.go
@@ -22,6 +22,7 @@ import (
"image/color"
"maunium.net/go/mautrix"
+ "maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
@@ -32,32 +33,30 @@ import (
)
type ImageMessage struct {
- BaseMessage
Body string
Homeserver string
FileID string
data []byte
+ buffer []tstring.TString
matrix ifc.MatrixContainer
}
// NewImageMessage creates a new ImageMessage object with the provided values and the default state.
-func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) UIMessage {
- return &ImageMessage{
- newBaseMessage(event, displayname),
- body,
- homeserver,
- fileID,
- data,
- matrix,
- }
+func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) *UIMessage {
+ return newUIMessage(event, displayname, &ImageMessage{
+ Body: body,
+ Homeserver: homeserver,
+ FileID: fileID,
+ data: data,
+ matrix: matrix,
+ })
}
-func (msg *ImageMessage) Clone() UIMessage {
+func (msg *ImageMessage) Clone() MessageRenderer {
data := make([]byte, len(msg.data))
copy(data, msg.data)
return &ImageMessage{
- BaseMessage: msg.BaseMessage.clone(),
Body: msg.Body,
Homeserver: msg.Homeserver,
FileID: msg.FileID,
@@ -70,7 +69,7 @@ func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) {
msg.matrix = matrix
if len(msg.data) == 0 {
- go msg.updateData()
+ //FIXME go msg.updateData()
}
}
@@ -82,6 +81,10 @@ func (msg *ImageMessage) PlainText() string {
return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.Homeserver, msg.FileID))
}
+func (msg *ImageMessage) String() string {
+ return fmt.Sprintf(`&messages.ImageMessage{Body="%s", Homeserver="%s", FileID="%s"}`, msg.Body, msg.Homeserver, msg.FileID)
+}
+
func (msg *ImageMessage) updateData() {
defer debug.Recover()
debug.Print("Loading image:", msg.Homeserver, msg.FileID)
@@ -101,14 +104,13 @@ func (msg *ImageMessage) Path() string {
// CalculateBuffer generates the internal buffer for this message that consists
// of the text of this message split into lines at most as wide as the width
// parameter.
-func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
+func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
if width < 2 {
return
}
- msg.CalculateReplyBuffer(prefs, width)
if prefs.BareMessageView || prefs.DisableImages {
- msg.calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width)
+ msg.buffer = calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width, uiMsg)
return
}
@@ -121,3 +123,13 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int
msg.buffer = image.Render()
}
+
+func (msg *ImageMessage) Height() int {
+ return len(msg.buffer)
+}
+
+func (msg *ImageMessage) Draw(screen mauview.Screen) {
+ for y, line := range msg.buffer {
+ line.Draw(screen, 0, y)
+ }
+}
diff --git a/ui/messages/message.go b/ui/messages/message.go
deleted file mode 100644
index c990368..0000000
--- a/ui/messages/message.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// gomuks - A terminal Matrix client written in Go.
-// Copyright (C) 2019 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package messages
-
-import (
- "maunium.net/go/gomuks/config"
- "maunium.net/go/gomuks/interface"
- "maunium.net/go/mautrix"
- "maunium.net/go/mauview"
- "maunium.net/go/tcell"
-)
-
-// UIMessage is a wrapper for the content and metadata of a Matrix message intended to be displayed.
-type UIMessage interface {
- ifc.Message
-
- Type() mautrix.MessageType
- Sender() string
- SenderColor() tcell.Color
- TextColor() tcell.Color
- TimestampColor() tcell.Color
- FormatTime() string
- FormatDate() string
- SameDate(message UIMessage) bool
-
- SetReplyTo(message UIMessage)
- CalculateBuffer(preferences config.UserPreferences, width int)
- Draw(screen mauview.Screen)
- Height() int
- PlainText() string
-
- Clone() UIMessage
-
- RealSender() string
- RegisterMatrix(matrix ifc.MatrixContainer)
-}
-
-const DateFormat = "January _2, 2006"
-const TimeFormat = "15:04:05"
diff --git a/ui/messages/parser.go b/ui/messages/parser.go
index 0723257..123f323 100644
--- a/ui/messages/parser.go
+++ b/ui/messages/parser.go
@@ -31,10 +31,10 @@ import (
"maunium.net/go/gomuks/ui/widget"
)
-func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage {
+func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage {
if roomView := mainView.GetRoom(roomID); roomView != nil {
if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil {
- if replyToMsg, ok := replyToIfcMsg.(UIMessage); ok && replyToMsg != nil {
+ if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil {
return replyToMsg
}
}
@@ -42,24 +42,19 @@ func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage {
return nil
}
-func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *mautrix.Event) UIMessage {
+func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.Room, evt *mautrix.Event) *UIMessage {
msg := directParseEvent(matrix, room, evt)
if msg == nil {
return nil
}
if len(evt.Content.GetReplyTo()) > 0 {
- replyToRoom := room
- if len(evt.Content.RelatesTo.InReplyTo.RoomID) > 0 {
- replyToRoom = matrix.GetRoom(evt.Content.RelatesTo.InReplyTo.RoomID)
- }
-
- if replyToMsg := getCachedEvent(mainView, replyToRoom.ID, evt.Content.GetReplyTo()); replyToMsg != nil {
+ if replyToMsg := getCachedEvent(mainView, room.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)
+ replyToMsg.ReplyTo = nil
+ msg.ReplyTo = replyToMsg
+ } else if replyToEvt, _ := matrix.GetEvent(room, evt.Content.GetReplyTo()); replyToEvt != nil {
+ if replyToMsg := directParseEvent(matrix, room, replyToEvt); replyToMsg != nil {
+ msg.ReplyTo = replyToMsg
} else {
// TODO add unrenderable reply header
}
@@ -70,15 +65,22 @@ func ParseEvent(matrix ifc.MatrixContainer, mainView ifc.MainView, room *rooms.R
return msg
}
-func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage {
+func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) *UIMessage {
+ displayname := evt.Sender
+ member := room.GetMember(evt.Sender)
+ if member != nil {
+ displayname = member.Displayname
+ }
switch evt.Type {
case mautrix.EventSticker:
evt.Content.MsgType = mautrix.MsgImage
fallthrough
case mautrix.EventMessage:
- return ParseMessage(matrix, room, evt)
+ return ParseMessage(matrix, room, evt, displayname)
+ case mautrix.EventEncrypted:
+ return NewExpandedTextMessage(evt, displayname, tstring.NewStyleTString("Encrypted messages are not yet supported", tcell.StyleDefault.Italic(true)))
case mautrix.StateTopic, mautrix.StateRoomName, mautrix.StateAliases, mautrix.StateCanonicalAlias:
- return ParseStateEvent(matrix, room, evt)
+ return ParseStateEvent(evt, displayname)
case mautrix.StateMember:
return ParseMembershipEvent(room, evt)
}
@@ -86,12 +88,7 @@ func directParseEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix
return nil
}
-func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage {
- displayname := evt.Sender
- member := room.GetMember(evt.Sender)
- if member != nil {
- displayname = member.Displayname
- }
+func ParseStateEvent(evt *mautrix.Event, displayname string) *UIMessage {
text := tstring.NewColorTString(displayname, widget.GetHashColor(evt.Sender))
switch evt.Type {
case mautrix.StateTopic:
@@ -124,15 +121,13 @@ func ParseStateEvent(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.
return NewExpandedTextMessage(evt, displayname, text)
}
-func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event) UIMessage {
- displayname := evt.Sender
- member := room.GetMember(evt.Sender)
- if member != nil {
- displayname = member.Displayname
- }
+func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Event, displayname string) *UIMessage {
if len(evt.Content.GetReplyTo()) > 0 {
evt.Content.RemoveReplyFallback()
}
+ if evt.Content.GetRelatesTo().Type == mautrix.RelReplace && evt.Content.NewContent != nil {
+ evt.Content = *evt.Content.NewContent
+ }
switch evt.Content.MsgType {
case "m.text", "m.notice", "m.emote":
if evt.Content.Format == mautrix.FormatHTML {
@@ -224,7 +219,7 @@ func getMembershipEventContent(room *rooms.Room, evt *mautrix.Event) (sender str
return
}
-func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) UIMessage {
+func ParseMembershipEvent(room *rooms.Room, evt *mautrix.Event) *UIMessage {
displayname, text := getMembershipEventContent(room, evt)
if len(text) == 0 {
return nil
diff --git a/ui/messages/textbase.go b/ui/messages/textbase.go
index 321d998..0d1cc3b 100644
--- a/ui/messages/textbase.go
+++ b/ui/messages/textbase.go
@@ -52,12 +52,12 @@ func matchBoundaryPattern(bare bool, extract tstring.TString) tstring.TString {
// CalculateBuffer generates the internal buffer for this message that consists
// of the text of this message split into lines at most as wide as the width
// parameter.
-func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int) {
+func calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int, msg *UIMessage) []tstring.TString {
if width < 2 {
- return
+ return nil
}
- msg.buffer = []tstring.TString{}
+ var buffer []tstring.TString
if prefs.BareMessageView {
newText := tstring.NewTString(msg.FormatTime())
@@ -74,7 +74,7 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
newlines := 0
for _, str := range forcedLinebreaks {
if len(str) == 0 && newlines < 1 {
- msg.buffer = append(msg.buffer, tstring.TString{})
+ buffer = append(buffer, tstring.TString{})
newlines++
} else {
newlines = 0
@@ -88,8 +88,9 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
}
extract = matchBoundaryPattern(prefs.BareMessageView, extract)
}
- msg.buffer = append(msg.buffer, extract)
+ buffer = append(buffer, extract)
str = str[len(extract):]
}
}
+ return buffer
}
diff --git a/ui/messages/textmessage.go b/ui/messages/textmessage.go
index f8c4573..9ace201 100644
--- a/ui/messages/textmessage.go
+++ b/ui/messages/textmessage.go
@@ -20,72 +20,82 @@ import (
"fmt"
"time"
+ ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
+ "maunium.net/go/mauview"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/ui/messages/tstring"
)
type TextMessage struct {
- BaseMessage
- cache tstring.TString
- MsgText string
+ cache tstring.TString
+ buffer []tstring.TString
+ Text string
}
// NewTextMessage creates a new UITextMessage object with the provided values and the default state.
-func NewTextMessage(event *mautrix.Event, displayname string, text string) UIMessage {
- return &TextMessage{
- BaseMessage: newBaseMessage(event, displayname),
- MsgText: text,
- }
+func NewTextMessage(event *mautrix.Event, displayname string, text string) *UIMessage {
+ return newUIMessage(event, displayname, &TextMessage{
+ Text: text,
+ })
}
-func NewServiceMessage(text string) UIMessage {
- return &TextMessage{
- BaseMessage: BaseMessage{
- MsgSenderID: "*",
- MsgSender: "*",
- MsgTimestamp: time.Now(),
- MsgIsService: true,
+func NewServiceMessage(text string) *UIMessage {
+ return &UIMessage{
+ SenderID: "*",
+ SenderName: "*",
+ Timestamp: time.Now(),
+ IsService: true,
+ Renderer: &TextMessage{
+ Text: text,
},
- MsgText: text,
}
}
-func (msg *TextMessage) Clone() UIMessage {
+func (msg *TextMessage) Clone() MessageRenderer {
return &TextMessage{
- BaseMessage: msg.BaseMessage.clone(),
- MsgText: msg.MsgText,
+ Text: msg.Text,
}
}
-func (msg *TextMessage) getCache() tstring.TString {
+func (msg *TextMessage) getCache(uiMsg *UIMessage) tstring.TString {
if msg.cache == nil {
- switch msg.MsgType {
+ switch uiMsg.Type {
case "m.emote":
- msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", msg.MsgSender, msg.MsgText), msg.TextColor())
- msg.cache.Colorize(0, len(msg.MsgSender)+2, msg.SenderColor())
+ msg.cache = tstring.NewColorTString(fmt.Sprintf("* %s %s", uiMsg.SenderName, msg.Text), uiMsg.TextColor())
+ msg.cache.Colorize(0, len(uiMsg.SenderName)+2, uiMsg.SenderColor())
default:
- msg.cache = tstring.NewColorTString(msg.MsgText, msg.TextColor())
+ msg.cache = tstring.NewColorTString(msg.Text, uiMsg.TextColor())
}
}
return msg.cache
}
-func (msg *TextMessage) SetIsHighlight(isHighlight bool) {
- msg.BaseMessage.SetIsHighlight(isHighlight)
- msg.cache = nil
-}
-
func (msg *TextMessage) NotificationContent() string {
- return msg.MsgText
+ return msg.Text
}
func (msg *TextMessage) PlainText() string {
- return msg.MsgText
+ return msg.Text
}
-func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
- msg.CalculateReplyBuffer(prefs, width)
- msg.calculateBufferWithText(prefs, msg.getCache(), width)
+func (msg *TextMessage) String() string {
+ return fmt.Sprintf(`&messages.TextMessage{Text="%s"}`, msg.Text)
}
+
+func (msg *TextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
+ msg.buffer = calculateBufferWithText(prefs, msg.getCache(uiMsg), width, uiMsg)
+}
+
+func (msg *TextMessage) Height() int {
+ return len(msg.buffer)
+}
+
+func (msg *TextMessage) Draw(screen mauview.Screen) {
+ for y, line := range msg.buffer {
+ line.Draw(screen, 0, y)
+ }
+}
+
+func (msg *TextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
diff --git a/ui/room-list.go b/ui/room-list.go
index 6b22c8e..f0a9703 100644
--- a/ui/room-list.go
+++ b/ui/room-list.go
@@ -236,6 +236,10 @@ func (list *RoomList) AddScrollOffset(offset int) {
func (list *RoomList) First() (string, *rooms.Room) {
list.RLock()
defer list.RUnlock()
+ return list.first()
+}
+
+func (list *RoomList) first() (string, *rooms.Room) {
for _, tag := range list.tags {
trl := list.items[tag]
if trl.HasVisibleRooms() {
@@ -248,6 +252,10 @@ func (list *RoomList) First() (string, *rooms.Room) {
func (list *RoomList) Last() (string, *rooms.Room) {
list.RLock()
defer list.RUnlock()
+ return list.last()
+}
+
+func (list *RoomList) last() (string, *rooms.Room) {
for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- {
tag := list.tags[tagIndex]
trl := list.items[tag]
@@ -273,7 +281,7 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
if len(list.items) == 0 {
return "", nil
} else if list.selected == nil {
- return list.First()
+ return list.first()
}
trl := list.items[list.selectedTag]
@@ -295,11 +303,11 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
return prevTag, prevTRL.LastVisible()
}
}
- return list.Last()
+ return list.last()
} else if index >= 0 {
return list.selectedTag, trl.Visible()[index+1].Room
}
- return list.First()
+ return list.first()
}
func (list *RoomList) Next() (string, *rooms.Room) {
@@ -308,7 +316,7 @@ func (list *RoomList) Next() (string, *rooms.Room) {
if len(list.items) == 0 {
return "", nil
} else if list.selected == nil {
- return list.First()
+ return list.first()
}
trl := list.items[list.selectedTag]
@@ -330,11 +338,11 @@ func (list *RoomList) Next() (string, *rooms.Room) {
return nextTag, nextTRL.FirstVisible()
}
}
- return list.First()
+ return list.first()
} else if index > 0 {
return list.selectedTag, trl.Visible()[index-1].Room
}
- return list.Last()
+ return list.last()
}
// NextWithActivity Returns next room with activity.
diff --git a/ui/room-view.go b/ui/room-view.go
index a2e1bc1..b37ebff 100644
--- a/ui/room-view.go
+++ b/ui/room-view.go
@@ -57,6 +57,8 @@ type RoomView struct {
ulBorderScreen *mauview.ProxyScreen
ulScreen *mauview.ProxyScreen
+ userListLoaded bool
+
prevScreen mauview.Screen
parent *MainView
@@ -99,7 +101,6 @@ func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
SetTabCompleteFunc(view.InputTabComplete)
view.topic.
- SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
SetTextColor(tcell.ColorWhite).
SetBackgroundColor(tcell.ColorDarkGreen)
@@ -385,11 +386,11 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) {
text = emoji.Sprint(text)
}
evt := view.parent.matrix.PrepareMarkdownMessage(view.Room.ID, msgtype, text)
- msg := view.ParseEvent(evt)
+ msg := view.parseEvent(evt)
view.AddMessage(msg)
eventID, err := view.parent.matrix.SendEvent(evt)
if err != nil {
- msg.SetState(mautrix.EventStateSendFail)
+ msg.State = mautrix.EventStateSendFail
// Show shorter version if available
if httpErr, ok := err.(mautrix.HTTPError); ok {
err = httpErr
@@ -401,7 +402,10 @@ func (view *RoomView) SendMessage(msgtype mautrix.MessageType, text string) {
view.parent.parent.Render()
} else {
debug.Print("Event ID received:", eventID)
- //view.MessageView().UpdateMessageID(msg, eventID)
+ msg.EventID = eventID
+ msg.State = mautrix.EventStateDefault
+ view.MessageView().setMessageID(msg)
+ view.parent.parent.Render()
}
}
@@ -413,12 +417,20 @@ func (view *RoomView) MxRoom() *rooms.Room {
return view.Room
}
+func (view *RoomView) Update() {
+ view.topic.SetText(strings.Replace(view.Room.GetTopic(), "\n", " ", -1))
+ if !view.userListLoaded {
+ view.UpdateUserList()
+ }
+}
+
func (view *RoomView) UpdateUserList() {
pls := &mautrix.PowerLevels{}
if plEvent := view.Room.GetStateEvent(mautrix.StatePowerLevels, ""); plEvent != nil {
pls = plEvent.Content.GetPowerLevels()
}
view.userList.Update(view.Room.GetMembers(), pls)
+ view.userListLoaded = true
}
func (view *RoomView) AddServiceMessage(text string) {
@@ -429,10 +441,18 @@ func (view *RoomView) AddMessage(message ifc.Message) {
view.content.AddMessage(message, AppendMessage)
}
-func (view *RoomView) ParseEvent(evt *mautrix.Event) ifc.Message {
+func (view *RoomView) parseEvent(evt *mautrix.Event) *messages.UIMessage {
return messages.ParseEvent(view.parent.matrix, view.parent, view.Room, evt)
}
+func (view *RoomView) ParseEvent(evt *mautrix.Event) ifc.Message {
+ msg := view.parseEvent(evt)
+ if msg == nil {
+ return nil
+ }
+ return msg
+}
+
func (view *RoomView) GetEvent(eventID string) ifc.Message {
message, ok := view.content.messageIDs[eventID]
if !ok {
diff --git a/ui/view-login.go b/ui/view-login.go
index b929d62..a65a77c 100644
--- a/ui/view-login.go
+++ b/ui/view-login.go
@@ -74,7 +74,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
view.username.SetText(ui.gmx.Config().UserID)
view.password.SetMaskCharacter('*')
- view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorDarkCyan)
+ view.quitButton.SetOnClick(func() { ui.gmx.Stop(true) }).SetBackgroundColor(tcell.ColorDarkCyan)
view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorDarkCyan)
view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1})
diff --git a/ui/view-main.go b/ui/view-main.go
index 48004c1..5d2e1f9 100644
--- a/ui/view-main.go
+++ b/ui/view-main.go
@@ -26,6 +26,7 @@ import (
sync "github.com/sasha-s/go-deadlock"
+ "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@@ -256,6 +257,7 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
if room == nil {
return
}
+ room.Load()
roomView, ok := view.getRoomView(room.ID, lock)
if !ok {
@@ -263,12 +265,15 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
debug.Print(tag, room)
return
}
+ roomView.Update()
view.roomView.SetInnerComponent(roomView)
view.currentRoom = roomView
view.MarkRead(roomView)
view.roomList.SetSelected(tag, room)
view.parent.Render()
- if len(roomView.MessageView().messages) == 0 {
+
+ if msgView := roomView.MessageView(); len(msgView.messages) < 20 && !msgView.initialHistoryLoaded {
+ msgView.initialHistoryLoaded = true
go view.LoadHistory(room.ID)
}
}
@@ -278,12 +283,6 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
roomView := NewRoomView(view, room).
SetInputChangedFunc(view.InputChanged)
view.rooms[room.ID] = roomView
- roomView.UpdateUserList()
-
- // TODO make sure this works
- if len(roomView.MessageView().messages) == 0 {
- go view.LoadHistory(room.ID)
- }
return roomView
}
return nil
@@ -292,7 +291,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
func (view *MainView) GetRoom(roomID string) ifc.RoomView {
room, ok := view.getRoomView(roomID, true)
if !ok {
- return view.addRoom(view.matrix.GetRoom(roomID))
+ return view.addRoom(view.matrix.GetOrCreateRoom(roomID))
}
return room
}
@@ -348,11 +347,11 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView {
return roomView
}
-func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
+func (view *MainView) SetRooms(rooms *rooms.RoomCache) {
view.roomList.Clear()
view.roomsLock.Lock()
view.rooms = make(map[string]*RoomView)
- for _, room := range rooms {
+ for _, room := range rooms.Map {
if room.HasLeft {
continue
}
@@ -390,7 +389,8 @@ func sendNotification(room *rooms.Room, sender, text string, critical, sound boo
func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, should pushrules.PushActionArrayShould) {
view.roomList.Bump(room)
- if message.SenderID() == view.config.UserID {
+ uiMsg, ok := message.(*messages.UIMessage)
+ if ok && uiMsg.SenderID == view.config.UserID {
return
}
// Whether or not the room where the message came is the currently shown room.
@@ -420,16 +420,6 @@ func (view *MainView) NotifyMessage(room *rooms.Room, message ifc.Message, shoul
message.SetIsHighlight(should.Highlight)
}
-func (view *MainView) InitialSyncDone() {
- view.roomList.Clear()
- view.roomsLock.RLock()
- for _, room := range view.rooms {
- view.roomList.Add(room.Room)
- room.UpdateUserList()
- }
- view.roomsLock.RUnlock()
-}
-
func (view *MainView) LoadHistory(roomID string) {
defer debug.Recover()
roomView, ok := view.getRoomView(roomID, true)