commit b46a755

delthas  ·  2022-02-10 14:44:05 +0000 UTC
parent 506b4f5
Add support for the soju.im/read capability and READ command

See: https://github.com/emersion/soju/blob/c7f0634ec8ee94425547b159bc36705582151012/doc/read.md
6 files changed,  +93, -0
M app.go
M app.go
+10, -0
 1@@ -221,6 +221,12 @@ func (app *App) eventLoop() {
 2 		}
 3 
 4 		if !app.pasting {
 5+			if netID, buffer, timestamp := app.win.UpdateRead(); buffer != "" {
 6+				s := app.sessions[netID]
 7+				if s != nil {
 8+					s.ReadSet(buffer, timestamp)
 9+				}
10+			}
11 			app.setStatus()
12 			app.updatePrompt()
13 			app.setBufferNumbers()
14@@ -741,6 +747,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
15 			if _, added := app.win.AddBuffer(netID, "", buffer); added {
16 				app.monitor[netID][buffer] = struct{}{}
17 				s.MonitorAdd(buffer)
18+				s.ReadGet(buffer)
19 				s.NewHistoryRequest(buffer).
20 					WithLimit(500).
21 					Before(msg.TimeOrNow())
22@@ -763,6 +770,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
23 				continue
24 			}
25 			s.MonitorAdd(target)
26+			s.ReadGet(target)
27 			app.win.AddBuffer(netID, "", target)
28 			// CHATHISTORY BEFORE excludes its bound, so add 1ms
29 			// (precision of the time tag) to include that last message.
30@@ -809,6 +817,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
31 		if !bounds.IsZero() {
32 			app.messageBounds[boundKey{netID, ev.Target}] = bounds
33 		}
34+	case irc.ReadEvent:
35+		app.win.SetRead(netID, ev.Target, ev.Timestamp)
36 	case irc.BouncerNetworkEvent:
37 		_, added := app.win.AddBuffer(ev.ID, ev.Name, "")
38 		if added {
+2, -0
 1@@ -343,6 +343,7 @@ func commandDoMsg(app *App, args []string) (err error) {
 2 		if buffer != "" && !s.IsChannel(target) {
 3 			app.monitor[netID][buffer] = struct{}{}
 4 			s.MonitorAdd(buffer)
 5+			s.ReadGet(buffer)
 6 			app.win.AddBuffer(netID, "", buffer)
 7 		}
 8 
 9@@ -452,6 +453,7 @@ func commandDoQuery(app *App, args []string) (err error) {
10 		return fmt.Errorf("cannot query a channel, use JOIN instead")
11 	}
12 	s.MonitorAdd(target)
13+	s.ReadGet(target)
14 	i, _ := app.win.AddBuffer(netID, "", target)
15 	s.NewHistoryRequest(target).WithLimit(200).Before(time.Now())
16 	app.win.JumpBufferIndex(i)
+5, -0
 1@@ -94,6 +94,11 @@ type HistoryTargetsEvent struct {
 2 	Targets map[string]time.Time
 3 }
 4 
 5+type ReadEvent struct {
 6+	Target    string
 7+	Timestamp time.Time
 8+}
 9+
10 type BouncerNetworkEvent struct {
11 	ID   string
12 	Name string
+33, -0
 1@@ -60,6 +60,7 @@ var SupportedCapabilities = map[string]struct{}{
 2 	"draft/chathistory":        {},
 3 	"draft/event-playback":     {},
 4 	"soju.im/bouncer-networks": {},
 5+	"soju.im/read":             {},
 6 }
 7 
 8 // Values taken by the "@+typing=" client tag.  TypingUnspec means the value or
 9@@ -426,6 +427,18 @@ func (s *Session) TypingStop(target string) {
10 	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
11 }
12 
13+func (s *Session) ReadGet(target string) {
14+	if _, ok := s.enabledCaps["soju.im/read"]; ok {
15+		s.out <- NewMessage("READ", target)
16+	}
17+}
18+
19+func (s *Session) ReadSet(target string, timestamp time.Time) {
20+	if _, ok := s.enabledCaps["soju.im/read"]; ok {
21+		s.out <- NewMessage("READ", target, formatTimestamp(timestamp))
22+	}
23+}
24+
25 func (s *Session) MonitorAdd(target string) {
26 	targetCf := s.casemap(target)
27 	if _, ok := s.monitors[targetCf]; !ok {
28@@ -1237,6 +1250,26 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
29 				Time:       msg.TimeOrNow(),
30 			}, nil
31 		}
32+	case "READ":
33+		if len(msg.Params) < 2 {
34+			break
35+		}
36+		var target, timestamp string
37+		if err := msg.ParseParams(&target, &timestamp); err != nil {
38+			return nil, err
39+		}
40+		if !strings.HasPrefix(timestamp, "timestamp=") {
41+			return nil, nil
42+		}
43+		timestamp = strings.TrimPrefix(timestamp, "timestamp=")
44+		t, ok := parseTimestamp(timestamp)
45+		if !ok {
46+			return nil, nil
47+		}
48+		return ReadEvent{
49+			Target:    target,
50+			Timestamp: t,
51+		}, nil
52 	case "BOUNCER":
53 		if len(msg.Params) < 3 {
54 			break
+34, -0
 1@@ -182,6 +182,7 @@ type buffer struct {
 2 	title      string
 3 	highlights int
 4 	unread     bool
 5+	read       time.Time
 6 
 7 	lines []Line
 8 	topic string
 9@@ -389,6 +390,39 @@ func (bs *BufferList) SetTopic(netID, title string, topic string) {
10 	b.topic = topic
11 }
12 
13+func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
14+	idx := bs.idx(netID, title)
15+	if idx < 0 {
16+		return
17+	}
18+	b := &bs.list[idx]
19+	if len(b.lines) > 0 && !b.lines[len(b.lines)-1].At.After(timestamp) {
20+		b.highlights = 0
21+		b.unread = false
22+	}
23+	if b.read.Before(timestamp) {
24+		b.read = timestamp
25+	}
26+}
27+
28+func (bs *BufferList) UpdateRead() (netID, title string, timestamp time.Time) {
29+	b := &bs.list[bs.current]
30+	var line *Line
31+	y := 0
32+	for i := len(b.lines) - 1; 0 <= i; i-- {
33+		line = &b.lines[i]
34+		if y >= b.scrollAmt {
35+			break
36+		}
37+		y += len(line.NewLines(bs.tlInnerWidth)) + 1
38+	}
39+	if line != nil && line.At.After(b.read) {
40+		b.read = line.At
41+		return b.netID, b.title, b.read
42+	}
43+	return "", "", time.Time{}
44+}
45+
46 func (bs *BufferList) Current() (netID, title string) {
47 	b := &bs.list[bs.current]
48 	return b.netID, b.title
+9, -0
 1@@ -3,6 +3,7 @@ package ui
 2 import (
 3 	"strings"
 4 	"sync/atomic"
 5+	"time"
 6 
 7 	"git.sr.ht/~taiite/senpai/irc"
 8 
 9@@ -236,6 +237,14 @@ func (ui *UI) SetTopic(netID, buffer string, topic string) {
10 	ui.bs.SetTopic(netID, buffer, topic)
11 }
12 
13+func (ui *UI) SetRead(netID, buffer string, timestamp time.Time) {
14+	ui.bs.SetRead(netID, buffer, timestamp)
15+}
16+
17+func (ui *UI) UpdateRead() (netID, buffer string, timestamp time.Time) {
18+	return ui.bs.UpdateRead()
19+}
20+
21 func (ui *UI) SetStatus(status string) {
22 	ui.status = status
23 }