chat/message: Fix RecentActiveUsers sort order
[ssh-chat] / chat / message / user.go
index d4d1fcb7d47fb63cdd2fe93ad1bf9001842e8f0f..d4cc304c539c995076dcc27e02dba1adefc84538 100644 (file)
@@ -8,37 +8,45 @@ import (
        "regexp"
        "sync"
        "time"
+
+       "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
-       Config   UserConfig
+       Ignored  *set.Set
        colorIdx int
        joined   time.Time
        msg      chan Message
        done     chan struct{}
 
-       replyTo   *User // Set when user gets a /msg, for replying.
        screen    io.WriteCloser
        closeOnce sync.Once
+
+       mu      sync.Mutex
+       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:    set.New(),
        }
-       u.SetColorIdx(rand.Int())
+       u.setColorIdx(rand.Int())
 
        return &u
 }
@@ -50,38 +58,48 @@ 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.
 func (u *User) ReplyTo() *User {
+       u.mu.Lock()
+       defer u.mu.Unlock()
        return u.replyTo
 }
 
 // SetReplyTo sets the last user to message this user.
 func (u *User) SetReplyTo(user *User) {
+       u.mu.Lock()
+       defer u.mu.Unlock()
        u.replyTo = user
 }
 
-// ToggleQuietMode will toggle whether or not quiet mode is enabled
-func (u *User) ToggleQuietMode() {
-       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() {
@@ -114,30 +132,67 @@ func (u *User) ConsumeOne() Message {
        return <-u.msg
 }
 
+// Check if there are pending messages, used for testing
+func (u *User) HasMessages() bool {
+       select {
+       case msg := <-u.msg:
+               u.msg <- msg
+               return true
+       default:
+               return false
+       }
+}
+
 // SetHighlight sets the highlighting regular expression to match string.
 func (u *User) SetHighlight(s string) error {
        re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
        if err != nil {
                return err
        }
-       u.Config.Highlight = re
+       u.mu.Lock()
+       u.config.Highlight = re
+       u.mu.Unlock()
        return nil
 }
 
 func (u *User) render(m Message) string {
+       cfg := u.Config()
+       var out string
        switch m := m.(type) {
-       case *PublicMsg:
-               return m.RenderFor(u.Config) + Newline
+       case PublicMsg:
+               if u == m.From() {
+                       if !cfg.Echo {
+                               return ""
+                       }
+                       out += m.RenderSelf(cfg)
+               } else {
+                       out += m.RenderFor(cfg)
+               }
        case *PrivateMsg:
-               u.SetReplyTo(m.From())
-               return m.Render(u.Config.Theme) + Newline
+               out += m.Render(cfg.Theme)
+               if cfg.Bell {
+                       out += Bel
+               }
+       case *CommandMsg:
+               out += m.RenderSelf(cfg)
        default:
-               return m.Render(u.Config.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 {
@@ -147,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()
@@ -163,10 +226,13 @@ func (u *User) Send(m Message) error {
 
 // 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
@@ -175,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)
+       }
+
+}