nandi/frqpublic Fork 0
6e5b1aa
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 each screen its own key, and the list its bands

The pinned header did not stay pinned: arrive at the chats screen from
another one and its join box turned up under the tab bar, at the bottom
of the window. A tree dump says why — at mount the children are in the
order they were written, and after a screen change the card has been
moved to the end.

Every screen was one position in the tree, so moving between two of them
diffed one screen's children against another's: the reconciler matched
what it could by position and patched the rest, leaving nodes from the
screen you left standing in the one you arrived at. (A conversation's
error box was still there too, twice over.) A key per branch makes the
swap a swap — the old tree out whole, the new one in whole.

The chats screen's three bands are wrapped as three while we are here,
so its top-level children are the same three whatever the list is doing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-01T21:15:44-07:00 Browse files
6e5b1aa parent: 66ba416
modified src/frq/app.jolt +57 -44
@@ -194,39 +194,40 @@
194194 ;; and the split view's 900: the list is full-bleed there now, which is
195195 ;; what the conversation beside it has always been.
196196 [:vbox {:spacing 8 :margin 12}
197- ;; Your nick in the title, where "Chats" alone said nothing you did not
198- ;; already know. It is what every channel calls you and what your own
199- ;; lines are signed with, and the only other place it showed was Settings.
200- [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}]
201- [error-note]
202- ;; Join and search are the same shape — a channel box with a button
203- ;; beside it — so they read as one control block rather than a titled
204- ;; card and a stray row: same card, same widths, same gap. The screen
205- ;; title already says what the box is for, which is what the "Join
206- ;; channel" header was doing.
207- [:card {}
208- [:vbox {:spacing 8}
209- [:hbox {:spacing 8}
210- ;; Both kinds of conversation through one box: `#room` joins a
211- ;; channel, `@nick` opens a message to a person.
212- [:entry {:text @s/join-input
213- :width-request (control-entry-width)
214- :placeholder "#channel or @nick"
215- :on-change #(reset! s/join-input %)
216- :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]
217- [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")
218- :kind :primary
219- :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]
220- ;; The clear button only shows while there is something to clear: an
221- ;; empty box has nothing to undo, and a dead button beside it reads as
222- ;; one that stopped working.
223- [:hbox {:spacing 8}
224- [:entry {:text @s/search
225- :width-request (control-entry-width)
226- :placeholder "Search channels"
227- :on-change #(reset! s/search %)}]
228- (when (seq @s/search)
229- [:button {:label "✕" :on-click #(reset! s/search "")}])]]]
197+ [:vbox {:key :head :spacing 8}
198+ ;; Your nick in the title, where "Chats" alone said nothing you did not
199+ ;; already know. It is what every channel calls you and what your own
200+ ;; lines are signed with, and the only other place it showed was Settings.
201+ [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}]
202+ [error-note]
203+ ;; Join and search are the same shape — a channel box with a button
204+ ;; beside it — so they read as one control block rather than a titled
205+ ;; card and a stray row: same card, same widths, same gap. The screen
206+ ;; title already says what the box is for, which is what the "Join
207+ ;; channel" header was doing.
208+ [:card {}
209+ [:vbox {:spacing 8}
210+ [:hbox {:spacing 8}
211+ ;; Both kinds of conversation through one box: `#room` joins a
212+ ;; channel, `@nick` opens a message to a person.
213+ [:entry {:text @s/join-input
214+ :width-request (control-entry-width)
215+ :placeholder "#channel or @nick"
216+ :on-change #(reset! s/join-input %)
217+ :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]
218+ [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")
219+ :kind :primary
220+ :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]
221+ ;; The clear button only shows while there is something to clear: an
222+ ;; empty box has nothing to undo, and a dead button beside it reads as
223+ ;; one that stopped working.
224+ [:hbox {:spacing 8}
225+ [:entry {:text @s/search
226+ :width-request (control-entry-width)
227+ :placeholder "Search channels"
228+ :on-change #(reset! s/search %)}]
229+ (when (seq @s/search)
230+ [:button {:label "✕" :on-click #(reset! s/search "")}])]]]]
230231 ;; `:fill-height` so the scroll inside is handed the rest of the window
231232 ;; rather than the one card it starts out holding, and `:reserve` to keep
232233 ;; the tabs' strip out of what it may take.
@@ -237,8 +238,9 @@
237238 (if (seq buffers)
238239 (for [b buffers] [conversation-row b])
239240 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]
240- [:separator {}]
241- [tab-bar]]))
241+ [:vbox {:key :foot :spacing 8}
242+ [:separator {}]
243+ [tab-bar]]]))
242244
243245 ;; ---------------------------------------------------------------- chat
244246
@@ -1273,21 +1275,32 @@
12731275 ;; ---------------------------------------------------------------- shell
12741276
12751277 (defn app []
1278+ ;; Every branch in its own keyed wrapper, and every key different.
1279+ ;;
1280+ ;; Without them the screens are all one position in the tree, and moving
1281+ ;; between two of them is a diff of one screen's children against another's:
1282+ ;; the reconciler matches what it can by position and patches the rest, which
1283+ ;; leaves nodes from the screen you left standing in the one you arrived at —
1284+ ;; the chats screen's join box turning up under its tab bar, an error box
1285+ ;; from the conversation you were in a moment ago. A key that changes with
1286+ ;; the screen makes the swap a swap: the old tree comes out whole and the new
1287+ ;; one goes in whole.
12761288 (cond
1277- @s/lightbox [lightbox-screen]
1278- @profile/viewing [profile-screen]
1279- @s/image-picker [image-picker-screen]
1289+ @s/lightbox [:vbox {:key :screen-lightbox} [lightbox-screen]]
1290+ @profile/viewing [:vbox {:key :screen-profile} [profile-screen]]
1291+ @s/image-picker [:vbox {:key :screen-picker} [image-picker-screen]]
12801292 ;; Wide enough for both, and on one of the two screens that are halves of
12811293 ;; the same thing: the list and the conversation it opens. Discover and
12821294 ;; settings stay whole screens — they are somewhere else, not the other
12831295 ;; half of here.
1284- (and (s/wide?) (contains? #{:chats :chat} @s/screen)) [split-screen]
1296+ (and (s/wide?) (contains? #{:chats :chat} @s/screen))
1297+ [:vbox {:key :screen-split} [split-screen]]
12851298 :else (case @s/screen
1286- :connect [connect-screen]
1287- :chat [chat-screen]
1288- :discover [discover-screen]
1289- :settings [settings-screen]
1290- [chats-screen])))
1299+ :connect [:vbox {:key :screen-connect} [connect-screen]]
1300+ :chat [:vbox {:key :screen-chat} [chat-screen]]
1301+ :discover [:vbox {:key :screen-discover} [discover-screen]]
1302+ :settings [:vbox {:key :screen-settings} [settings-screen]]
1303+ [:vbox {:key :screen-chats} [chats-screen]])))
12911304
12921305 (defn -main [& _]
12931306 ;; Before the window: the settings, the rooms this client has been in, and a
@@ -194,39 +194,40 @@
194 ;; and the split view's 900: the list is full-bleed there now, which is194 ;; and the split view's 900: the list is full-bleed there now, which is
195 ;; what the conversation beside it has always been.195 ;; what the conversation beside it has always been.
196 [:vbox {:spacing 8 :margin 12}196 [:vbox {:spacing 8 :margin 12}
197- ;; Your nick in the title, where "Chats" alone said nothing you did not197+ [:vbox {:key :head :spacing 8}
198- ;; already know. It is what every channel calls you and what your own198+ ;; Your nick in the title, where "Chats" alone said nothing you did not
199- ;; lines are signed with, and the only other place it showed was Settings.199+ ;; already know. It is what every channel calls you and what your own
200- [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}]200+ ;; lines are signed with, and the only other place it showed was Settings.
201- [error-note]201+ [:title {:label (if (s/connected?) (str "Chats as " @s/form-nick) "Chats")}]
202- ;; Join and search are the same shape — a channel box with a button202+ [error-note]
203- ;; beside it — so they read as one control block rather than a titled203+ ;; Join and search are the same shape — a channel box with a button
204- ;; card and a stray row: same card, same widths, same gap. The screen204+ ;; beside it — so they read as one control block rather than a titled
205- ;; title already says what the box is for, which is what the "Join205+ ;; card and a stray row: same card, same widths, same gap. The screen
206- ;; channel" header was doing.206+ ;; title already says what the box is for, which is what the "Join
207- [:card {}207+ ;; channel" header was doing.
208- [:vbox {:spacing 8}208+ [:card {}
209- [:hbox {:spacing 8}209+ [:vbox {:spacing 8}
210- ;; Both kinds of conversation through one box: `#room` joins a210+ [:hbox {:spacing 8}
211- ;; channel, `@nick` opens a message to a person.211+ ;; Both kinds of conversation through one box: `#room` joins a
212- [:entry {:text @s/join-input212+ ;; channel, `@nick` opens a message to a person.
213- :width-request (control-entry-width)213+ [:entry {:text @s/join-input
214- :placeholder "#channel or @nick"214+ :width-request (control-entry-width)
215- :on-change #(reset! s/join-input %)215+ :placeholder "#channel or @nick"
216- :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]216+ :on-change #(reset! s/join-input %)
217- [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")217+ :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]
218- :kind :primary218+ [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")
219- :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]219+ :kind :primary
220- ;; The clear button only shows while there is something to clear: an220+ :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]
221- ;; empty box has nothing to undo, and a dead button beside it reads as221+ ;; The clear button only shows while there is something to clear: an
222- ;; one that stopped working.222+ ;; empty box has nothing to undo, and a dead button beside it reads as
223- [:hbox {:spacing 8}223+ ;; one that stopped working.
224- [:entry {:text @s/search224+ [:hbox {:spacing 8}
225- :width-request (control-entry-width)225+ [:entry {:text @s/search
226- :placeholder "Search channels"226+ :width-request (control-entry-width)
227- :on-change #(reset! s/search %)}]227+ :placeholder "Search channels"
228- (when (seq @s/search)228+ :on-change #(reset! s/search %)}]
229- [:button {:label "✕" :on-click #(reset! s/search "")}])]]]229+ (when (seq @s/search)
230+ [:button {:label "✕" :on-click #(reset! s/search "")}])]]]]
230 ;; `:fill-height` so the scroll inside is handed the rest of the window231 ;; `:fill-height` so the scroll inside is handed the rest of the window
231 ;; rather than the one card it starts out holding, and `:reserve` to keep232 ;; rather than the one card it starts out holding, and `:reserve` to keep
232 ;; the tabs' strip out of what it may take.233 ;; the tabs' strip out of what it may take.
@@ -237,8 +238,9 @@
237 (if (seq buffers)238 (if (seq buffers)
238 (for [b buffers] [conversation-row b])239 (for [b buffers] [conversation-row b])
239 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]240 [:card {} [:dim-label {:label "No conversations yet — join a channel."}]])]]
240- [:separator {}]241+ [:vbox {:key :foot :spacing 8}
241- [tab-bar]]))242+ [:separator {}]
243+ [tab-bar]]]))
242 244
243 ;; ---------------------------------------------------------------- chat245 ;; ---------------------------------------------------------------- chat
244 246
@@ -1273,21 +1275,32 @@
1273 ;; ---------------------------------------------------------------- shell1275 ;; ---------------------------------------------------------------- shell
1274 1276
1275 (defn app []1277 (defn app []
1278+ ;; Every branch in its own keyed wrapper, and every key different.
1279+ ;;
1280+ ;; Without them the screens are all one position in the tree, and moving
1281+ ;; between two of them is a diff of one screen's children against another's:
1282+ ;; the reconciler matches what it can by position and patches the rest, which
1283+ ;; leaves nodes from the screen you left standing in the one you arrived at —
1284+ ;; the chats screen's join box turning up under its tab bar, an error box
1285+ ;; from the conversation you were in a moment ago. A key that changes with
1286+ ;; the screen makes the swap a swap: the old tree comes out whole and the new
1287+ ;; one goes in whole.
1276 (cond1288 (cond
1277- @s/lightbox [lightbox-screen]1289+ @s/lightbox [:vbox {:key :screen-lightbox} [lightbox-screen]]
1278- @profile/viewing [profile-screen]1290+ @profile/viewing [:vbox {:key :screen-profile} [profile-screen]]
1279- @s/image-picker [image-picker-screen]1291+ @s/image-picker [:vbox {:key :screen-picker} [image-picker-screen]]
1280 ;; Wide enough for both, and on one of the two screens that are halves of1292 ;; Wide enough for both, and on one of the two screens that are halves of
1281 ;; the same thing: the list and the conversation it opens. Discover and1293 ;; the same thing: the list and the conversation it opens. Discover and
1282 ;; settings stay whole screens — they are somewhere else, not the other1294 ;; settings stay whole screens — they are somewhere else, not the other
1283 ;; half of here.1295 ;; half of here.
1284- (and (s/wide?) (contains? #{:chats :chat} @s/screen)) [split-screen]1296+ (and (s/wide?) (contains? #{:chats :chat} @s/screen))
1297+ [:vbox {:key :screen-split} [split-screen]]
1285 :else (case @s/screen1298 :else (case @s/screen
1286- :connect [connect-screen]1299+ :connect [:vbox {:key :screen-connect} [connect-screen]]
1287- :chat [chat-screen]1300+ :chat [:vbox {:key :screen-chat} [chat-screen]]
1288- :discover [discover-screen]1301+ :discover [:vbox {:key :screen-discover} [discover-screen]]
1289- :settings [settings-screen]1302+ :settings [:vbox {:key :screen-settings} [settings-screen]]
1290- [chats-screen])))1303+ [:vbox {:key :screen-chats} [chats-screen]])))
1291 1304
1292 (defn -main [& _]1305 (defn -main [& _]
1293 ;; Before the window: the settings, the rooms this client has been in, and a1306 ;; Before the window: the settings, the rooms this client has been in, and a