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>
6e5b1aa parent: 66ba416 modified
src/frq/app.jolt +57 -44 | @@ -194,39 +194,40 @@ | ||
| 194 | 194 | ;; and the split view's 900: the list is full-bleed there now, which is |
| 195 | 195 | ;; what the conversation beside it has always been. |
| 196 | 196 | [: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 "")}])]]]] | |
| 230 | 231 | ;; `:fill-height` so the scroll inside is handed the rest of the window |
| 231 | 232 | ;; rather than the one card it starts out holding, and `:reserve` to keep |
| 232 | 233 | ;; the tabs' strip out of what it may take. |
| @@ -237,8 +238,9 @@ | ||
| 237 | 238 | (if (seq buffers) |
| 238 | 239 | (for [b buffers] [conversation-row b]) |
| 239 | 240 | [: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]]])) | |
| 242 | 244 | |
| 243 | 245 | ;; ---------------------------------------------------------------- chat |
| 244 | 246 | |
| @@ -1273,21 +1275,32 @@ | ||
| 1273 | 1275 | ;; ---------------------------------------------------------------- shell |
| 1274 | 1276 | |
| 1275 | 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 | 1288 | (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]] | |
| 1280 | 1292 | ;; Wide enough for both, and on one of the two screens that are halves of |
| 1281 | 1293 | ;; the same thing: the list and the conversation it opens. Discover and |
| 1282 | 1294 | ;; settings stay whole screens — they are somewhere else, not the other |
| 1283 | 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 | 1298 | :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]]))) | |
| 1291 | 1304 | |
| 1292 | 1305 | (defn -main [& _] |
| 1293 | 1306 | ;; 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 is | 194 | ;; 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 not | 197 | + [:vbox {:key :head :spacing 8} |
| 198 | - ;; already know. It is what every channel calls you and what your own | 198 | + ;; 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 button | 202 | + [error-note] |
| 203 | - ;; beside it — so they read as one control block rather than a titled | 203 | + ;; 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 screen | 204 | + ;; 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 "Join | 205 | + ;; 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 a | 210 | + [: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-input | 212 | + ;; 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 :primary | 218 | + [: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: an | 220 | + :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 as | 221 | + ;; 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/search | 224 | + [: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 window | 231 | ;; `: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 keep | 232 | ;; 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 | ;; ---------------------------------------------------------------- chat | 245 | ;; ---------------------------------------------------------------- chat |
| 244 | 246 | ||
| @@ -1273,21 +1275,32 @@ | |||
| 1273 | ;; ---------------------------------------------------------------- shell | 1275 | ;; ---------------------------------------------------------------- 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 | (cond | 1288 | (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 of | 1292 | ;; 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 and | 1293 | ;; the same thing: the list and the conversation it opens. Discover and |
| 1282 | ;; settings stay whole screens — they are somewhere else, not the other | 1294 | ;; 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/screen | 1298 | :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 a | 1306 | ;; Before the window: the settings, the rooms this client has been in, and a |