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

Give every message its own heading, and the chips a place on it

The actions were full-size buttons at the right of the text row, which took
width off every line under them and wrapped messages that had the room to sit
on one. They are reaction-sized chips now, riding the sender's row against its
right edge, so the words keep the whole column.

Which means every line needs that row: a run collapsed to one heading has
nowhere to put the actions of its fourth line, and no name on the line quoted
back when you answer it.

And reactions go last, under the picture where there is one — pills between
the words and the image they introduce read as reactions to the words alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T22:17:45-07:00 Browse files
3043efe parent: 3ba502d
modified src/frq/app.jolt +73 -68
@@ -303,13 +303,31 @@
303303 said, not a second thing said."
304304 14)
305305
306-(defn- react-button
307- "The way to put an emoji on a message, beside the way to answer it.
306+(defn- action-chips
307+ "Answering and reacting, on the sender's row above the message.
308308
309- Both are things done *to* a message rather than parts of it, so they share
310- the sender's line and the right edge. It opens the picker on this message."
309+ Both are things done *to* a message rather than parts of it, so they ride
310+ the sender's row against its right edge, at the size a reaction is. In the
311+ line with the text they took width off every line under them and wrapped a
312+ message that had the room to sit on one.
313+
314+ Chips rather than buttons: a button is sized for a label and these carry a
315+ glyph, which made two lozenges taller than the line they belonged to.
316+ `:reaction` with no count is the same pill the picker offers, so what you
317+ press to react and what appears once you have look like one family.
318+
319+ A row laid out from the right lays its first child furthest right, so
320+ reacting comes first in the source and this reads ↩️ then 🙂 on screen."
311321 [channel m]
312- [:button {:label "☺" :on-click #(s/open-picker! channel m)}])
322+ [:hbox {:key :actions :align :end :spacing 4}
323+ [:reaction {:key :react
324+ :emoji "🙂"
325+ :size pill-size
326+ :on-click #(s/open-picker! channel m)}]
327+ [:reaction {:key :reply
328+ :emoji "↩️"
329+ :size pill-size
330+ :on-click #(s/reply-to! m)}]])
313331
314332 (defn- reaction-row
315333 "What people have put on a message, under it.
@@ -437,18 +455,18 @@
437455 [:dim-label {:label "Click for the full profile"}]]))
438456
439457 (defn message-row
440- "One message. `prev` is the message above it, which decides whether this one
441- repeats the sender.
458+ "One message: who said it, when, what you can do to it, and the words.
459+
460+ Every line names its sender, rather than the first of a run only. A run
461+ collapsed to one heading reads well until you answer the fourth line of it,
462+ and then the line quoted back has no name on it; and the actions live on the
463+ sender's row, which a headerless line has nowhere to put.
442464
443465 Sender above the text, not beside it: a wrapping label in a horizontal row
444466 lays out against the row's width rather than the column's, so one long URL
445467 drags every line that follows it off the left edge."
446- [i prev m]
447- (let [same-sender? (and prev
448- (not (:system? m))
449- (not (:system? prev))
450- (= (:from prev) (:from m)))
451- ;; What a jump landed on wears a surface of its own for a moment, so
468+ [i m]
469+ (let [;; What a jump landed on wears a surface of its own for a moment, so
452470 ;; the answer to "which one was I sent to" is on the screen rather
453471 ;; than in the reader's count of rows.
454472 highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))]
@@ -457,22 +475,21 @@
457475 ;; list's scrollbar rides it too — without this the ↩ is what the
458476 ;; scrollbar is drawn over.
459477 :margin-right 10
460- ;; Space above a new speaker, none inside their run: the gap is
461- ;; what separates one person's block from the next, and evenly
462- ;; spaced lines are what made the column read as one dense list.
463- :margin-top (if same-sender? 0 10)
478+ ;; One gap between messages: every line carries its own heading
479+ ;; now, so there are no runs to hold together and nothing for a
480+ ;; closed-up line to belong to.
481+ :margin-top 10
464482 ;; The jump target is what a "go to message" click scrolls to.
465483 :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))}
466484 ;; Likewise a key each: the surface a jump leaves behind is a different
467485 ;; node from the plain column, not the same node wearing another tag.
468486 [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain)
469487 :spacing 2 :margin 0}
470- ;; A run from one person reads as one block; repeating the nick on every
471- ;; line is what made a busy channel look like a list of headers. The time
472- ;; rides on that same line, for the same reason: once per run, not once
473- ;; per line.
488+ ;; Who, when, and what can be done about it, on one line above the
489+ ;; words: the name and the time say where the message came from, and the
490+ ;; chips at the far end are the two answers to it.
474491 [:vbox {:key :who}
475- (when-not (or same-sender? (:system? m))
492+ (when-not (:system? m)
476493 ;; Everything that is about the person rather than the line: their
477494 ;; picture, their name, and when they started saying this.
478495 [:hbox {:spacing 6}
@@ -505,52 +522,32 @@
505522 [:label {:label (:from m)}]
506523 (when-let [at (:at m)]
507524 [:dim-label {:label (clock/clock-time at)}])
508- ])]
509- ;; Every message can be answered, not only the one that opened a run —
510- ;; laid out from the right so the action sits at the edge and the text
511- ;; takes the width that is left. The text keeps a column of its own:
512- ;; a wrapping label directly in a row wraps against the row.
525+ ;; And the two things you can do to the message, at the far end of
526+ ;; its heading: a nested row laid out from the right takes what is
527+ ;; left of the width and puts the chips against the edge of it, so
528+ ;; the words below keep the whole column.
529+ ;;
530+ ;; A key of its own for each branch, rather than one key whose node
531+ ;; changes tag under it: a line this client sent has no msgid until
532+ ;; the server echoes it back, and swapping a `:spacer` for a chip in
533+ ;; place is what left the row rebuilt in the wrong order.
534+ (if (:id m)
535+ [action-chips {:key :actions} @s/current m]
536+ [:spacer {:key :actions-gap :size 0}])])]
513537 ;; A little air under the sender's row: the name and face are a heading
514538 ;; for what follows, and a heading that touches its text is not one.
515- [:hbox {:key :line :align :end :spacing 6 :margin-top 10}
516- ;; Answering and reacting sit together because they are the same kind
517- ;; of thing: what you do to a message rather than what it says. Both
518- ;; are in the row itself rather than in a box of their own — a box with
519- ;; no width takes the rest of the row and puts the text on a line below
520- ;; it, and one with a width takes the column's whole height. Reacting
521- ;; comes first in the source because a row laid out from the right lays
522- ;; its first child furthest right: on screen this reads ↩ then ☺.
523- ;; A key of its own for each branch, rather than one key whose node
524- ;; changes tag under it: a line this client sent has no msgid until the
525- ;; server echoes it back, and swapping a `:spacer` for a `:button` in
526- ;; place is what left the row rebuilt in the wrong order — the actions
527- ;; ended up after the text, and every row beside it laid out from an
528- ;; edge that was no longer the row's. Two keys make it a removal and an
529- ;; addition, which is what it is.
530- (if (:id m)
531- [react-button {:key :react-button} @s/current m]
532- [:spacer {:key :react-gap :size 0}])
533- (if (:id m)
534- [:button {:key :reply-button :label "↩" :on-click #(s/reply-to! m)}]
535- [:spacer {:key :reply-gap :size 0}])
536- ;; The text column carries the pills as well as the words: put them
537- ;; outside this row and they sit below it, and the row is as tall as
538- ;; the buttons at its right edge — which is half a line of air between
539- ;; a message and the reactions on it, coming from nothing anyone asked
540- ;; for.
541- [:vbox {:key :text :spacing 2}
542- ;; What this answers, in the column the answer itself is in: the chip
543- ;; is a quote of a line, so it wraps to the width a line has. Outside
544- ;; this column it wrapped to the whole row instead — wider than any
545- ;; message, and out to the window's edge on a narrow screen.
546- [:vbox {:key :reply-chip}
547- (when-let [reply-to (:reply-to m)]
548- [reply-chip @s/current reply-to])]
549- (map-indexed (fn [j run] (run-node j run (:system? m)))
550- (text-runs (:text m)))
551- [:vbox {:key :reactions-row :margin-top 6}
552- (when (and (:id m) (not (:system? m)) (seq (:reactions m)))
553- [reaction-row @s/current m])]]]
539+ ;; The text keeps a column of its own: a wrapping label directly in a
540+ ;; row wraps against the row.
541+ [:vbox {:key :text :spacing 2 :margin-top 4}
542+ ;; What this answers, in the column the answer itself is in: the chip
543+ ;; is a quote of a line, so it wraps to the width a line has. Outside
544+ ;; this column it wrapped to the whole row instead — wider than any
545+ ;; message, and out to the window's edge on a narrow screen.
546+ [:vbox {:key :reply-chip}
547+ (when-let [reply-to (:reply-to m)]
548+ [reply-chip @s/current reply-to])]
549+ (map-indexed (fn [j run] (run-node j run (:system? m)))
550+ (text-runs (:text m)))]
554551 ;; And the picker, when this is the message it was opened on: under the
555552 ;; line it is about, where the reader is already looking.
556553 ;;
@@ -571,7 +568,15 @@
571568 [:image {:key url
572569 :src path
573570 :max-height 260
574- :on-click #(reset! s/lightbox {:path path :url url})}]))))]]]))
571+ :on-click #(reset! s/lightbox {:path path :url url})}]))))]
572+ ;; Reactions go last, under whatever the message turned out to be: a
573+ ;; line with a picture on it is the picture, and pills between the words
574+ ;; and the image they introduce read as reactions to the words alone.
575+ ;; Nothing moves for a message with no picture — the box above it is
576+ ;; empty, and the pills still sit under the last line of text.
577+ [:vbox {:key :reactions-row :margin-top 6}
578+ (when (and (:id m) (not (:system? m)) (seq (:reactions m)))
579+ [reaction-row @s/current m])]]]))
575580
576581 (defn- day-separator [day-key label]
577582 [:vbox {:key day-key :spacing 4 :margin 0}
@@ -598,7 +603,7 @@
598603 ;; replay, an echo taking the place of what was sent — shifts
599604 ;; every row after it onto the widgets of its neighbour.
600605 true (conj ^{:key (or (:id m) (str "row-" i))}
601- [message-row i (when-not new-day? prev) m]))))
606+ [message-row i m]))))
602607 (range (count messages))
603608 messages))
604609
@@ -303,13 +303,31 @@
303 said, not a second thing said."303 said, not a second thing said."
304 14)304 14)
305 305
306-(defn- react-button306+(defn- action-chips
307- "The way to put an emoji on a message, beside the way to answer it.307+ "Answering and reacting, on the sender's row above the message.
308 308
309- Both are things done *to* a message rather than parts of it, so they share309+ Both are things done *to* a message rather than parts of it, so they ride
310- the sender's line and the right edge. It opens the picker on this message."310+ the sender's row against its right edge, at the size a reaction is. In the
311+ line with the text they took width off every line under them and wrapped a
312+ message that had the room to sit on one.
313+
314+ Chips rather than buttons: a button is sized for a label and these carry a
315+ glyph, which made two lozenges taller than the line they belonged to.
316+ `:reaction` with no count is the same pill the picker offers, so what you
317+ press to react and what appears once you have look like one family.
318+
319+ A row laid out from the right lays its first child furthest right, so
320+ reacting comes first in the source and this reads ↩️ then 🙂 on screen."
311 [channel m]321 [channel m]
312- [:button {:label "☺" :on-click #(s/open-picker! channel m)}])322+ [:hbox {:key :actions :align :end :spacing 4}
323+ [:reaction {:key :react
324+ :emoji "🙂"
325+ :size pill-size
326+ :on-click #(s/open-picker! channel m)}]
327+ [:reaction {:key :reply
328+ :emoji "↩️"
329+ :size pill-size
330+ :on-click #(s/reply-to! m)}]])
313 331
314 (defn- reaction-row332 (defn- reaction-row
315 "What people have put on a message, under it.333 "What people have put on a message, under it.
@@ -437,18 +455,18 @@
437 [:dim-label {:label "Click for the full profile"}]]))455 [:dim-label {:label "Click for the full profile"}]]))
438 456
439 (defn message-row457 (defn message-row
440- "One message. `prev` is the message above it, which decides whether this one458+ "One message: who said it, when, what you can do to it, and the words.
441- repeats the sender.459+
460+ Every line names its sender, rather than the first of a run only. A run
461+ collapsed to one heading reads well until you answer the fourth line of it,
462+ and then the line quoted back has no name on it; and the actions live on the
463+ sender's row, which a headerless line has nowhere to put.
442 464
443 Sender above the text, not beside it: a wrapping label in a horizontal row465 Sender above the text, not beside it: a wrapping label in a horizontal row
444 lays out against the row's width rather than the column's, so one long URL466 lays out against the row's width rather than the column's, so one long URL
445 drags every line that follows it off the left edge."467 drags every line that follows it off the left edge."
446- [i prev m]468+ [i m]
447- (let [same-sender? (and prev469+ (let [;; What a jump landed on wears a surface of its own for a moment, so
448- (not (:system? m))
449- (not (:system? prev))
450- (= (:from prev) (:from m)))
451- ;; What a jump landed on wears a surface of its own for a moment, so
452 ;; the answer to "which one was I sent to" is on the screen rather470 ;; the answer to "which one was I sent to" is on the screen rather
453 ;; than in the reader's count of rows.471 ;; than in the reader's count of rows.
454 highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))]472 highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))]
@@ -457,22 +475,21 @@
457 ;; list's scrollbar rides it too — without this the ↩ is what the475 ;; list's scrollbar rides it too — without this the ↩ is what the
458 ;; scrollbar is drawn over.476 ;; scrollbar is drawn over.
459 :margin-right 10477 :margin-right 10
460- ;; Space above a new speaker, none inside their run: the gap is478+ ;; One gap between messages: every line carries its own heading
461- ;; what separates one person's block from the next, and evenly479+ ;; now, so there are no runs to hold together and nothing for a
462- ;; spaced lines are what made the column read as one dense list.480+ ;; closed-up line to belong to.
463- :margin-top (if same-sender? 0 10)481+ :margin-top 10
464 ;; The jump target is what a "go to message" click scrolls to.482 ;; The jump target is what a "go to message" click scrolls to.
465 :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))}483 :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))}
466 ;; Likewise a key each: the surface a jump leaves behind is a different484 ;; Likewise a key each: the surface a jump leaves behind is a different
467 ;; node from the plain column, not the same node wearing another tag.485 ;; node from the plain column, not the same node wearing another tag.
468 [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain)486 [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain)
469 :spacing 2 :margin 0}487 :spacing 2 :margin 0}
470- ;; A run from one person reads as one block; repeating the nick on every488+ ;; Who, when, and what can be done about it, on one line above the
471- ;; line is what made a busy channel look like a list of headers. The time489+ ;; words: the name and the time say where the message came from, and the
472- ;; rides on that same line, for the same reason: once per run, not once490+ ;; chips at the far end are the two answers to it.
473- ;; per line.
474 [:vbox {:key :who}491 [:vbox {:key :who}
475- (when-not (or same-sender? (:system? m))492+ (when-not (:system? m)
476 ;; Everything that is about the person rather than the line: their493 ;; Everything that is about the person rather than the line: their
477 ;; picture, their name, and when they started saying this.494 ;; picture, their name, and when they started saying this.
478 [:hbox {:spacing 6}495 [:hbox {:spacing 6}
@@ -505,52 +522,32 @@
505 [:label {:label (:from m)}]522 [:label {:label (:from m)}]
506 (when-let [at (:at m)]523 (when-let [at (:at m)]
507 [:dim-label {:label (clock/clock-time at)}])524 [:dim-label {:label (clock/clock-time at)}])
508- ])]525+ ;; And the two things you can do to the message, at the far end of
509- ;; Every message can be answered, not only the one that opened a run —526+ ;; its heading: a nested row laid out from the right takes what is
510- ;; laid out from the right so the action sits at the edge and the text527+ ;; left of the width and puts the chips against the edge of it, so
511- ;; takes the width that is left. The text keeps a column of its own:528+ ;; the words below keep the whole column.
512- ;; a wrapping label directly in a row wraps against the row.529+ ;;
530+ ;; A key of its own for each branch, rather than one key whose node
531+ ;; changes tag under it: a line this client sent has no msgid until
532+ ;; the server echoes it back, and swapping a `:spacer` for a chip in
533+ ;; place is what left the row rebuilt in the wrong order.
534+ (if (:id m)
535+ [action-chips {:key :actions} @s/current m]
536+ [:spacer {:key :actions-gap :size 0}])])]
513 ;; A little air under the sender's row: the name and face are a heading537 ;; A little air under the sender's row: the name and face are a heading
514 ;; for what follows, and a heading that touches its text is not one.538 ;; for what follows, and a heading that touches its text is not one.
515- [:hbox {:key :line :align :end :spacing 6 :margin-top 10}539+ ;; The text keeps a column of its own: a wrapping label directly in a
516- ;; Answering and reacting sit together because they are the same kind540+ ;; row wraps against the row.
517- ;; of thing: what you do to a message rather than what it says. Both541+ [:vbox {:key :text :spacing 2 :margin-top 4}
518- ;; are in the row itself rather than in a box of their own — a box with542+ ;; What this answers, in the column the answer itself is in: the chip
519- ;; no width takes the rest of the row and puts the text on a line below543+ ;; is a quote of a line, so it wraps to the width a line has. Outside
520- ;; it, and one with a width takes the column's whole height. Reacting544+ ;; this column it wrapped to the whole row instead — wider than any
521- ;; comes first in the source because a row laid out from the right lays545+ ;; message, and out to the window's edge on a narrow screen.
522- ;; its first child furthest right: on screen this reads ↩ then ☺.546+ [:vbox {:key :reply-chip}
523- ;; A key of its own for each branch, rather than one key whose node547+ (when-let [reply-to (:reply-to m)]
524- ;; changes tag under it: a line this client sent has no msgid until the548+ [reply-chip @s/current reply-to])]
525- ;; server echoes it back, and swapping a `:spacer` for a `:button` in549+ (map-indexed (fn [j run] (run-node j run (:system? m)))
526- ;; place is what left the row rebuilt in the wrong order — the actions550+ (text-runs (:text m)))]
527- ;; ended up after the text, and every row beside it laid out from an
528- ;; edge that was no longer the row's. Two keys make it a removal and an
529- ;; addition, which is what it is.
530- (if (:id m)
531- [react-button {:key :react-button} @s/current m]
532- [:spacer {:key :react-gap :size 0}])
533- (if (:id m)
534- [:button {:key :reply-button :label "↩" :on-click #(s/reply-to! m)}]
535- [:spacer {:key :reply-gap :size 0}])
536- ;; The text column carries the pills as well as the words: put them
537- ;; outside this row and they sit below it, and the row is as tall as
538- ;; the buttons at its right edge — which is half a line of air between
539- ;; a message and the reactions on it, coming from nothing anyone asked
540- ;; for.
541- [:vbox {:key :text :spacing 2}
542- ;; What this answers, in the column the answer itself is in: the chip
543- ;; is a quote of a line, so it wraps to the width a line has. Outside
544- ;; this column it wrapped to the whole row instead — wider than any
545- ;; message, and out to the window's edge on a narrow screen.
546- [:vbox {:key :reply-chip}
547- (when-let [reply-to (:reply-to m)]
548- [reply-chip @s/current reply-to])]
549- (map-indexed (fn [j run] (run-node j run (:system? m)))
550- (text-runs (:text m)))
551- [:vbox {:key :reactions-row :margin-top 6}
552- (when (and (:id m) (not (:system? m)) (seq (:reactions m)))
553- [reaction-row @s/current m])]]]
554 ;; And the picker, when this is the message it was opened on: under the551 ;; And the picker, when this is the message it was opened on: under the
555 ;; line it is about, where the reader is already looking.552 ;; line it is about, where the reader is already looking.
556 ;;553 ;;
@@ -571,7 +568,15 @@
571 [:image {:key url568 [:image {:key url
572 :src path569 :src path
573 :max-height 260570 :max-height 260
574- :on-click #(reset! s/lightbox {:path path :url url})}]))))]]]))571+ :on-click #(reset! s/lightbox {:path path :url url})}]))))]
572+ ;; Reactions go last, under whatever the message turned out to be: a
573+ ;; line with a picture on it is the picture, and pills between the words
574+ ;; and the image they introduce read as reactions to the words alone.
575+ ;; Nothing moves for a message with no picture — the box above it is
576+ ;; empty, and the pills still sit under the last line of text.
577+ [:vbox {:key :reactions-row :margin-top 6}
578+ (when (and (:id m) (not (:system? m)) (seq (:reactions m)))
579+ [reaction-row @s/current m])]]]))
575 580
576 (defn- day-separator [day-key label]581 (defn- day-separator [day-key label]
577 [:vbox {:key day-key :spacing 4 :margin 0}582 [:vbox {:key day-key :spacing 4 :margin 0}
@@ -598,7 +603,7 @@
598 ;; replay, an echo taking the place of what was sent — shifts603 ;; replay, an echo taking the place of what was sent — shifts
599 ;; every row after it onto the widgets of its neighbour.604 ;; every row after it onto the widgets of its neighbour.
600 true (conj ^{:key (or (:id m) (str "row-" i))}605 true (conj ^{:key (or (:id m) (str "row-" i))}
601- [message-row i (when-not new-day? prev) m]))))606+ [message-row i m]))))
602 (range (count messages))607 (range (count messages))
603 messages))608 messages))
604 609