---
app.go | 3 +++
irc/events.go | 5 +++++
irc/session.go | 26 ++++++++++++++++++++++++++
irc/tokens.go | 14 +++++---------
ui/buffers.go | 40 +++++++++++++++++++++++++++++++++++++++-
ui/ui.go | 8 ++++++++
window.go | 11 +++++++++++
7 files changed, 97 insertions(+), 10 deletions(-)
diff --git a/app.go b/app.go
index 01f9bc4..d9e5971 100644
--- a/app.go
+++ b/app.go
@@ -115,6 +115,7 @@ func (app *App) eventLoop() {
app.handleEvents(evs)
if !app.pasting {
app.setStatus()
+ app.updateRead()
app.win.Draw()
}
}
@@ -572,6 +573,8 @@ func (app *App) handleIRCEvent(ev interface{}) {
HeadColor: tcell.ColorGray,
Body: body.StyledString(),
})
+ case irc.ReadEvent:
+ app.win.SetRead(ev.Target, ev.Time)
case irc.MessageEvent:
buffer, line, hlNotification := app.formatMessage(ev)
app.win.AddLine(buffer, hlNotification, line)
diff --git a/irc/events.go b/irc/events.go
index bdd6914..ea5695d 100644
--- a/irc/events.go
+++ b/irc/events.go
@@ -62,3 +62,8 @@ type HistoryEvent struct {
Target string
Messages []Event
}
+
+type ReadEvent struct {
+ Target string
+ Time time.Time
+}
diff --git a/irc/session.go b/irc/session.go
index e50312f..bd45321 100644
--- a/irc/session.go
+++ b/irc/session.go
@@ -58,6 +58,7 @@ var SupportedCapabilities = map[string]struct{}{
"server-time": {},
"sasl": {},
"setname": {},
+ "soju.im/read": {},
"userhost-in-names": {},
}
@@ -282,6 +283,13 @@ func (s *Session) ChangeTopic(channel, topic string) {
s.out <- NewMessage("TOPIC", channel, topic)
}
+func (s *Session) Read(target string, time time.Time) {
+ if _, ok := s.enabledCaps["soju.im/read"]; !ok {
+ return
+ }
+ s.out <- NewMessage("READ", target, fmt.Sprintf("timestamp=%s", time.UTC().Format(serverTimeLayout)))
+}
+
func (s *Session) Quit(reason string) {
s.out <- NewMessage("QUIT", reason)
}
@@ -691,6 +699,24 @@ func (s *Session) handleRegistered(msg Message) Event {
Topic: c.Topic,
}
}
+ case "READ":
+ if !strings.HasPrefix(msg.Params[1], "timestamp=") {
+ break
+ }
+ stamp, err := time.Parse(serverTimeLayout, strings.TrimPrefix(msg.Params[1], "timestamp="))
+ if err != nil {
+ break
+ }
+ stamp = stamp.In(time.Local)
+
+ name := msg.Params[0]
+ if c, ok := s.channels[s.Casemap(msg.Params[0])]; ok {
+ name = c.Name
+ }
+ return ReadEvent{
+ Target: name,
+ Time: stamp,
+ }
case "PRIVMSG", "NOTICE":
targetCf := s.casemap(msg.Params[0])
nickCf := s.casemap(msg.Prefix.Name)
diff --git a/irc/tokens.go b/irc/tokens.go
index 9c669bb..bec7dbc 100644
--- a/irc/tokens.go
+++ b/irc/tokens.go
@@ -2,12 +2,13 @@ package irc
import (
"errors"
- "fmt"
"strconv"
"strings"
"time"
)
+const serverTimeLayout = "2006-01-02T15:04:05.000Z"
+
// CasemapASCII of name is the canonical representation of name according to the
// ascii casemapping.
func CasemapASCII(name string) string {
@@ -416,22 +417,17 @@ func (msg *Message) IsValid() bool {
// Time returns the time when the message has been sent, if present.
func (msg *Message) Time() (t time.Time, ok bool) {
var tag string
- var year, month, day, hour, minute, second, millis int
-
tag, ok = msg.Tags["time"]
if !ok {
return
}
- tag = strings.TrimSuffix(tag, "Z")
-
- _, err := fmt.Sscanf(tag, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis)
- if err != nil || month < 1 || 12 < month {
+ t, err := time.Parse(serverTimeLayout, tag)
+ if err != nil {
ok = false
return
}
-
- t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC)
+ t = t.In(time.Local)
return
}
diff --git a/ui/buffers.go b/ui/buffers.go
index 0ebaadf..174ade4 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -160,6 +160,7 @@ func (l *Line) NewLines(width int) []int {
type buffer struct {
title string
highlights int
+ read time.Time
unread bool
lines []Line
@@ -277,7 +278,7 @@ func (bs *BufferList) AddLine(title string, highlight bool, line Line) {
}
}
- if !line.Mergeable && idx != bs.current {
+ if !line.Mergeable && idx != bs.current && !line.At.Before(b.read) {
b.unread = true
}
if highlight && idx != bs.current {
@@ -342,6 +343,43 @@ func (bs *BufferList) CurrentOldestTime() (t *time.Time) {
return
}
+func (bs *BufferList) SetRead(title string, stamp time.Time) {
+ idx := bs.idx(title)
+ if idx < 0 {
+ return
+ }
+ b := &bs.list[idx]
+ if stamp.After(b.read) {
+ b.read = stamp
+ if len(b.lines) > 0 {
+ last := b.lines[len(b.lines)-1].At
+ if !stamp.Before(last) {
+ b.unread = false
+ }
+ }
+ }
+}
+
+func (bs *BufferList) UpdateRead() (string, time.Time) {
+ b := &bs.list[bs.current]
+ if len(b.lines) == 0 {
+ return "", time.Time{}
+ }
+
+ // TODO: also update read if we're not at bottom, by using the stamp of the newest visible Line
+ lastStamp := b.lines[len(b.lines)-1].At
+ if !lastStamp.After(b.read) {
+ return "", time.Time{}
+ }
+ now := time.Now()
+ if !now.After(lastStamp) {
+ return "", time.Time{}
+ }
+ b.read = now
+
+ return b.title, now
+}
+
func (bs *BufferList) ScrollUp(n int) {
b := &bs.list[bs.current]
if b.isAtTop {
diff --git a/ui/ui.go b/ui/ui.go
index be43272..0a16d8b 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -163,6 +163,14 @@ func (ui *UI) SetPrompt(prompt StyledString) {
ui.prompt = prompt
}
+func (ui *UI) SetRead(buffer string, stamp time.Time) {
+ ui.bs.SetRead(buffer, stamp)
+}
+
+func (ui *UI) UpdateRead() (string, time.Time) {
+ return ui.bs.UpdateRead()
+}
+
func (ui *UI) InputIsCommand() bool {
return ui.e.IsCommand()
}
diff --git a/window.go b/window.go
index 3d80776..919af7c 100644
--- a/window.go
+++ b/window.go
@@ -61,6 +61,17 @@ func (app *App) setStatus() {
app.win.SetStatus(status)
}
+func (app *App) updateRead() {
+ if app.s == nil {
+ return
+ }
+ target, stamp := app.win.UpdateRead()
+ if target == "" || target == Home {
+ return
+ }
+ app.s.Read(target, stamp)
+}
+
func identColor(ident string) tcell.Color {
h := fnv.New32()
_, _ = h.Write([]byte(ident))
--
2.30.0