members: show joined date in admin + public lists; 'last active: X ago'

Admin Users view appends 'joined YYYY-MM-DD'. Public member directory
shows 'last active: N days ago' for offline members plus the joined date,
both sourced from store.User.CreatedAt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-07-01 02:40:24 +00:00
parent 28ad36834e
commit 042166b05f
2 changed files with 35 additions and 3 deletions

View file

@ -344,6 +344,9 @@ func (m Model) viewUsers() (string, string) {
if u.EmailVerified { if u.EmailVerified {
flags = append(flags, "verified") flags = append(flags, "verified")
} }
if !u.CreatedAt.IsZero() {
flags = append(flags, "joined "+u.CreatedAt.Format("2006-01-02"))
}
name := u.Name name := u.Name
if u.Banned { if u.Banned {
name = warnStyle.Render(name + " [BANNED]") name = warnStyle.Render(name + " [BANNED]")

View file

@ -51,6 +51,7 @@ type person struct {
online bool online bool
lastSeen time.Time lastSeen time.Time
seenOK bool seenOK bool
joined time.Time
} }
type model struct { type model struct {
@ -92,7 +93,7 @@ func (m *model) load() tea.Cmd {
if u.Name == me { if u.Name == me {
continue // don't list yourself in the directory continue // don't list yourself in the directory
} }
p := person{name: u.Name, kind: u.Kind, online: online[strings.ToLower(u.Name)]} p := person{name: u.Name, kind: u.Kind, online: online[strings.ToLower(u.Name)], joined: u.CreatedAt}
if t, ok, _ := st.LastSeen(u.ID); ok { if t, ok, _ := st.LastSeen(u.ID); ok {
p.lastSeen, p.seenOK = t, true p.lastSeen, p.seenOK = t, true
} }
@ -410,9 +411,12 @@ func (m *model) listView() string {
c = cur.Render(" ") c = cur.Render(" ")
name = sel.Render(name) name = sel.Render(name)
} }
seen := "online" seen := "online now"
if !p.online { if !p.online {
seen = "last " + relTime(p.lastSeen, p.seenOK) seen = "last active: " + relTimeLong(p.lastSeen, p.seenOK)
}
if !p.joined.IsZero() {
seen += " · joined " + p.joined.Format("2006-01-02")
} }
row := fmt.Sprintf("%s%s %s %-20s %-8s %s", c, box, dot, name, p.kind, dim.Render(seen)) row := fmt.Sprintf("%s%s %s %-20s %-8s %s", c, box, dot, name, p.kind, dim.Render(seen))
s += row + "\n" s += row + "\n"
@ -507,3 +511,28 @@ func relTime(t time.Time, ok bool) string {
return fmt.Sprintf("%dd", int(d.Hours()/24)) return fmt.Sprintf("%dd", int(d.Hours()/24))
} }
} }
// relTimeLong renders a spelled-out "3 days ago" / "2 hours ago" / "just now"
// age for the public directory. ok=false → "never".
func relTimeLong(t time.Time, ok bool) string {
if !ok || t.IsZero() {
return "never"
}
d := time.Since(t)
plural := func(n int, unit string) string {
if n == 1 {
return fmt.Sprintf("1 %s ago", unit)
}
return fmt.Sprintf("%d %ss ago", n, unit)
}
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
return plural(int(d.Minutes()), "minute")
case d < 24*time.Hour:
return plural(int(d.Hours()), "hour")
default:
return plural(int(d.Hours()/24), "day")
}
}