nandi/frqpublic Fork 0
4876db8
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Answer a message, not just read that one was answered

The chip said what a message replied to; there was no way to make one. ↩ sits
beside the sender's name — where a run of lines from one person is answered as
the thing it is, rather than every line of it carrying a button — and the
compose bar then says who is being answered, with a way out of it.

The draft goes out tagged `+draft/reply`, which is what freeq's other clients
thread on and what this one draws its chips from. The local echo carries the
same link, so the chip is there before the server's copy comes back, if it
ever does.

The banner's row is added to the scroll's reserve, and does move the compose
bar. That is the difference between it and the jump button: this row appears
because the reader asked to answer something, and knows to expect it.

Verified on irc.freeq.at with a second client watching: the line arrives
tagged, pointing at the message that was answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T12:10:07-07:00 Browse files
4876db8 parent: cbbcecd
modified README.md +2 -1
@@ -108,7 +108,8 @@ surface — that surface does not work on Android either, while the syscalls do.
108108 * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
109109 * Twelve-hour timestamps from the server's own clock, with a heading wherever
110110 the day changes
111-* A chip above a reply quoting what it answers, and a click that goes there
111+* A chip above a reply quoting what it answers, and a click that goes there;
112+ ↩ beside a sender to answer them, with `+draft/reply` on the way out
112113 * Inline previews for PNG links, fetched once and cached under
113114 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
114115 * Join/part notices, DMs bucketed under the sender's nick
@@ -108,7 +108,8 @@ surface — that surface does not work on Android either, while the syscalls do.
108 * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead108 * Backlog on join, and `CHATHISTORY` for the channels freeq restores instead
109 * Twelve-hour timestamps from the server's own clock, with a heading wherever109 * Twelve-hour timestamps from the server's own clock, with a heading wherever
110 the day changes110 the day changes
111-* A chip above a reply quoting what it answers, and a click that goes there111+* A chip above a reply quoting what it answers, and a click that goes there;
112+ ↩ beside a sender to answer them, with `+draft/reply` on the way out
112 * Inline previews for PNG links, fetched once and cached under113 * Inline previews for PNG links, fetched once and cached under
113 `$XDG_CACHE_HOME/frq/media`; click one to see it full size114 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
114 * Join/part notices, DMs bucketed under the sender's nick115 * Join/part notices, DMs bucketed under the sender's nick
modified src/frq/app.jolt +18 -2
@@ -280,7 +280,13 @@
280280 :size 22}])
281281 [:dim-label {:label (:from m)}]
282282 (when-let [at (:at m)]
283- [:dim-label {:label (clock/clock-time at)}])])]
283+ [:dim-label {:label (clock/clock-time at)}])
284+ ;; Answering is offered where the sender is named: a run of lines from
285+ ;; one person is answered as the thing it is, rather than every line
286+ ;; of it carrying a button of its own.
287+ [:vbox {:key :reply-action}
288+ (when (:id m)
289+ [:button {:label "↩" :on-click #(s/reply-to! m)}])]])]
284290 [:vbox {:key :text :spacing 2}
285291 (map-indexed (fn [j run] (run-node j run (:system? m)))
286292 (text-runs (:text m)))]
@@ -357,7 +363,11 @@
357363 ;; move the compose bar under the reader every time the button came and
358364 ;; went.
359365 [:scroll {:orientation :vertical
360- :reserve 116
366+ ;; Plus the reply banner's row when there is one. That row does
367+ ;; move the compose bar, and should: it appears because the
368+ ;; reader asked to answer something, unlike the jump button,
369+ ;; which appears on its own and must not shift what is under it.
370+ :reserve (if @s/replying-to 150 116)
361371 :stick-to-bottom true
362372 :scroll-to-bottom @s/jump-tick
363373 :on-change #(reset! s/at-present? (= "end" %))}
@@ -373,6 +383,12 @@
373383 [:spacer {:size 34}]
374384 [:button {:label "↓ Jump to present" :on-click s/jump-to-present!}])]
375385 [:separator {}]
386+ ;; What the draft is answering, directly above where it is being typed.
387+ [:vbox {:key :replying}
388+ (when-let [target @s/replying-to]
389+ [:hbox {:spacing 8}
390+ [:dim-label {:label (str "↩ " (:from target) ": " (summarise target 36))}]
391+ [:button {:label "✕" :on-click s/cancel-reply!}]])]
376392 [:hbox {:spacing 8}
377393 ;; narrow enough that Send keeps its place on a phone-width row
378394 [:entry {:text @s/draft
@@ -280,7 +280,13 @@
280 :size 22}])280 :size 22}])
281 [:dim-label {:label (:from m)}]281 [:dim-label {:label (:from m)}]
282 (when-let [at (:at m)]282 (when-let [at (:at m)]
283- [:dim-label {:label (clock/clock-time at)}])])]283+ [:dim-label {:label (clock/clock-time at)}])
284+ ;; Answering is offered where the sender is named: a run of lines from
285+ ;; one person is answered as the thing it is, rather than every line
286+ ;; of it carrying a button of its own.
287+ [:vbox {:key :reply-action}
288+ (when (:id m)
289+ [:button {:label "↩" :on-click #(s/reply-to! m)}])]])]
284 [:vbox {:key :text :spacing 2}290 [:vbox {:key :text :spacing 2}
285 (map-indexed (fn [j run] (run-node j run (:system? m)))291 (map-indexed (fn [j run] (run-node j run (:system? m)))
286 (text-runs (:text m)))]292 (text-runs (:text m)))]
@@ -357,7 +363,11 @@
357 ;; move the compose bar under the reader every time the button came and363 ;; move the compose bar under the reader every time the button came and
358 ;; went.364 ;; went.
359 [:scroll {:orientation :vertical365 [:scroll {:orientation :vertical
360- :reserve 116366+ ;; Plus the reply banner's row when there is one. That row does
367+ ;; move the compose bar, and should: it appears because the
368+ ;; reader asked to answer something, unlike the jump button,
369+ ;; which appears on its own and must not shift what is under it.
370+ :reserve (if @s/replying-to 150 116)
361 :stick-to-bottom true371 :stick-to-bottom true
362 :scroll-to-bottom @s/jump-tick372 :scroll-to-bottom @s/jump-tick
363 :on-change #(reset! s/at-present? (= "end" %))}373 :on-change #(reset! s/at-present? (= "end" %))}
@@ -373,6 +383,12 @@
373 [:spacer {:size 34}]383 [:spacer {:size 34}]
374 [:button {:label "↓ Jump to present" :on-click s/jump-to-present!}])]384 [:button {:label "↓ Jump to present" :on-click s/jump-to-present!}])]
375 [:separator {}]385 [:separator {}]
386+ ;; What the draft is answering, directly above where it is being typed.
387+ [:vbox {:key :replying}
388+ (when-let [target @s/replying-to]
389+ [:hbox {:spacing 8}
390+ [:dim-label {:label (str "↩ " (:from target) ": " (summarise target 36))}]
391+ [:button {:label "✕" :on-click s/cancel-reply!}]])]
376 [:hbox {:spacing 8}392 [:hbox {:spacing 8}
377 ;; narrow enough that Send keeps its place on a phone-width row393 ;; narrow enough that Send keeps its place on a phone-width row
378 [:entry {:text @s/draft394 [:entry {:text @s/draft
modified src/frq/irc.jolt +8 -2
@@ -313,8 +313,14 @@
313313 (defn join! [conn channel] (send-line! conn (str "JOIN " channel)))
314314 (defn part! [conn channel] (send-line! conn (str "PART " channel)))
315315
316-(defn privmsg! [conn target text]
317- (send-line! conn (str "PRIVMSG " target " :" text)))
316+(defn privmsg!
317+ "Say something. With `reply-to`, say it as an answer to that message: the
318+ `+draft/reply` tag is what every other freeq client reads to thread it, and
319+ what this one draws its chips from."
320+ ([conn target text] (privmsg! conn target text nil))
321+ ([conn target text reply-to]
322+ (send-line! conn (str (when (seq reply-to) (str "@+draft/reply=" reply-to " "))
323+ "PRIVMSG " target " :" text))))
318324
319325 (defn close! [conn]
320326 ;; Written straight out rather than queued: the reader may already be gone,
@@ -313,8 +313,14 @@
313 (defn join! [conn channel] (send-line! conn (str "JOIN " channel)))313 (defn join! [conn channel] (send-line! conn (str "JOIN " channel)))
314 (defn part! [conn channel] (send-line! conn (str "PART " channel)))314 (defn part! [conn channel] (send-line! conn (str "PART " channel)))
315 315
316-(defn privmsg! [conn target text]316+(defn privmsg!
317- (send-line! conn (str "PRIVMSG " target " :" text)))317+ "Say something. With `reply-to`, say it as an answer to that message: the
318+ `+draft/reply` tag is what every other freeq client reads to thread it, and
319+ what this one draws its chips from."
320+ ([conn target text] (privmsg! conn target text nil))
321+ ([conn target text reply-to]
322+ (send-line! conn (str (when (seq reply-to) (str "@+draft/reply=" reply-to " "))
323+ "PRIVMSG " target " :" text))))
318 324
319 (defn close! [conn]325 (defn close! [conn]
320 ;; Written straight out rather than queued: the reader may already be gone,326 ;; Written straight out rather than queued: the reader may already be gone,
modified src/frq/state.jolt +15 -3
@@ -61,6 +61,13 @@
6161 (defonce channels (atom {}))
6262 (defonce current (atom nil))
6363 (defonce draft (atom ""))
64+;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
65+;; rather than as an id alone so the compose bar can say who is being answered
66+;; without going looking for them.
67+(defonce replying-to (atom nil))
68+
69+(defn reply-to! [m] (reset! replying-to (select-keys m [:id :from :text])))
70+(defn cancel-reply! [] (reset! replying-to nil))
6471
6572 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,
6673 ;; so this is a screen of its own rather than a layer over the chat.
@@ -430,12 +437,16 @@
430437
431438 (defn send-draft! []
432439 (let [text (str/trim @draft)
433- target @current]
440+ target @current
441+ reply-to @replying-to]
434442 (when (and (seq text) target)
435443 ;; Saying something is a way of asking to see it.
436444 (jump-to-present!)
437- (when-let [c @conn] (irc/privmsg! c target text))
438- (push-message! target @form-nick text)
445+ (when-let [c @conn] (irc/privmsg! c target text (:id reply-to)))
446+ ;; The local echo carries the same link, so the chip is there before the
447+ ;; server's copy of the line comes back — if it ever does.
448+ (push-message! target @form-nick text {:reply-to (:id reply-to)})
449+ (reset! replying-to nil)
439450 (reset! draft ""))))
440451
441452 (defn channel-list
@@ -496,6 +507,7 @@
496507 (defonce jump-to (atom nil))
497508
498509
510+
499511 (defn last-preview [buffer]
500512 (if-let [m (last (:messages buffer))]
501513 (str (:from m) ": " (:text m))
@@ -61,6 +61,13 @@
61 (defonce channels (atom {}))61 (defonce channels (atom {}))
62 (defonce current (atom nil))62 (defonce current (atom nil))
63 (defonce draft (atom ""))63 (defonce draft (atom ""))
64+;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
65+;; rather than as an id alone so the compose bar can say who is being answered
66+;; without going looking for them.
67+(defonce replying-to (atom nil))
68+
69+(defn reply-to! [m] (reset! replying-to (select-keys m [:id :from :text])))
70+(defn cancel-reply! [] (reset! replying-to nil))
64 71
65 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,72 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,
66 ;; so this is a screen of its own rather than a layer over the chat.73 ;; so this is a screen of its own rather than a layer over the chat.
@@ -430,12 +437,16 @@
430 437
431 (defn send-draft! []438 (defn send-draft! []
432 (let [text (str/trim @draft)439 (let [text (str/trim @draft)
433- target @current]440+ target @current
441+ reply-to @replying-to]
434 (when (and (seq text) target)442 (when (and (seq text) target)
435 ;; Saying something is a way of asking to see it.443 ;; Saying something is a way of asking to see it.
436 (jump-to-present!)444 (jump-to-present!)
437- (when-let [c @conn] (irc/privmsg! c target text))445+ (when-let [c @conn] (irc/privmsg! c target text (:id reply-to)))
438- (push-message! target @form-nick text)446+ ;; The local echo carries the same link, so the chip is there before the
447+ ;; server's copy of the line comes back — if it ever does.
448+ (push-message! target @form-nick text {:reply-to (:id reply-to)})
449+ (reset! replying-to nil)
439 (reset! draft ""))))450 (reset! draft ""))))
440 451
441 (defn channel-list452 (defn channel-list
@@ -496,6 +507,7 @@
496 (defonce jump-to (atom nil))507 (defonce jump-to (atom nil))
497 508
498 509
510+
499 (defn last-preview [buffer]511 (defn last-preview [buffer]
500 (if-let [m (last (:messages buffer))]512 (if-let [m (last (:messages buffer))]
501 (str (:from m) ": " (:text m))513 (str (:from m) ": " (:text m))