chat/message: Fix RecentActiveUsers sort order
[ssh-chat] / chat / message / user.go
index d8934d3deaa7224db6fe4d83a450fdf2c3b7f58e..d4cc304c539c995076dcc27e02dba1adefc84538 100644 (file)
@@ -9,42 +9,44 @@ import (
        "sync"
        "time"
 
-       "github.com/shazow/ssh-chat/common"
+       "github.com/shazow/ssh-chat/set"
 )
 
 const messageBuffer = 5
 const messageTimeout = 5 * time.Second
 const reHighlight = `\b(%s)\b`
+const timestampTimeout = 30 * time.Minute
 
 var ErrUserClosed = errors.New("user closed")
 
 // User definition, implemented set Item interface and io.Writer
 type User struct {
        Identifier
+       Ignored  *set.Set
        colorIdx int
        joined   time.Time
        msg      chan Message
        done     chan struct{}
-       Ignored  *common.IdSet
 
        screen    io.WriteCloser
        closeOnce sync.Once
 
        mu      sync.Mutex
-       Config  UserConfig
-       replyTo *User // Set when user gets a /msg, for replying.
+       config  UserConfig
+       replyTo *User     // Set when user gets a /msg, for replying.
+       lastMsg time.Time // When the last message was rendered
 }
 
 func NewUser(identity Identifier) *User {
        u := User{
                Identifier: identity,
-               Config:     DefaultUserConfig,
+               config:     DefaultUserConfig,
                joined:     time.Now(),
                msg:        make(chan Message, messageBuffer),
                done:       make(chan struct{}),
-               Ignored:    common.NewIdSet(),
+               Ignored:    set.New(),
        }
-       u.SetColorIdx(rand.Int())
+       u.setColorIdx(rand.Int())
 
        return &u
 }
@@ -56,10 +58,26 @@ func NewUserScreen(identity Identifier, screen io.WriteCloser) *User {
        return u
 }
 
+func (u *User) Joined() time.Time {
+       return u.joined
+}
+
+func (u *User) Config() UserConfig {
+       u.mu.Lock()
+       defer u.mu.Unlock()
+       return u.config
+}
+
+func (u *User) SetConfig(cfg UserConfig) {
+       u.mu.Lock()
+       u.config = cfg
+       u.mu.Unlock()
+}
+
 // Rename the user with a new Identifier.
-func (u *User) SetId(id string) {
-       u.Identifier.SetId(id)
-       u.SetColorIdx(rand.Int())
+func (u *User) SetID(id string) {
+       u.Identifier.SetID(id)
+       u.setColorIdx(rand.Int())
 }
 
 // ReplyTo returns the last user that messaged this user.
@@ -76,24 +94,12 @@ func (u *User) SetReplyTo(user *User) {
        u.replyTo = user
 }
 
-// ToggleQuietMode will toggle whether or not quiet mode is enabled
-func (u *User) ToggleQuietMode() {
-       u.mu.Lock()
-       defer u.mu.Unlock()
-       u.Config.Quiet = !u.Config.Quiet
-}
-
-// SetColorIdx will set the colorIdx to a specific value, primarily used for
+// setColorIdx will set the colorIdx to a specific value, primarily used for
 // testing.
-func (u *User) SetColorIdx(idx int) {
+func (u *User) setColorIdx(idx int) {
        u.colorIdx = idx
 }
 
-// Block until user is closed
-func (u *User) Wait() {
-       <-u.done
-}
-
 // Disconnect user, stop accepting messages
 func (u *User) Close() {
        u.closeOnce.Do(func() {
@@ -144,28 +150,49 @@ func (u *User) SetHighlight(s string) error {
                return err
        }
        u.mu.Lock()
-       u.Config.Highlight = re
+       u.config.Highlight = re
        u.mu.Unlock()
        return nil
 }
 
 func (u *User) render(m Message) string {
-       u.mu.Lock()
-       cfg := u.Config
-       u.mu.Unlock()
+       cfg := u.Config()
+       var out string
        switch m := m.(type) {
        case PublicMsg:
-               return m.RenderFor(cfg) + Newline
-       case PrivateMsg:
-               u.SetReplyTo(m.From())
-               return m.Render(cfg.Theme) + Newline
+               if u == m.From() {
+                       if !cfg.Echo {
+                               return ""
+                       }
+                       out += m.RenderSelf(cfg)
+               } else {
+                       out += m.RenderFor(cfg)
+               }
+       case *PrivateMsg:
+               out += m.Render(cfg.Theme)
+               if cfg.Bell {
+                       out += Bel
+               }
+       case *CommandMsg:
+               out += m.RenderSelf(cfg)
        default:
-               return m.Render(cfg.Theme) + Newline
+               out += m.Render(cfg.Theme)
        }
+       if cfg.Timeformat != nil {
+               ts := m.Timestamp()
+               if cfg.Timezone != nil {
+                       ts = ts.In(cfg.Timezone)
+               } else {
+                       ts = ts.UTC()
+               }
+               return cfg.Theme.Timestamp(ts.Format(*cfg.Timeformat)) + "  " + out + Newline
+       }
+       return out + Newline
 }
 
-// HandleMsg will render the message to the screen, blocking.
-func (u *User) HandleMsg(m Message) error {
+// writeMsg renders the message and attempts to write it, will Close the user
+// if it fails.
+func (u *User) writeMsg(m Message) error {
        r := u.render(m)
        _, err := u.screen.Write([]byte(r))
        if err != nil {
@@ -175,12 +202,20 @@ func (u *User) HandleMsg(m Message) error {
        return err
 }
 
+// HandleMsg will render the message to the screen, blocking.
+func (u *User) HandleMsg(m Message) error {
+       u.mu.Lock()
+       u.lastMsg = m.Timestamp()
+       u.mu.Unlock()
+       return u.writeMsg(m)
+}
+
 // Add message to consume by user
 func (u *User) Send(m Message) error {
        select {
-       case u.msg <- m:
        case <-u.done:
                return ErrUserClosed
+       case u.msg <- m:
        case <-time.After(messageTimeout):
                logger.Printf("Message buffer full, closing: %s", u.Name())
                u.Close()
@@ -189,42 +224,15 @@ func (u *User) Send(m Message) error {
        return nil
 }
 
-func (u *User) Ignore(identified common.Identified) error {
-       if identified == nil {
-               return errors.New("user is nil.")
-       }
-
-       if identified.Id() == u.Id() {
-               return errors.New("cannot ignore self.")
-       }
-
-       if u.Ignored.In(identified) {
-               return errors.New("user already ignored.")
-       }
-
-       u.Ignored.Add(identified)
-       return nil
-}
-
-func (u *User) Unignore(id string) error {
-       if id == "" {
-               return errors.New("user is nil.")
-       }
-
-       identified, err := u.Ignored.Get(id)
-       if err != nil {
-               return err
-       }
-
-       return u.Ignored.Remove(identified)
-}
-
 // Container for per-user configurations.
 type UserConfig struct {
-       Highlight *regexp.Regexp
-       Bell      bool
-       Quiet     bool
-       Theme     *Theme
+       Highlight  *regexp.Regexp
+       Bell       bool
+       Quiet      bool
+       Echo       bool // Echo shows your own messages after sending, disabled for bots
+       Timeformat *string
+       Timezone   *time.Location
+       Theme      *Theme
 }
 
 // Default user configuration to use
@@ -233,8 +241,28 @@ var DefaultUserConfig UserConfig
 func init() {
        DefaultUserConfig = UserConfig{
                Bell:  true,
+               Echo:  true,
                Quiet: false,
        }
 
        // TODO: Seed random?
 }
+
+// RecentActiveUsers is a slice of *Users that knows how to be sorted by the time of the last message.
+type RecentActiveUsers []*User
+
+func (a RecentActiveUsers) Len() int      { return len(a) }
+func (a RecentActiveUsers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a RecentActiveUsers) Less(i, j int) bool {
+       a[i].mu.Lock()
+       defer a[i].mu.Unlock()
+       a[j].mu.Lock()
+       defer a[j].mu.Unlock()
+
+       if a[i].lastMsg.IsZero() {
+               return a[i].joined.Before(a[j].joined)
+       } else {
+               return a[i].lastMsg.Before(a[j].lastMsg)
+       }
+
+}