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>
7e53cb4 parent: 78e7c29 modified
src/frq/app.jolt +8 -1 | @@ -321,10 +321,17 @@ | ||
| 321 | 321 | [error-note] |
| 322 | 322 | ;; :reserve leaves room for the separator and compose bar below, so the |
| 323 | 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 | 327 | (if (seq (:messages buffer)) |
| 326 | 328 | (message-rows (:messages buffer)) |
| 327 | 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 | 335 | [:separator {}] |
| 329 | 336 | [:hbox {:spacing 8} |
| 330 | 337 | ;; 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 the | 322 | ;; :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 row | 337 | ;; narrow enough that Send keeps its place on a phone-width row |
modified
src/frq/state.jolt +10 -0 | @@ -66,6 +66,14 @@ | ||
| 66 | 66 | ;; so this is a screen of its own rather than a layer over the chat. |
| 67 | 67 | (defonce lightbox (atom nil)) ; {:path :url} |
| 68 | 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 | 77 | ;; A counter rather than a clock: the list only needs their order, and a |
| 70 | 78 | ;; monotonic tick cannot be surprised by the system time moving. |
| 71 | 79 | (defonce access-tick (atom 0)) |
| @@ -411,6 +419,8 @@ | ||
| 411 | 419 | (let [text (str/trim @draft) |
| 412 | 420 | target @current] |
| 413 | 421 | (when (and (seq text) target) |
| 422 | + ;; Saying something is a way of asking to see it. | |
| 423 | + (jump-to-present!) | |
| 414 | 424 | (when-let [c @conn] (irc/privmsg! c target text)) |
| 415 | 425 | (push-message! target @form-nick text) |
| 416 | 426 | (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 a | 77 | ;; 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 "")))) |