commit 466484d
sewn
·
2026-07-28 11:20:26 +0000 UTC
parent ea46a8d
Add displaying message replyto
5 files changed,
+119,
-12
M
app.go
+2,
-0
1@@ -2145,6 +2145,8 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
2
3 line = ui.Line{
4 At: ev.Time,
5+ ID: ev.ID,
6+ ReplyTo: ev.ReplyTo,
7 Head: head.StyledString(),
8 Notify: notification,
9 Body: body.StyledString(),
+2,
-0
1@@ -88,6 +88,8 @@ type InviteEvent struct {
2 }
3
4 type MessageEvent struct {
5+ ID string
6+ ReplyTo string
7 User string
8 Target string
9 TargetIsChannel bool
+2,
-0
1@@ -2182,6 +2182,8 @@ func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
2 target := rawTarget[i:]
3
4 ev = MessageEvent{
5+ ID: msg.ID(),
6+ ReplyTo: msg.ReplyTo(),
7 User: msg.Prefix.Name, // TODO correctly casemap
8 Target: target, // TODO correctly casemap
9 TargetPrefix: prefix,
+12,
-0
1@@ -377,6 +377,18 @@ func parseTimestamp(timestamp string) (time.Time, bool) {
2 return t.UTC(), true
3 }
4
5+func (msg *Message) ID() string {
6+ return msg.Tags["msgid"]
7+}
8+
9+// ReplyTo returns the msgid of the message this one is a reply to, if any.
10+func (msg *Message) ReplyTo() string {
11+ if id, ok := msg.Tags["+draft/reply"]; ok {
12+ return id
13+ }
14+ return msg.Tags["+reply"]
15+}
16+
17 // Time returns the time when the message has been sent, if present.
18 func (msg *Message) Time() (t time.Time, ok bool) {
19 tag, ok := msg.Tags["time"]
+101,
-12
1@@ -50,11 +50,20 @@ type Line struct {
2 Mergeable bool
3 Data interface{}
4
5+ ID string
6+ ReplyTo string
7+ Reply *Reply
8+
9 splitPoints []point
10 width int
11 newLines []int
12 }
13
14+type Reply struct {
15+ Nick StyledString
16+ Body StyledString
17+}
18+
19 func (l *Line) IsZero() bool {
20 return l.Body.string == ""
21 }
22@@ -194,6 +203,16 @@ func (l *Line) NewLines(vx *Vaxis, width int) []int {
23 return l.newLines
24 }
25
26+// Rows returns the number of terminal rows l occupies at the given width,
27+// including its reply preview row, if any.
28+func (l *Line) Rows(vx *Vaxis, width int) int {
29+ n := len(l.NewLines(vx, width)) + 1
30+ if l.Reply != nil {
31+ n++
32+ }
33+ return n
34+}
35+
36 type buffer struct {
37 netID string
38 netName string
39@@ -501,6 +520,10 @@ func (bs *BufferList) AddLine(netID, title string, line Line) {
40 }
41 current := bs.cur()
42
43+ if line.ReplyTo != "" {
44+ line.Reply = bs.resolveReply(netID, title, line.ReplyTo)
45+ }
46+
47 n := len(b.lines)
48 line.At = line.At.UTC()
49
50@@ -518,7 +541,7 @@ func (bs *BufferList) AddLine(netID, title string, line Line) {
51 line.computeSplitPoints(bs.ui.vx)
52 b.lines = append(b.lines, line)
53 if b == current && 0 < b.scrollAmt {
54- b.scrollAmt += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
55+ b.scrollAmt += line.Rows(bs.ui.vx, bs.textWidth)
56 }
57 }
58
59@@ -537,6 +560,46 @@ func (bs *BufferList) AddLine(netID, title string, line Line) {
60 }
61 }
62
63+func (bs *BufferList) resolveReply(netID, title, replyTo string) *Reply {
64+ _, b := bs.at(netID, title)
65+ if b == nil {
66+ return nil
67+ }
68+ for i := len(b.lines) - 1; i >= 0; i-- {
69+ if b.lines[i].ID == replyTo {
70+ return &Reply{
71+ Nick: b.lines[i].Head,
72+ Body: b.lines[i].Body,
73+ }
74+ }
75+ }
76+ return nil
77+}
78+
79+func (bs *BufferList) replyPreviewText(reply *Reply) StyledString {
80+ var sb StyledStringBuilder
81+ sb.WriteStyledString(ColorString("╭─ ", bs.ui.config.Colors.Gray))
82+
83+ if reply == nil {
84+ sb.WriteStyledString(ColorString("(message not found)", bs.ui.config.Colors.Gray))
85+ return sb.StyledString()
86+ }
87+
88+ sb.WriteStyledString(reply.Nick)
89+ sb.WriteStyledString(ColorString(": ", bs.ui.config.Colors.Gray))
90+
91+ snippet := strings.Map(func(r rune) rune {
92+ if r == '\n' || r == '\r' {
93+ return ' '
94+ }
95+ return r
96+ }, reply.Body.string)
97+ snippet = truncate(bs.ui.vx, snippet, max(bs.textWidth, 48), "…")
98+ sb.WriteStyledString(ColorString(snippet, bs.ui.config.Colors.Gray))
99+
100+ return sb.StyledString()
101+}
102+
103 func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
104 _, b := bs.at(netID, title)
105 if b == nil {
106@@ -559,6 +622,17 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
107 }
108 line.computeSplitPoints(bs.ui.vx)
109 }
110+ if line.ReplyTo != "" && line.Reply == nil {
111+ for j := len(lines) - 1; j >= 0; j-- {
112+ if lines[j].ID == line.ReplyTo {
113+ line.Reply = &Reply{Nick: lines[j].Head, Body: lines[j].Body}
114+ break
115+ }
116+ }
117+ if line.Reply == nil {
118+ line.Reply = bs.resolveReply(netID, title, line.ReplyTo)
119+ }
120+ }
121 lines = append(lines, line)
122 }
123
124@@ -816,7 +890,7 @@ func (bs *BufferList) forEachLine(b *buffer, f func(line *Line, y int) bool) boo
125 if f(line, y) {
126 return true
127 }
128- y += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
129+ y += line.Rows(bs.ui.vx, bs.textWidth)
130 }
131 return false
132 }
133@@ -1215,37 +1289,52 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
134 }
135
136 yi -= len(nls) + 1
137+ if line.Reply != nil {
138+ yi--
139+ }
140 if y0+bs.tlHeight <= yi {
141 continue
142 }
143
144- showDate := bs.shouldShowDate(b, i, yi, y0)
145+ cY := yi
146+ if line.Reply != nil {
147+ cY++
148+ if yi >= y0 {
149+ preview := bs.replyPreviewText(line.Reply)
150+ preview.string = truncate(bs.ui.vx, preview.string,
151+ nickColWidth+2+bs.textWidth, "…")
152+ x := x0 + nickColWidth + 6
153+ printString(vx, &x, yi, preview)
154+ }
155+ }
156+
157+ showDate := bs.shouldShowDate(b, i, cY, y0)
158 if showDate {
159 st := vaxis.Style{
160 Attribute: vaxis.AttrBold,
161 }
162 // as a special case, always draw the first visible message date, even if it is a continuation line
163- yd := yi
164+ yd := cY
165 if yd < y0 {
166 yd = y0
167 }
168 printDate(vx, x0, yd, st, line.At.Local())
169 } else {
170- showTime := b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) && yi >= y0
171+ showTime := b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) && cY >= y0
172 if !showTime {
173 // also try to show the time if we previously drew the date
174- yp := yi - (len(b.lines[i-1].NewLines(bs.ui.vx, bs.textWidth)) + 1)
175+ yp := cY - b.lines[i-1].Rows(bs.ui.vx, bs.textWidth)
176 showTime = i == 0 || bs.shouldShowDate(b, i-1, yp, y0)
177 }
178 if showTime {
179 st := vaxis.Style{
180 Foreground: bs.ui.config.Colors.Gray,
181 }
182- printTime(vx, x0, yi, st, line.At.Local())
183+ printTime(vx, x0, cY, st, line.At.Local())
184 }
185 }
186
187- if yi >= y0 {
188+ if cY >= y0 {
189 head := line.Head
190 if line.Highlight && len(line.Head.styles) > 0 {
191 var sb StyledStringBuilder
192@@ -1257,7 +1346,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
193 }
194 head = sb.StyledString()
195 }
196- xb, xe := printIdent(vx, x0+7, yi, nickColWidth, head)
197+ xb, xe := printIdent(vx, x0+7, cY, nickColWidth, head)
198
199 lastHead := line.Head.string
200 if len(line.Head.styles) > 0 {
201@@ -1268,7 +1357,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
202 ui.clickEvents = append(ui.clickEvents, clickEvent{
203 xb: xb,
204 xe: xe,
205- y: yi,
206+ y: cY,
207 event: &events.EventClickNick{
208 EventClick: events.EventClick{
209 NetID: b.netID,
210@@ -1281,7 +1370,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
211 }
212
213 x := x1
214- y := yi
215+ y := cY
216 var style vaxis.Style
217 nextStyles := line.Body.styles
218
219@@ -1305,7 +1394,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
220 }
221 }
222
223- if y != yi && x == x1 && IsSplitRune(l[0]) {
224+ if y != cY && x == x1 && IsSplitRune(l[0]) {
225 lbi += len(string(l[0]))
226 l = l[1:]
227 continue