Show the reader which message a jump landed on
Arriving at a screenful of messages says nothing about which one was asked for. The message a reply chip sent you to now wears a surface of its own for two seconds, so it answers for itself rather than leaving the reader to count rows. The scroll target could not do this itself: it is cleared the frame after it scrolls, since one that stayed set would pin the view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ef6123f parent: 7bc82bd modified
src/frq/app.jolt +59 -46 | @@ -237,9 +237,17 @@ | ||
| 237 | 237 | (if-let [target (s/message-by-id channel id)] |
| 238 | 238 | [:button {:label (str "↩ " (:from target) ": " (summarise target 48)) |
| 239 | 239 | :on-click #(do (reset! s/jump-to id) |
| 240 | + (reset! s/highlight id) | |
| 240 | 241 | ;; Off again once the frame that scrolled has |
| 241 | 242 | ;; been painted, so the reader keeps the view. |
| 242 | - (vidya/after! 120 (fn [] (reset! s/jump-to nil))))}] | |
| 243 | + (vidya/after! 120 (fn [] (reset! s/jump-to nil))) | |
| 244 | + ;; The highlight stays long enough to be read, | |
| 245 | + ;; and only clears itself: a later jump elsewhere | |
| 246 | + ;; owns the highlight from then on. | |
| 247 | + (vidya/after! 2000 | |
| 248 | + (fn [] | |
| 249 | + (when (= id @s/highlight) | |
| 250 | + (reset! s/highlight nil)))))}] | |
| 243 | 251 | ;; The message it answers is older than this buffer goes. |
| 244 | 252 | [:dim-label {:label "↩ replying to an earlier message"}])]) |
| 245 | 253 | |
| @@ -254,55 +262,60 @@ | ||
| 254 | 262 | (let [same-sender? (and prev |
| 255 | 263 | (not (:system? m)) |
| 256 | 264 | (not (:system? prev)) |
| 257 | - (= (:from prev) (:from m)))] | |
| 265 | + (= (:from prev) (:from m))) | |
| 266 | + ;; What a jump landed on wears a surface of its own for a moment, so | |
| 267 | + ;; the answer to "which one was I sent to" is on the screen rather | |
| 268 | + ;; than in the reader's count of rows. | |
| 269 | + highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))] | |
| 258 | 270 | [:vbox {:key i :spacing 2 :margin 0 |
| 259 | 271 | ;; The jump target is what a "go to message" click scrolls to. |
| 260 | 272 | :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))} |
| 261 | - [:vbox {:key :reply-chip} | |
| 262 | - (when-let [reply-to (:reply-to m)] | |
| 263 | - [reply-chip @s/current reply-to])] | |
| 264 | - ;; A run from one person reads as one block; repeating the nick on every | |
| 265 | - ;; line is what made a busy channel look like a list of headers. The time | |
| 266 | - ;; rides on that same line, for the same reason: once per run, not once | |
| 267 | - ;; per line. | |
| 268 | - [:vbox {:key :who} | |
| 269 | - (when-not (or same-sender? (:system? m)) | |
| 270 | - ;; Everything that is about the person rather than the line: their | |
| 271 | - ;; picture, their name, and when they started saying this. | |
| 272 | - [:hbox {:spacing 6} | |
| 273 | - ;; Always an avatar, picture or not: the initial stands in until the | |
| 274 | - ;; fetch lands, and for the guests who have no profile at all, which | |
| 275 | - ;; is what keeps the column of faces straight down the left. | |
| 276 | - ;; Reading the tick subscribes this row to a fetch finishing. | |
| 273 | + [(if highlit? :card :vbox) {:key :body :spacing 2 :margin 0} | |
| 274 | + [:vbox {:key :reply-chip} | |
| 275 | + (when-let [reply-to (:reply-to m)] | |
| 276 | + [reply-chip @s/current reply-to])] | |
| 277 | + ;; A run from one person reads as one block; repeating the nick on every | |
| 278 | + ;; line is what made a busy channel look like a list of headers. The time | |
| 279 | + ;; rides on that same line, for the same reason: once per run, not once | |
| 280 | + ;; per line. | |
| 281 | + [:vbox {:key :who} | |
| 282 | + (when-not (or same-sender? (:system? m)) | |
| 283 | + ;; Everything that is about the person rather than the line: their | |
| 284 | + ;; picture, their name, and when they started saying this. | |
| 285 | + [:hbox {:spacing 6} | |
| 286 | + ;; Always an avatar, picture or not: the initial stands in until the | |
| 287 | + ;; fetch lands, and for the guests who have no profile at all, which | |
| 288 | + ;; is what keeps the column of faces straight down the left. | |
| 289 | + ;; Reading the tick subscribes this row to a fetch finishing. | |
| 290 | + (let [_ @s/media-tick] | |
| 291 | + [:avatar {:label (:from m) | |
| 292 | + :src (or (avatars/path-when-ready (:actor m)) "") | |
| 293 | + :size 22}]) | |
| 294 | + [:dim-label {:label (:from m)}] | |
| 295 | + (when-let [at (:at m)] | |
| 296 | + [:dim-label {:label (clock/clock-time at)}]) | |
| 297 | + ;; Answering is offered where the sender is named — a run of lines | |
| 298 | + ;; from one person is answered as the thing it is — and against the | |
| 299 | + ;; right edge, out of the way of the name and the time, which are what | |
| 300 | + ;; the eye is going down the column for. | |
| 301 | + [:hbox {:key :reply-action :align :end} | |
| 302 | + (when (:id m) | |
| 303 | + [:button {:label "↩" :on-click #(s/reply-to! m)}])]])] | |
| 304 | + [:vbox {:key :text :spacing 2} | |
| 305 | + (map-indexed (fn [j run] (run-node j run (:system? m))) | |
| 306 | + (text-runs (:text m)))] | |
| 307 | + ;; Pictures under the line that linked them. The link stays: it is what a | |
| 308 | + ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | |
| 309 | + [:vbox {:key :images :spacing 4} | |
| 310 | + (when (seq (:images m)) | |
| 311 | + ;; Reading the tick is what subscribes this row to a fetch finishing. | |
| 277 | 312 | (let [_ @s/media-tick] |
| 278 | - [:avatar {:label (:from m) | |
| 279 | - :src (or (avatars/path-when-ready (:actor m)) "") | |
| 280 | - :size 22}]) | |
| 281 | - [:dim-label {:label (:from m)}] | |
| 282 | - (when-let [at (:at m)] | |
| 283 | - [:dim-label {:label (clock/clock-time at)}]) | |
| 284 | - ;; Answering is offered where the sender is named — a run of lines | |
| 285 | - ;; from one person is answered as the thing it is — and against the | |
| 286 | - ;; right edge, out of the way of the name and the time, which are what | |
| 287 | - ;; the eye is going down the column for. | |
| 288 | - [:hbox {:key :reply-action :align :end} | |
| 289 | - (when (:id m) | |
| 290 | - [:button {:label "↩" :on-click #(s/reply-to! m)}])]])] | |
| 291 | - [:vbox {:key :text :spacing 2} | |
| 292 | - (map-indexed (fn [j run] (run-node j run (:system? m))) | |
| 293 | - (text-runs (:text m)))] | |
| 294 | - ;; Pictures under the line that linked them. The link stays: it is what a | |
| 295 | - ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | |
| 296 | - [:vbox {:key :images :spacing 4} | |
| 297 | - (when (seq (:images m)) | |
| 298 | - ;; Reading the tick is what subscribes this row to a fetch finishing. | |
| 299 | - (let [_ @s/media-tick] | |
| 300 | - (for [url (:images m)] | |
| 301 | - (when-let [path (media/path-when-ready url)] | |
| 302 | - [:image {:key url | |
| 303 | - :src path | |
| 304 | - :max-height 260 | |
| 305 | - :on-click #(reset! s/lightbox {:path path :url url})}]))))]])) | |
| 313 | + (for [url (:images m)] | |
| 314 | + (when-let [path (media/path-when-ready url)] | |
| 315 | + [:image {:key url | |
| 316 | + :src path | |
| 317 | + :max-height 260 | |
| 318 | + :on-click #(reset! s/lightbox {:path path :url url})}]))))]]])) | |
| 306 | 319 | |
| 307 | 320 | (defn- day-separator [day-key label] |
| 308 | 321 | [:vbox {:key day-key :spacing 4 :margin 0} |
| @@ -237,9 +237,17 @@ | |||
| 237 | (if-let [target (s/message-by-id channel id)] | 237 | (if-let [target (s/message-by-id channel id)] |
| 238 | [:button {:label (str "↩ " (:from target) ": " (summarise target 48)) | 238 | [:button {:label (str "↩ " (:from target) ": " (summarise target 48)) |
| 239 | :on-click #(do (reset! s/jump-to id) | 239 | :on-click #(do (reset! s/jump-to id) |
| 240 | + (reset! s/highlight id) | ||
| 240 | ;; Off again once the frame that scrolled has | 241 | ;; Off again once the frame that scrolled has |
| 241 | ;; been painted, so the reader keeps the view. | 242 | ;; been painted, so the reader keeps the view. |
| 242 | - (vidya/after! 120 (fn [] (reset! s/jump-to nil))))}] | 243 | + (vidya/after! 120 (fn [] (reset! s/jump-to nil))) |
| 244 | + ;; The highlight stays long enough to be read, | ||
| 245 | + ;; and only clears itself: a later jump elsewhere | ||
| 246 | + ;; owns the highlight from then on. | ||
| 247 | + (vidya/after! 2000 | ||
| 248 | + (fn [] | ||
| 249 | + (when (= id @s/highlight) | ||
| 250 | + (reset! s/highlight nil)))))}] | ||
| 243 | ;; The message it answers is older than this buffer goes. | 251 | ;; The message it answers is older than this buffer goes. |
| 244 | [:dim-label {:label "↩ replying to an earlier message"}])]) | 252 | [:dim-label {:label "↩ replying to an earlier message"}])]) |
| 245 | 253 | ||
| @@ -254,55 +262,60 @@ | |||
| 254 | (let [same-sender? (and prev | 262 | (let [same-sender? (and prev |
| 255 | (not (:system? m)) | 263 | (not (:system? m)) |
| 256 | (not (:system? prev)) | 264 | (not (:system? prev)) |
| 257 | - (= (:from prev) (:from m)))] | 265 | + (= (:from prev) (:from m))) |
| 266 | + ;; What a jump landed on wears a surface of its own for a moment, so | ||
| 267 | + ;; the answer to "which one was I sent to" is on the screen rather | ||
| 268 | + ;; than in the reader's count of rows. | ||
| 269 | + highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))] | ||
| 258 | [:vbox {:key i :spacing 2 :margin 0 | 270 | [:vbox {:key i :spacing 2 :margin 0 |
| 259 | ;; The jump target is what a "go to message" click scrolls to. | 271 | ;; The jump target is what a "go to message" click scrolls to. |
| 260 | :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))} | 272 | :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))} |
| 261 | - [:vbox {:key :reply-chip} | 273 | + [(if highlit? :card :vbox) {:key :body :spacing 2 :margin 0} |
| 262 | - (when-let [reply-to (:reply-to m)] | 274 | + [:vbox {:key :reply-chip} |
| 263 | - [reply-chip @s/current reply-to])] | 275 | + (when-let [reply-to (:reply-to m)] |
| 264 | - ;; A run from one person reads as one block; repeating the nick on every | 276 | + [reply-chip @s/current reply-to])] |
| 265 | - ;; line is what made a busy channel look like a list of headers. The time | 277 | + ;; A run from one person reads as one block; repeating the nick on every |
| 266 | - ;; rides on that same line, for the same reason: once per run, not once | 278 | + ;; line is what made a busy channel look like a list of headers. The time |
| 267 | - ;; per line. | 279 | + ;; rides on that same line, for the same reason: once per run, not once |
| 268 | - [:vbox {:key :who} | 280 | + ;; per line. |
| 269 | - (when-not (or same-sender? (:system? m)) | 281 | + [:vbox {:key :who} |
| 270 | - ;; Everything that is about the person rather than the line: their | 282 | + (when-not (or same-sender? (:system? m)) |
| 271 | - ;; picture, their name, and when they started saying this. | 283 | + ;; Everything that is about the person rather than the line: their |
| 272 | - [:hbox {:spacing 6} | 284 | + ;; picture, their name, and when they started saying this. |
| 273 | - ;; Always an avatar, picture or not: the initial stands in until the | 285 | + [:hbox {:spacing 6} |
| 274 | - ;; fetch lands, and for the guests who have no profile at all, which | 286 | + ;; Always an avatar, picture or not: the initial stands in until the |
| 275 | - ;; is what keeps the column of faces straight down the left. | 287 | + ;; fetch lands, and for the guests who have no profile at all, which |
| 276 | - ;; Reading the tick subscribes this row to a fetch finishing. | 288 | + ;; is what keeps the column of faces straight down the left. |
| 289 | + ;; Reading the tick subscribes this row to a fetch finishing. | ||
| 290 | + (let [_ @s/media-tick] | ||
| 291 | + [:avatar {:label (:from m) | ||
| 292 | + :src (or (avatars/path-when-ready (:actor m)) "") | ||
| 293 | + :size 22}]) | ||
| 294 | + [:dim-label {:label (:from m)}] | ||
| 295 | + (when-let [at (:at m)] | ||
| 296 | + [:dim-label {:label (clock/clock-time at)}]) | ||
| 297 | + ;; Answering is offered where the sender is named — a run of lines | ||
| 298 | + ;; from one person is answered as the thing it is — and against the | ||
| 299 | + ;; right edge, out of the way of the name and the time, which are what | ||
| 300 | + ;; the eye is going down the column for. | ||
| 301 | + [:hbox {:key :reply-action :align :end} | ||
| 302 | + (when (:id m) | ||
| 303 | + [:button {:label "↩" :on-click #(s/reply-to! m)}])]])] | ||
| 304 | + [:vbox {:key :text :spacing 2} | ||
| 305 | + (map-indexed (fn [j run] (run-node j run (:system? m))) | ||
| 306 | + (text-runs (:text m)))] | ||
| 307 | + ;; Pictures under the line that linked them. The link stays: it is what a | ||
| 308 | + ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | ||
| 309 | + [:vbox {:key :images :spacing 4} | ||
| 310 | + (when (seq (:images m)) | ||
| 311 | + ;; Reading the tick is what subscribes this row to a fetch finishing. | ||
| 277 | (let [_ @s/media-tick] | 312 | (let [_ @s/media-tick] |
| 278 | - [:avatar {:label (:from m) | 313 | + (for [url (:images m)] |
| 279 | - :src (or (avatars/path-when-ready (:actor m)) "") | 314 | + (when-let [path (media/path-when-ready url)] |
| 280 | - :size 22}]) | 315 | + [:image {:key url |
| 281 | - [:dim-label {:label (:from m)}] | 316 | + :src path |
| 282 | - (when-let [at (:at m)] | 317 | + :max-height 260 |
| 283 | - [:dim-label {:label (clock/clock-time at)}]) | 318 | + :on-click #(reset! s/lightbox {:path path :url url})}]))))]]])) |
| 284 | - ;; Answering is offered where the sender is named — a run of lines | ||
| 285 | - ;; from one person is answered as the thing it is — and against the | ||
| 286 | - ;; right edge, out of the way of the name and the time, which are what | ||
| 287 | - ;; the eye is going down the column for. | ||
| 288 | - [:hbox {:key :reply-action :align :end} | ||
| 289 | - (when (:id m) | ||
| 290 | - [:button {:label "↩" :on-click #(s/reply-to! m)}])]])] | ||
| 291 | - [:vbox {:key :text :spacing 2} | ||
| 292 | - (map-indexed (fn [j run] (run-node j run (:system? m))) | ||
| 293 | - (text-runs (:text m)))] | ||
| 294 | - ;; Pictures under the line that linked them. The link stays: it is what a | ||
| 295 | - ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | ||
| 296 | - [:vbox {:key :images :spacing 4} | ||
| 297 | - (when (seq (:images m)) | ||
| 298 | - ;; Reading the tick is what subscribes this row to a fetch finishing. | ||
| 299 | - (let [_ @s/media-tick] | ||
| 300 | - (for [url (:images m)] | ||
| 301 | - (when-let [path (media/path-when-ready url)] | ||
| 302 | - [:image {:key url | ||
| 303 | - :src path | ||
| 304 | - :max-height 260 | ||
| 305 | - :on-click #(reset! s/lightbox {:path path :url url})}]))))]])) | ||
| 306 | 319 | ||
| 307 | (defn- day-separator [day-key label] | 320 | (defn- day-separator [day-key label] |
| 308 | [:vbox {:key day-key :spacing 4 :margin 0} | 321 | [:vbox {:key day-key :spacing 4 :margin 0} |
modified
src/frq/state.jolt +5 -0 | @@ -506,6 +506,11 @@ | ||
| 506 | 506 | ;; view there and take scrolling away from the reader. |
| 507 | 507 | (defonce jump-to (atom nil)) |
| 508 | 508 | |
| 509 | +;; The message a jump has just landed on. It outlives the scroll: arriving at a | |
| 510 | +;; screenful of messages says nothing about which one was asked for, so the one | |
| 511 | +;; that was answers for itself until the reader has had time to see it. | |
| 512 | +(defonce highlight (atom nil)) | |
| 513 | + | |
| 509 | 514 | |
| 510 | 515 | |
| 511 | 516 | (defn last-preview [buffer] |
| @@ -506,6 +506,11 @@ | |||
| 506 | ;; view there and take scrolling away from the reader. | 506 | ;; view there and take scrolling away from the reader. |
| 507 | (defonce jump-to (atom nil)) | 507 | (defonce jump-to (atom nil)) |
| 508 | 508 | ||
| 509 | +;; The message a jump has just landed on. It outlives the scroll: arriving at a | ||
| 510 | +;; screenful of messages says nothing about which one was asked for, so the one | ||
| 511 | +;; that was answers for itself until the reader has had time to see it. | ||
| 512 | +(defonce highlight (atom nil)) | ||
| 513 | + | ||
| 509 | 514 | ||
| 510 | 515 | ||
| 511 | (defn last-preview [buffer] | 516 | (defn last-preview [buffer] |