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

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>
nandi committed 2026-08-30T12:16:36-07:00 Browse files
ef6123f parent: 7bc82bd
modified src/frq/app.jolt +59 -46
@@ -237,9 +237,17 @@
237237 (if-let [target (s/message-by-id channel id)]
238238 [:button {:label (str "↩ " (:from target) ": " (summarise target 48))
239239 :on-click #(do (reset! s/jump-to id)
240+ (reset! s/highlight id)
240241 ;; Off again once the frame that scrolled has
241242 ;; 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)))))}]
243251 ;; The message it answers is older than this buffer goes.
244252 [:dim-label {:label "↩ replying to an earlier message"}])])
245253
@@ -254,55 +262,60 @@
254262 (let [same-sender? (and prev
255263 (not (:system? m))
256264 (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)))]
258270 [:vbox {:key i :spacing 2 :margin 0
259271 ;; The jump target is what a "go to message" click scrolls to.
260272 :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.
277312 (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})}]))))]]]))
306319
307320 (defn- day-separator [day-key label]
308321 [: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 has241 ;; 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 prev262 (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 0270 [: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 every276+ [reply-chip @s/current reply-to])]
265- ;; line is what made a busy channel look like a list of headers. The time277+ ;; 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 once278+ ;; 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: their282+ (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 the285+ [:hbox {:spacing 6}
274- ;; fetch lands, and for the guests who have no profile at all, which286+ ;; 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 @@
506506 ;; view there and take scrolling away from the reader.
507507 (defonce jump-to (atom nil))
508508
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+
509514
510515
511516 (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]