nandi/frqpublic Fork 0
7e53cb4
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.

Offer the way back to the present

A chat that follows the newest line is no use if you cannot leave it, and
leaving it is no use if you cannot get back. The scroll now says when the view
has left the end, and a button appears directly above the compose bar while it
has — next to the thing that would put you back there anyway.

Sending counts as asking to see it: the draft going out jumps to the present
too, since a message you cannot see arriving is a strange thing to have sent.

The button is inside a wrapper of its own with a key, like every other
conditional child here: one that renders nil when it has nothing to say shifts
every sibling after it, and the reconciler matches by position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T11:41:57-07:00 Browse files
7e53cb4 parent: 78e7c29
modified src/frq/app.jolt +8 -1
@@ -321,10 +321,17 @@
321321 [error-note]
322322 ;; :reserve leaves room for the separator and compose bar below, so the
323323 ;; list fills whatever the window has and no more.
324- [:scroll {:orientation :vertical :reserve 72 :stick-to-bottom true}
324+ [:scroll {:orientation :vertical :reserve 72 :stick-to-bottom true
325+ :scroll-to-bottom @s/jump-tick
326+ :on-change #(reset! s/at-present? (= "end" %))}
325327 (if (seq (:messages buffer))
326328 (message-rows (:messages buffer))
327329 [:dim-label {:label "Nothing here yet."}])]
330+ ;; Only while it is needed, and directly above the compose bar: the way
331+ ;; back to the present belongs next to the thing that puts you there.
332+ [:vbox {:key :jump}
333+ (when-not @s/at-present?
334+ [:button {:label "↓ Jump to present" :on-click s/jump-to-present!}])]
328335 [:separator {}]
329336 [:hbox {:spacing 8}
330337 ;; narrow enough that Send keeps its place on a phone-width row
@@ -321,10 +321,17 @@
321 [error-note]321 [error-note]
322 ;; :reserve leaves room for the separator and compose bar below, so the322 ;; :reserve leaves room for the separator and compose bar below, so the
323 ;; list fills whatever the window has and no more.323 ;; list fills whatever the window has and no more.
324- [:scroll {:orientation :vertical :reserve 72 :stick-to-bottom true}324+ [:scroll {:orientation :vertical :reserve 72 :stick-to-bottom true
325+ :scroll-to-bottom @s/jump-tick
326+ :on-change #(reset! s/at-present? (= "end" %))}
325 (if (seq (:messages buffer))327 (if (seq (:messages buffer))
326 (message-rows (:messages buffer))328 (message-rows (:messages buffer))
327 [:dim-label {:label "Nothing here yet."}])]329 [:dim-label {:label "Nothing here yet."}])]
330+ ;; Only while it is needed, and directly above the compose bar: the way
331+ ;; back to the present belongs next to the thing that puts you there.
332+ [:vbox {:key :jump}
333+ (when-not @s/at-present?
334+ [:button {:label "↓ Jump to present" :on-click s/jump-to-present!}])]
328 [:separator {}]335 [:separator {}]
329 [:hbox {:spacing 8}336 [:hbox {:spacing 8}
330 ;; narrow enough that Send keeps its place on a phone-width row337 ;; narrow enough that Send keeps its place on a phone-width row
modified src/frq/state.jolt +10 -0
@@ -66,6 +66,14 @@
6666 ;; so this is a screen of its own rather than a layer over the chat.
6767 (defonce lightbox (atom nil)) ; {:path :url}
6868 (defonce join-input (atom ""))
69+
70+;; Whether the chat view is showing the newest line, and a counter the view
71+;; watches to be told to go back to it. A counter rather than a flag: a flag
72+;; would need clearing, and there is no frame in which to clear it.
73+(defonce at-present? (atom true))
74+(defonce jump-tick (atom 0))
75+
76+(defn jump-to-present! [] (swap! jump-tick inc))
6977 ;; A counter rather than a clock: the list only needs their order, and a
7078 ;; monotonic tick cannot be surprised by the system time moving.
7179 (defonce access-tick (atom 0))
@@ -411,6 +419,8 @@
411419 (let [text (str/trim @draft)
412420 target @current]
413421 (when (and (seq text) target)
422+ ;; Saying something is a way of asking to see it.
423+ (jump-to-present!)
414424 (when-let [c @conn] (irc/privmsg! c target text))
415425 (push-message! target @form-nick text)
416426 (reset! draft ""))))
@@ -66,6 +66,14 @@
66 ;; so this is a screen of its own rather than a layer over the chat.66 ;; so this is a screen of its own rather than a layer over the chat.
67 (defonce lightbox (atom nil)) ; {:path :url}67 (defonce lightbox (atom nil)) ; {:path :url}
68 (defonce join-input (atom ""))68 (defonce join-input (atom ""))
69+
70+;; Whether the chat view is showing the newest line, and a counter the view
71+;; watches to be told to go back to it. A counter rather than a flag: a flag
72+;; would need clearing, and there is no frame in which to clear it.
73+(defonce at-present? (atom true))
74+(defonce jump-tick (atom 0))
75+
76+(defn jump-to-present! [] (swap! jump-tick inc))
69 ;; A counter rather than a clock: the list only needs their order, and a77 ;; A counter rather than a clock: the list only needs their order, and a
70 ;; monotonic tick cannot be surprised by the system time moving.78 ;; monotonic tick cannot be surprised by the system time moving.
71 (defonce access-tick (atom 0))79 (defonce access-tick (atom 0))
@@ -411,6 +419,8 @@
411 (let [text (str/trim @draft)419 (let [text (str/trim @draft)
412 target @current]420 target @current]
413 (when (and (seq text) target)421 (when (and (seq text) target)
422+ ;; Saying something is a way of asking to see it.
423+ (jump-to-present!)
414 (when-let [c @conn] (irc/privmsg! c target text))424 (when-let [c @conn] (irc/privmsg! c target text))
415 (push-message! target @form-nick text)425 (push-message! target @form-nick text)
416 (reset! draft ""))))426 (reset! draft ""))))