Unbreak things

This commit is contained in:
Tulir Asokan 2019-06-15 01:11:51 +03:00
parent a4ac699c93
commit a55ea42d7f
26 changed files with 985 additions and 568 deletions

View File

@ -53,15 +53,19 @@ type Config struct {
AccessToken string `yaml:"access_token"`
HS string `yaml:"homeserver"`
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
@ -73,33 +77,36 @@ func NewConfig(configDir, cacheDir string) *Config {
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)
}

9
go.mod
View File

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

4
go.sum
View File

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

View File

@ -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
if save {
gmx.Save()
}
os.Exit(0)
}
@ -102,7 +104,7 @@ func (gmx *Gomuks) Start() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
gmx.Stop()
gmx.Stop(true)
}()
go gmx.StartAutosave()

View File

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

View File

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

View File

@ -43,14 +43,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)
}

View File

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

View File

@ -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)
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":
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().AddRoom(room)
}
room.HasLeft = false
case "leave":
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)
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,13 +448,15 @@ 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
if c.config.AuthCache.InitialSyncDone {
c.ui.MainView().UpdateTags(room)
}
}
}
}
// HandlePushRules is the event handler for the m.push_rules account data event.
@ -466,14 +488,19 @@ func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
}
index++
}
mainView := c.ui.MainView()
room.RawTags = newTags
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
}

View File

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

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

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

View File

@ -107,8 +107,6 @@ func NewGomuksSyncer(session SyncerSession) *GomuksSyncer {
// ProcessResponse processes a Matrix sync response.
func (s *GomuksSyncer) ProcessResponse(res *mautrix.RespSync, since string) (err error) {
debug.Print("Received sync response")
// dat, _ := json.MarshalIndent(res, "", " ")
// debug.Print(string(dat))
s.processSyncEvents(nil, res.Presence.Events, EventSourcePresence)
s.processSyncEvents(nil, res.AccountData.Events, EventSourceAccountData)
@ -215,6 +213,10 @@ func (s *GomuksSyncer) GetFilterJSON(userID string) json.RawMessage {
Timeline: mautrix.FilterPart{
Types: []string{
"m.room.message",
"m.room.encrypted",
"m.sticker",
"m.reaction",
"m.room.member",
"m.room.name",
"m.room.topic",

View File

@ -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, "<font color=\"%s\">%c</font>", color, char)
_, _ = fmt.Fprintf(&html, "<font color=\"%s\">%c</font>", color, char)
}
go cmd.Room.SendMessage("m.text", html.String())
cmd.UI.Render()
}
func cmdQuit(cmd *Command) {
cmd.Gomuks.Stop()
cmd.Gomuks.Stop(true)
}
func cmdClearCache(cmd *Command) {
cmd.Config.Clear()
cmd.Gomuks.Stop()
cmd.Gomuks.Stop(false)
}
func cmdUnknownCommand(cmd *Command) {

View File

@ -25,6 +25,7 @@ import (
"github.com/mattn/go-runewidth"
sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@ -58,11 +59,13 @@ type MessageView struct {
prevPrefs config.UserPreferences
messageIDLock sync.RWMutex
messageIDs map[string]messages.UIMessage
messageIDs map[string]*messages.UIMessage
messagesLock sync.RWMutex
messages []messages.UIMessage
messages []*messages.UIMessage
msgBufferLock sync.RWMutex
msgBuffer []messages.UIMessage
msgBuffer []*messages.UIMessage
initialHistoryLoaded bool
}
func NewMessageView(parent *RoomView) *MessageView {
@ -74,9 +77,9 @@ func NewMessageView(parent *RoomView) *MessageView {
TimestampWidth: len(messages.TimeFormat),
ScrollOffset: 0,
messages: make([]messages.UIMessage, 0),
messageIDs: make(map[string]messages.UIMessage),
msgBuffer: make([]messages.UIMessage, 0),
messages: make([]*messages.UIMessage, 0),
messageIDs: make(map[string]*messages.UIMessage),
msgBuffer: make([]*messages.UIMessage, 0),
_width: 80,
_widestSender: 5,
@ -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

View File

@ -27,44 +27,60 @@ import (
"maunium.net/go/tcell"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/ui/messages/tstring"
"maunium.net/go/gomuks/ui/widget"
)
type BaseMessage struct {
MsgID string
MsgTxnID string
MsgType mautrix.MessageType
MsgSenderID string
MsgSender string
MsgSenderColor tcell.Color
MsgTimestamp time.Time
MsgState mautrix.OutgoingEventState
MsgIsHighlight bool
MsgIsService bool
MsgSource json.RawMessage
ReplyTo UIMessage
buffer []tstring.TString
type MessageRenderer interface {
Draw(screen mauview.Screen)
NotificationContent() string
PlainText() string
CalculateBuffer(prefs config.UserPreferences, width int, msg *UIMessage)
RegisterMatrix(matrix ifc.MatrixContainer)
Height() int
Clone() MessageRenderer
String() string
}
func newBaseMessage(event *mautrix.Event, displayname string) BaseMessage {
type UIMessage struct {
EventID string
TxnID string
Relation mautrix.RelatesTo
Type mautrix.MessageType
SenderID string
SenderName string
DefaultSenderColor tcell.Color
Timestamp time.Time
State mautrix.OutgoingEventState
IsHighlight bool
IsService bool
Source json.RawMessage
ReplyTo *UIMessage
Renderer MessageRenderer
}
const DateFormat = "January _2, 2006"
const TimeFormat = "15:04:05"
func newUIMessage(event *mautrix.Event, displayname string, renderer MessageRenderer) *UIMessage {
msgtype := event.Content.MsgType
if len(msgtype) == 0 {
msgtype = mautrix.MessageType(event.Type.String())
}
return BaseMessage{
MsgSenderID: event.Sender,
MsgSender: displayname,
MsgTimestamp: unixToTime(event.Timestamp),
MsgSenderColor: widget.GetHashColor(event.Sender),
MsgType: msgtype,
MsgID: event.ID,
MsgTxnID: event.Unsigned.TransactionID,
MsgState: event.Unsigned.OutgoingState,
MsgIsHighlight: false,
MsgIsService: false,
MsgSource: event.Content.VeryRaw,
return &UIMessage{
SenderID: event.Sender,
SenderName: displayname,
Timestamp: unixToTime(event.Timestamp),
DefaultSenderColor: widget.GetHashColor(event.Sender),
Type: msgtype,
EventID: event.ID,
TxnID: event.Unsigned.TransactionID,
Relation: *event.Content.GetRelatesTo(),
State: event.Unsigned.OutgoingState,
IsHighlight: false,
IsService: false,
Source: event.Content.VeryRaw,
Renderer: renderer,
}
}
@ -76,44 +92,38 @@ func unixToTime(unix int64) time.Time {
return timestamp
}
func (msg *BaseMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}
// Sender gets the string that should be displayed as the sender of this message.
//
// If the message is being sent, the sender is "Sending...".
// If sending has failed, the sender is "Error".
// If the message is an emote, the sender is blank.
// In any other case, the sender is the display name of the user who sent the message.
func (msg *BaseMessage) Sender() string {
switch msg.MsgState {
func (msg *UIMessage) Sender() string {
switch msg.State {
case mautrix.EventStateLocalEcho:
return "Sending..."
case mautrix.EventStateSendFail:
return "Error"
}
switch msg.MsgType {
switch msg.Type {
case "m.emote":
// Emotes don't show a separate sender, it's included in the buffer.
return ""
default:
return msg.MsgSender
return msg.SenderName
}
}
func (msg *BaseMessage) SenderID() string {
return msg.MsgSenderID
func (msg *UIMessage) NotificationSenderName() string {
return msg.SenderName
}
func (msg *BaseMessage) RealSender() string {
return msg.MsgSender
func (msg *UIMessage) NotificationContent() string {
return msg.Renderer.NotificationContent()
}
func (msg *BaseMessage) NotificationSenderName() string {
return msg.MsgSender
}
func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
switch msg.MsgState {
func (msg *UIMessage) getStateSpecificColor() tcell.Color {
switch msg.State {
case mautrix.EventStateLocalEcho:
return tcell.ColorGray
case mautrix.EventStateSendFail:
@ -132,31 +142,31 @@ func (msg *BaseMessage) getStateSpecificColor() tcell.Color {
//
// In any other case, the color is whatever is specified in the Message struct.
// Usually that means it is the hash-based color of the sender (see ui/widget/color.go)
func (msg *BaseMessage) SenderColor() tcell.Color {
func (msg *UIMessage) SenderColor() tcell.Color {
stateColor := msg.getStateSpecificColor()
switch {
case stateColor != tcell.ColorDefault:
return stateColor
case msg.MsgType == "m.room.member":
return widget.GetHashColor(msg.MsgSender)
case msg.MsgIsService:
case msg.Type == "m.room.member":
return widget.GetHashColor(msg.SenderName)
case msg.IsService:
return tcell.ColorGray
default:
return msg.MsgSenderColor
return msg.DefaultSenderColor
}
}
// TextColor returns the color the actual content of the message should be shown in.
func (msg *BaseMessage) TextColor() tcell.Color {
func (msg *UIMessage) TextColor() tcell.Color {
stateColor := msg.getStateSpecificColor()
switch {
case stateColor != tcell.ColorDefault:
return stateColor
case msg.MsgIsService, msg.MsgType == "m.notice":
case msg.IsService, msg.Type == "m.notice":
return tcell.ColorGray
case msg.MsgIsHighlight:
case msg.IsHighlight:
return tcell.ColorYellow
case msg.MsgType == "m.room.member":
case msg.Type == "m.room.member":
return tcell.ColorGreen
default:
return tcell.ColorDefault
@ -169,14 +179,14 @@ func (msg *BaseMessage) TextColor() tcell.Color {
// gray and red respectively.
//
// However, other messages are the default color instead of a color stored in the struct.
func (msg *BaseMessage) TimestampColor() tcell.Color {
if msg.MsgIsService {
func (msg *UIMessage) TimestampColor() tcell.Color {
if msg.IsService {
return tcell.ColorGray
}
return msg.getStateSpecificColor()
}
func (msg *BaseMessage) ReplyHeight() int {
func (msg *UIMessage) ReplyHeight() int {
if msg.ReplyTo != nil {
return 1 + msg.ReplyTo.Height()
}
@ -184,102 +194,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()
}

View File

@ -17,9 +17,12 @@
package messages
import (
"fmt"
"time"
ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
@ -27,55 +30,63 @@ import (
)
type ExpandedTextMessage struct {
BaseMessage
MsgText tstring.TString
Text tstring.TString
buffer []tstring.TString
}
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) UIMessage {
return &ExpandedTextMessage{
BaseMessage: newBaseMessage(event, displayname),
MsgText: text,
}
func NewExpandedTextMessage(event *mautrix.Event, displayname string, text tstring.TString) *UIMessage {
return newUIMessage(event, displayname, &ExpandedTextMessage{
Text: text,
})
}
func NewDateChangeMessage(text string) UIMessage {
func NewDateChangeMessage(text string) *UIMessage {
midnight := time.Now()
midnight = time.Date(midnight.Year(), midnight.Month(), midnight.Day(),
0, 0, 0, 0,
midnight.Location())
return &ExpandedTextMessage{
BaseMessage: BaseMessage{
MsgSenderID: "*",
MsgSender: "*",
MsgTimestamp: midnight,
MsgIsService: true,
return &UIMessage{
SenderID: "*",
SenderName: "*",
Timestamp: midnight,
IsService: true,
Renderer: &ExpandedTextMessage{
Text: tstring.NewColorTString(text, tcell.ColorGreen),
},
MsgText: tstring.NewColorTString(text, tcell.ColorGreen),
}
}
func (msg *ExpandedTextMessage) Clone() UIMessage {
func (msg *ExpandedTextMessage) Clone() MessageRenderer {
return &ExpandedTextMessage{
BaseMessage: msg.BaseMessage.clone(),
MsgText: msg.MsgText.Clone(),
Text: msg.Text.Clone(),
}
}
func (msg *ExpandedTextMessage) GenerateText() tstring.TString {
return msg.MsgText
}
func (msg *ExpandedTextMessage) NotificationContent() string {
return msg.MsgText.String()
return msg.Text.String()
}
func (msg *ExpandedTextMessage) PlainText() string {
return msg.MsgText.String()
return msg.Text.String()
}
func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
msg.CalculateReplyBuffer(prefs, width)
msg.calculateBufferWithText(prefs, msg.MsgText, width)
func (msg *ExpandedTextMessage) String() string {
return fmt.Sprintf(`&messages.ExpandedTextMessage{Text="%s"}`, msg.Text.String())
}
func (msg *ExpandedTextMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
msg.buffer = calculateBufferWithText(prefs, msg.Text, width, uiMsg)
}
func (msg *ExpandedTextMessage) Height() int {
return len(msg.buffer)
}
func (msg *ExpandedTextMessage) Draw(screen mauview.Screen) {
for y, line := range msg.buffer {
line.Draw(screen, 0, y)
}
}
func (msg *ExpandedTextMessage) RegisterMatrix(matrix ifc.MatrixContainer) {}

View File

@ -17,9 +17,7 @@
package messages
import (
"fmt"
"strings"
ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@ -29,30 +27,27 @@ import (
)
type HTMLMessage struct {
BaseMessage
Root html.Entity
FocusedBg tcell.Color
focused bool
}
func NewHTMLMessage(event *mautrix.Event, displayname string, root html.Entity) UIMessage {
return &HTMLMessage{
BaseMessage: newBaseMessage(event, displayname),
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,
}
}
func (hw *HTMLMessage) Draw(screen mauview.Screen) {
screen = hw.DrawReply(screen)
if hw.focused {
screen.SetStyle(tcell.StyleDefault.Background(hw.FocusedBg))
}
@ -80,18 +75,17 @@ func (hw *HTMLMessage) OnPasteEvent(event mauview.PasteEvent) bool {
return false
}
func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int) {
func (hw *HTMLMessage) CalculateBuffer(preferences config.UserPreferences, width int, msg *UIMessage) {
if width < 2 {
return
}
hw.CalculateReplyBuffer(preferences, width)
// TODO account for bare messages in initial startX
startX := 0
hw.Root.CalculateBuffer(width, startX, preferences.BareMessageView)
}
func (hw *HTMLMessage) Height() int {
return hw.ReplyHeight() + hw.Root.Height()
return hw.Root.Height()
}
func (hw *HTMLMessage) PlainText() string {
@ -103,8 +97,5 @@ func (hw *HTMLMessage) NotificationContent() string {
}
func (hw *HTMLMessage) String() string {
return fmt.Sprintf("&messages.HTMLMessage{\n" +
" Base=%s,\n" +
" Root=||\n%s\n" +
"}", strings.Replace(hw.BaseMessage.String(), "\n", "\n ", -1), hw.Root.String())
return hw.Root.String()
}

View File

@ -22,6 +22,7 @@ import (
"image/color"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/config"
@ -32,32 +33,30 @@ import (
)
type ImageMessage struct {
BaseMessage
Body string
Homeserver string
FileID string
data []byte
buffer []tstring.TString
matrix ifc.MatrixContainer
}
// NewImageMessage creates a new ImageMessage object with the provided values and the default state.
func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) UIMessage {
return &ImageMessage{
newBaseMessage(event, displayname),
body,
homeserver,
fileID,
data,
matrix,
}
func NewImageMessage(matrix ifc.MatrixContainer, event *mautrix.Event, displayname string, body, homeserver, fileID string, data []byte) *UIMessage {
return newUIMessage(event, displayname, &ImageMessage{
Body: body,
Homeserver: homeserver,
FileID: fileID,
data: data,
matrix: matrix,
})
}
func (msg *ImageMessage) Clone() UIMessage {
func (msg *ImageMessage) Clone() MessageRenderer {
data := make([]byte, len(msg.data))
copy(data, msg.data)
return &ImageMessage{
BaseMessage: msg.BaseMessage.clone(),
Body: msg.Body,
Homeserver: msg.Homeserver,
FileID: msg.FileID,
@ -70,7 +69,7 @@ func (msg *ImageMessage) RegisterMatrix(matrix ifc.MatrixContainer) {
msg.matrix = matrix
if len(msg.data) == 0 {
go msg.updateData()
//FIXME go msg.updateData()
}
}
@ -82,6 +81,10 @@ func (msg *ImageMessage) PlainText() string {
return fmt.Sprintf("%s: %s", msg.Body, msg.matrix.GetDownloadURL(msg.Homeserver, msg.FileID))
}
func (msg *ImageMessage) String() string {
return fmt.Sprintf(`&messages.ImageMessage{Body="%s", Homeserver="%s", FileID="%s"}`, msg.Body, msg.Homeserver, msg.FileID)
}
func (msg *ImageMessage) updateData() {
defer debug.Recover()
debug.Print("Loading image:", msg.Homeserver, msg.FileID)
@ -101,14 +104,13 @@ func (msg *ImageMessage) Path() string {
// CalculateBuffer generates the internal buffer for this message that consists
// of the text of this message split into lines at most as wide as the width
// parameter.
func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int) {
func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int, uiMsg *UIMessage) {
if width < 2 {
return
}
msg.CalculateReplyBuffer(prefs, width)
if prefs.BareMessageView || prefs.DisableImages {
msg.calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width)
msg.buffer = calculateBufferWithText(prefs, tstring.NewTString(msg.PlainText()), width, uiMsg)
return
}
@ -121,3 +123,13 @@ func (msg *ImageMessage) CalculateBuffer(prefs config.UserPreferences, width int
msg.buffer = image.Render()
}
func (msg *ImageMessage) Height() int {
return len(msg.buffer)
}
func (msg *ImageMessage) Draw(screen mauview.Screen) {
for y, line := range msg.buffer {
line.Draw(screen, 0, y)
}
}

View File

@ -1,53 +0,0 @@
// gomuks - A terminal Matrix client written in Go.
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package messages
import (
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
)
// UIMessage is a wrapper for the content and metadata of a Matrix message intended to be displayed.
type UIMessage interface {
ifc.Message
Type() mautrix.MessageType
Sender() string
SenderColor() tcell.Color
TextColor() tcell.Color
TimestampColor() tcell.Color
FormatTime() string
FormatDate() string
SameDate(message UIMessage) bool
SetReplyTo(message UIMessage)
CalculateBuffer(preferences config.UserPreferences, width int)
Draw(screen mauview.Screen)
Height() int
PlainText() string
Clone() UIMessage
RealSender() string
RegisterMatrix(matrix ifc.MatrixContainer)
}
const DateFormat = "January _2, 2006"
const TimeFormat = "15:04:05"

View File

@ -31,10 +31,10 @@ import (
"maunium.net/go/gomuks/ui/widget"
)
func getCachedEvent(mainView ifc.MainView, roomID, eventID string) UIMessage {
func getCachedEvent(mainView ifc.MainView, roomID, eventID string) *UIMessage {
if roomView := mainView.GetRoom(roomID); roomView != nil {
if replyToIfcMsg := roomView.GetEvent(eventID); replyToIfcMsg != nil {
if replyToMsg, ok := replyToIfcMsg.(UIMessage); ok && replyToMsg != nil {
if replyToMsg, ok := replyToIfcMsg.(*UIMessage); ok && replyToMsg != nil {
return replyToMsg
}
}
@ -42,24 +42,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

View File

@ -52,12 +52,12 @@ func matchBoundaryPattern(bare bool, extract tstring.TString) tstring.TString {
// CalculateBuffer generates the internal buffer for this message that consists
// of the text of this message split into lines at most as wide as the width
// parameter.
func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int) {
func calculateBufferWithText(prefs config.UserPreferences, text tstring.TString, width int, msg *UIMessage) []tstring.TString {
if width < 2 {
return
return nil
}
msg.buffer = []tstring.TString{}
var buffer []tstring.TString
if prefs.BareMessageView {
newText := tstring.NewTString(msg.FormatTime())
@ -74,7 +74,7 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
newlines := 0
for _, str := range forcedLinebreaks {
if len(str) == 0 && newlines < 1 {
msg.buffer = append(msg.buffer, tstring.TString{})
buffer = append(buffer, tstring.TString{})
newlines++
} else {
newlines = 0
@ -88,8 +88,9 @@ func (msg *BaseMessage) calculateBufferWithText(prefs config.UserPreferences, te
}
extract = matchBoundaryPattern(prefs.BareMessageView, extract)
}
msg.buffer = append(msg.buffer, extract)
buffer = append(buffer, extract)
str = str[len(extract):]
}
}
return buffer
}

View File

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

View File

@ -236,6 +236,10 @@ func (list *RoomList) AddScrollOffset(offset int) {
func (list *RoomList) First() (string, *rooms.Room) {
list.RLock()
defer list.RUnlock()
return list.first()
}
func (list *RoomList) first() (string, *rooms.Room) {
for _, tag := range list.tags {
trl := list.items[tag]
if trl.HasVisibleRooms() {
@ -248,6 +252,10 @@ func (list *RoomList) First() (string, *rooms.Room) {
func (list *RoomList) Last() (string, *rooms.Room) {
list.RLock()
defer list.RUnlock()
return list.last()
}
func (list *RoomList) last() (string, *rooms.Room) {
for tagIndex := len(list.tags) - 1; tagIndex >= 0; tagIndex-- {
tag := list.tags[tagIndex]
trl := list.items[tag]
@ -273,7 +281,7 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
if len(list.items) == 0 {
return "", nil
} else if list.selected == nil {
return list.First()
return list.first()
}
trl := list.items[list.selectedTag]
@ -295,11 +303,11 @@ func (list *RoomList) Previous() (string, *rooms.Room) {
return prevTag, prevTRL.LastVisible()
}
}
return list.Last()
return list.last()
} else if index >= 0 {
return list.selectedTag, trl.Visible()[index+1].Room
}
return list.First()
return list.first()
}
func (list *RoomList) Next() (string, *rooms.Room) {
@ -308,7 +316,7 @@ func (list *RoomList) Next() (string, *rooms.Room) {
if len(list.items) == 0 {
return "", nil
} else if list.selected == nil {
return list.First()
return list.first()
}
trl := list.items[list.selectedTag]
@ -330,11 +338,11 @@ func (list *RoomList) Next() (string, *rooms.Room) {
return nextTag, nextTRL.FirstVisible()
}
}
return list.First()
return list.first()
} else if index > 0 {
return list.selectedTag, trl.Visible()[index-1].Room
}
return list.Last()
return list.last()
}
// NextWithActivity Returns next room with activity.

View File

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

View File

@ -74,7 +74,7 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
view.username.SetText(ui.gmx.Config().UserID)
view.password.SetMaskCharacter('*')
view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorDarkCyan)
view.quitButton.SetOnClick(func() { ui.gmx.Stop(true) }).SetBackgroundColor(tcell.ColorDarkCyan)
view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorDarkCyan)
view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1})

View File

@ -26,6 +26,7 @@ import (
sync "github.com/sasha-s/go-deadlock"
"maunium.net/go/gomuks/ui/messages"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
@ -256,6 +257,7 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
if room == nil {
return
}
room.Load()
roomView, ok := view.getRoomView(room.ID, lock)
if !ok {
@ -263,12 +265,15 @@ func (view *MainView) switchRoom(tag string, room *rooms.Room, lock bool) {
debug.Print(tag, room)
return
}
roomView.Update()
view.roomView.SetInnerComponent(roomView)
view.currentRoom = roomView
view.MarkRead(roomView)
view.roomList.SetSelected(tag, room)
view.parent.Render()
if len(roomView.MessageView().messages) == 0 {
if msgView := roomView.MessageView(); len(msgView.messages) < 20 && !msgView.initialHistoryLoaded {
msgView.initialHistoryLoaded = true
go view.LoadHistory(room.ID)
}
}
@ -278,12 +283,6 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
roomView := NewRoomView(view, room).
SetInputChangedFunc(view.InputChanged)
view.rooms[room.ID] = roomView
roomView.UpdateUserList()
// TODO make sure this works
if len(roomView.MessageView().messages) == 0 {
go view.LoadHistory(room.ID)
}
return roomView
}
return nil
@ -292,7 +291,7 @@ func (view *MainView) addRoomPage(room *rooms.Room) *RoomView {
func (view *MainView) GetRoom(roomID string) ifc.RoomView {
room, ok := view.getRoomView(roomID, true)
if !ok {
return view.addRoom(view.matrix.GetRoom(roomID))
return view.addRoom(view.matrix.GetOrCreateRoom(roomID))
}
return room
}
@ -348,11 +347,11 @@ func (view *MainView) addRoom(room *rooms.Room) *RoomView {
return roomView
}
func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
func (view *MainView) SetRooms(rooms *rooms.RoomCache) {
view.roomList.Clear()
view.roomsLock.Lock()
view.rooms = make(map[string]*RoomView)
for _, room := range rooms {
for _, room := range rooms.Map {
if room.HasLeft {
continue
}
@ -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)