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

Put a conversation's Open on the line with its name

A row of chrome in a terminal is one cell, so a button on a line of its
own costs every card in the list a whole row. In a window the button is a
lozenge with air around it and keeps its own line, which is why this asks
`terminal?` rather than moving it for both.

And the lists that reorder are keyed, which the highlight this moved into
view made plain: opening a conversation bumps it to the top, and children
with no key at the list site reconcile by position — the widgets stay put
and their props are rewritten under them. Focus is a widget, so the
highlight stayed in the slot that was clicked while somebody else's
conversation slid into it. The `:key` was there all along, inside the
card, where the reconciler does not look for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-03T11:00:51-07:00 Browse files
6ba34b9 parent: b372e17
modified src/frq/app.jolt +34 -11
@@ -134,8 +134,20 @@
134134 [connect-action]]
135135 [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]])
136136
137+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
138+;; the first paint and never again. Two things in a message hang on it, and
139+;; both are about a cell grid rather than a canvas: there is no picture to
140+;; draw a face with, and a name with nothing to its left is the heading the
141+;; text hangs off — the shape a terminal has read messages in for forty years.
142+(defonce terminal? (atom false))
143+
137144 ;; ---------------------------------------------------------------- chats
138145
146+(defn- open-button [name]
147+ [:button {:label "Open"
148+ :kind :primary
149+ :on-click #(s/open-channel! name)}])
150+
139151 (defn conversation-row [buffer]
140152 (let [name (:name buffer)
141153 unread (:unread buffer)]
@@ -147,8 +159,17 @@
147159 ;; split down two lines — while the card, sized to a row that no longer
148160 ;; fits, sticks out past the cards above and below it.
149161 ;;
162+ ;; In a terminal the Open button rides up onto the name's line. A row of
163+ ;; chrome there is one cell, so a button on a line of its own costs the
164+ ;; card a whole row — four of them and the list has spent a screenful on
165+ ;; buttons. A window's row is 34 points and its button is a lozenge with
166+ ;; air around it, which is why it keeps its own line there.
167+ (if @terminal?
168+ [:hbox {:spacing 8 :wrap false}
169+ [:title-2 {:label name}]
170+ [open-button name]]
171+ [:title-2 {:label name}])
150172 ;; `:wrap false` on the badges: they sit beside each other or not at all.
151- [:title-2 {:label name}]
152173 [:hbox {:spacing 12 :wrap false}
153174 ;; A DM has no membership to report — there is nothing to be in — so the
154175 ;; badge says what the buffer is instead of answering a question nobody
@@ -159,7 +180,7 @@
159180 :live (boolean (:joined? buffer))}]
160181 (when (pos? unread) [:label {:label (str "● " unread)}])]
161182 [:dim-label {:label (s/last-preview buffer)}]
162- [:button {:label "Open" :kind :primary :on-click #(s/open-channel! name)}]]))
183+ (when-not @terminal? [open-button name])]))
163184
164185 (def ^:private sidebar-width 320)
165186
@@ -180,13 +201,6 @@
180201
181202 (defn- chrome-scale [] (/ (double @chrome-row) window-row))
182203
183-;; Whether the backend under this tree is a terminal, set by `frq.tui` before
184-;; the first paint and never again. Two things in a message hang on it, and
185-;; both are about a cell grid rather than a canvas: there is no picture to
186-;; draw a face with, and a name with nothing to its left is the heading the
187-;; text hangs off — the shape a terminal has read messages in for forty years.
188-(defonce terminal? (atom false))
189-
190204 (defn- chip-gap
191205 "The air between two chips on a row.
192206
@@ -289,7 +303,13 @@
289303 [:vbox {:key :list :fill-height true :reserve (below-list)}
290304 [:scroll {:scroll-key "chats-list" :orientation :vertical}
291305 (if (seq buffers)
292- (for [b buffers] [conversation-row b])
306+ ;; Keyed on the name, because this list reorders: opening a
307+ ;; conversation bumps it to the top, and unkeyed children reconcile by
308+ ;; position — the widgets stay put and their props are rewritten under
309+ ;; them. What is focused is a widget, so the highlight would stay in
310+ ;; the slot the reader clicked while the row that moved into it is
311+ ;; someone else's conversation.
312+ (for [b buffers] ^{:key (:name b)} [conversation-row b])
293313 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]
294314 [:vbox {:key :foot :spacing 8}
295315 [:separator {}]
@@ -1158,7 +1178,10 @@
11581178 [:scroll {:scroll-key (str "users-" name)
11591179 :orientation :vertical}
11601180 (if (seq people)
1161- (for [p people] [member-row p])
1181+ ;; Keyed, for the reason the conversation list is: this list reorders
1182+ ;; as people come and go, and a name is a button someone can be
1183+ ;; standing on.
1184+ (for [p people] ^{:key (:nick p)} [member-row p])
11621185 [:dim-label {:label "Nobody listed yet."}])]]))
11631186
11641187 (defn chat-screen []
@@ -134,8 +134,20 @@
134 [connect-action]]134 [connect-action]]
135 [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]])135 [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]])
136 136
137+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
138+;; the first paint and never again. Two things in a message hang on it, and
139+;; both are about a cell grid rather than a canvas: there is no picture to
140+;; draw a face with, and a name with nothing to its left is the heading the
141+;; text hangs off — the shape a terminal has read messages in for forty years.
142+(defonce terminal? (atom false))
143+
137 ;; ---------------------------------------------------------------- chats144 ;; ---------------------------------------------------------------- chats
138 145
146+(defn- open-button [name]
147+ [:button {:label "Open"
148+ :kind :primary
149+ :on-click #(s/open-channel! name)}])
150+
139 (defn conversation-row [buffer]151 (defn conversation-row [buffer]
140 (let [name (:name buffer)152 (let [name (:name buffer)
141 unread (:unread buffer)]153 unread (:unread buffer)]
@@ -147,8 +159,17 @@
147 ;; split down two lines — while the card, sized to a row that no longer159 ;; split down two lines — while the card, sized to a row that no longer
148 ;; fits, sticks out past the cards above and below it.160 ;; fits, sticks out past the cards above and below it.
149 ;;161 ;;
162+ ;; In a terminal the Open button rides up onto the name's line. A row of
163+ ;; chrome there is one cell, so a button on a line of its own costs the
164+ ;; card a whole row — four of them and the list has spent a screenful on
165+ ;; buttons. A window's row is 34 points and its button is a lozenge with
166+ ;; air around it, which is why it keeps its own line there.
167+ (if @terminal?
168+ [:hbox {:spacing 8 :wrap false}
169+ [:title-2 {:label name}]
170+ [open-button name]]
171+ [:title-2 {:label name}])
150 ;; `:wrap false` on the badges: they sit beside each other or not at all.172 ;; `:wrap false` on the badges: they sit beside each other or not at all.
151- [:title-2 {:label name}]
152 [:hbox {:spacing 12 :wrap false}173 [:hbox {:spacing 12 :wrap false}
153 ;; A DM has no membership to report — there is nothing to be in — so the174 ;; A DM has no membership to report — there is nothing to be in — so the
154 ;; badge says what the buffer is instead of answering a question nobody175 ;; badge says what the buffer is instead of answering a question nobody
@@ -159,7 +180,7 @@
159 :live (boolean (:joined? buffer))}]180 :live (boolean (:joined? buffer))}]
160 (when (pos? unread) [:label {:label (str "● " unread)}])]181 (when (pos? unread) [:label {:label (str "● " unread)}])]
161 [:dim-label {:label (s/last-preview buffer)}]182 [:dim-label {:label (s/last-preview buffer)}]
162- [:button {:label "Open" :kind :primary :on-click #(s/open-channel! name)}]]))183+ (when-not @terminal? [open-button name])]))
163 184
164 (def ^:private sidebar-width 320)185 (def ^:private sidebar-width 320)
165 186
@@ -180,13 +201,6 @@
180 201
181 (defn- chrome-scale [] (/ (double @chrome-row) window-row))202 (defn- chrome-scale [] (/ (double @chrome-row) window-row))
182 203
183-;; Whether the backend under this tree is a terminal, set by `frq.tui` before
184-;; the first paint and never again. Two things in a message hang on it, and
185-;; both are about a cell grid rather than a canvas: there is no picture to
186-;; draw a face with, and a name with nothing to its left is the heading the
187-;; text hangs off — the shape a terminal has read messages in for forty years.
188-(defonce terminal? (atom false))
189-
190 (defn- chip-gap204 (defn- chip-gap
191 "The air between two chips on a row.205 "The air between two chips on a row.
192 206
@@ -289,7 +303,13 @@
289 [:vbox {:key :list :fill-height true :reserve (below-list)}303 [:vbox {:key :list :fill-height true :reserve (below-list)}
290 [:scroll {:scroll-key "chats-list" :orientation :vertical}304 [:scroll {:scroll-key "chats-list" :orientation :vertical}
291 (if (seq buffers)305 (if (seq buffers)
292- (for [b buffers] [conversation-row b])306+ ;; Keyed on the name, because this list reorders: opening a
307+ ;; conversation bumps it to the top, and unkeyed children reconcile by
308+ ;; position — the widgets stay put and their props are rewritten under
309+ ;; them. What is focused is a widget, so the highlight would stay in
310+ ;; the slot the reader clicked while the row that moved into it is
311+ ;; someone else's conversation.
312+ (for [b buffers] ^{:key (:name b)} [conversation-row b])
293 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]313 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]
294 [:vbox {:key :foot :spacing 8}314 [:vbox {:key :foot :spacing 8}
295 [:separator {}]315 [:separator {}]
@@ -1158,7 +1178,10 @@
1158 [:scroll {:scroll-key (str "users-" name)1178 [:scroll {:scroll-key (str "users-" name)
1159 :orientation :vertical}1179 :orientation :vertical}
1160 (if (seq people)1180 (if (seq people)
1161- (for [p people] [member-row p])1181+ ;; Keyed, for the reason the conversation list is: this list reorders
1182+ ;; as people come and go, and a name is a button someone can be
1183+ ;; standing on.
1184+ (for [p people] ^{:key (:nick p)} [member-row p])
1162 [:dim-label {:label "Nobody listed yet."}])]]))1185 [:dim-label {:label "Nobody listed yet."}])]]))
1163 1186
1164 (defn chat-screen []1187 (defn chat-screen []