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

Merge branch 'claude/channel-message-layout-b148bc'

# Conflicts:
#	src/frq/app.jolt
nandi committed 2026-08-30T17:24:03-07:00 Browse files
5326d79 parents: cab7487 10a1bd0
modified deps.edn +6 -2
@@ -2,12 +2,16 @@
22
33 ;; glimmer owns the reactive half (ratom, components, reconciler);
44 ;; glimmer-vidya paints it as Vidya/egui through libvidya's retained-tree ABI.
5- ;; Our fork of glimmer, until the reconciler fix in it lands upstream: a
5+ ;; Our fork of glimmer, until the reconciler fixes in it land upstream. A
66 ;; component unmounted between a cell firing and the queued render running
77 ;; used to render anyway, into widgets the backend had already freed and
88 ;; handed out again — which took the message list apart in a busy channel.
9+ ;; And replacing a native node with one of another tag left every watcher
10+ ;; under it subscribed, so swapping the phone layout for the split one and
11+ ;; back shredded the chat: one component's props painted onto another's
12+ ;; widget.
913 :deps {jolt-lang/glimmer {:git/url "https://gitlab.com/nandithebull/glimmer"
10- :git/sha "4e733aa26343cef0478d8dfd3f2b76c0113b0969"}
14+ :git/sha "399df371c790d690fb6e4560c3d4d7f838502857"}
1115 nandi/glimmer-vidya {:local/root "../vidya/glimmer"}}
1216
1317 ;; libvidya, the Rust/egui build (the C/raylib one does not export the tree
@@ -2,12 +2,16 @@
2 2
3 ;; glimmer owns the reactive half (ratom, components, reconciler);3 ;; glimmer owns the reactive half (ratom, components, reconciler);
4 ;; glimmer-vidya paints it as Vidya/egui through libvidya's retained-tree ABI.4 ;; glimmer-vidya paints it as Vidya/egui through libvidya's retained-tree ABI.
5- ;; Our fork of glimmer, until the reconciler fix in it lands upstream: a5+ ;; Our fork of glimmer, until the reconciler fixes in it land upstream. A
6 ;; component unmounted between a cell firing and the queued render running6 ;; component unmounted between a cell firing and the queued render running
7 ;; used to render anyway, into widgets the backend had already freed and7 ;; used to render anyway, into widgets the backend had already freed and
8 ;; handed out again — which took the message list apart in a busy channel.8 ;; handed out again — which took the message list apart in a busy channel.
9+ ;; And replacing a native node with one of another tag left every watcher
10+ ;; under it subscribed, so swapping the phone layout for the split one and
11+ ;; back shredded the chat: one component's props painted onto another's
12+ ;; widget.
9 :deps {jolt-lang/glimmer {:git/url "https://gitlab.com/nandithebull/glimmer"13 :deps {jolt-lang/glimmer {:git/url "https://gitlab.com/nandithebull/glimmer"
10- :git/sha "4e733aa26343cef0478d8dfd3f2b76c0113b0969"}14+ :git/sha "399df371c790d690fb6e4560c3d4d7f838502857"}
11 nandi/glimmer-vidya {:local/root "../vidya/glimmer"}}15 nandi/glimmer-vidya {:local/root "../vidya/glimmer"}}
12 16
13 ;; libvidya, the Rust/egui build (the C/raylib one does not export the tree17 ;; libvidya, the Rust/egui build (the C/raylib one does not export the tree
modified src/frq/app.jolt +65 -8
@@ -584,7 +584,13 @@
584584 ;; bottom of a screen wants is above it, not beneath it.
585585 [:vbox {:spacing 8 :margin 12 :margin-bottom 4}
586586 [:hbox {:spacing 8}
587- [:button {:label "← Chats" :on-click #(reset! s/screen :chats)}]
587+ ;; The way back to the list, on a window with room for one thing at a
588+ ;; time. Beside the list there is nothing to go back to, so the button
589+ ;; goes in a wrapper of its own, since a child that comes and goes
590+ ;; would otherwise renumber the row for the reconciler.
591+ [:vbox {:key :back}
592+ (when-not (s/wide?)
593+ [:button {:label "← Chats" :on-click #(reset! s/screen :chats)}])]
588594 [:title {:label (or name "Chat")}]]
589595 [error-note]
590596 ;; :reserve leaves room for everything below: the jump button's row, the
@@ -660,6 +666,46 @@
660666 :on-activate s/send-draft!}]
661667 [:button {:label "Send" :kind :primary :on-click s/send-draft!}]]]))
662668
669+;; ---------------------------------------------------------------- split
670+
671+(def ^:private sidebar-width 320)
672+
673+(defn- no-chat-pane
674+ "What fills the second pane before a conversation has been picked. The pane
675+ is there either way a list that widened into two columns and back again as
676+ channels were opened would be a worse answer than an empty half."
677+ []
678+ [:vbox {:spacing 8 :margin 12}
679+ [:title {:label "frq"}]
680+ [:card {} [:dim-label {:label "Pick a conversation on the left."}]]])
681+
682+(defn split-screen
683+ "The chats list and the conversation side by side, for a window wide enough
684+ to hold both.
685+
686+ Same components as the narrow layout, in a row instead of one at a time: the
687+ list keeps its own scroll and its own tab bar, and the conversation keeps the
688+ compose bar pinned under a backlog that scrolls on its own.
689+
690+ Both panes take `:fill-height`: a column in a row is otherwise as tall as the
691+ row, which at the moment it is placed is one button and the list and the
692+ message backlog both size themselves against the height they are handed.
693+
694+ Only the list is given a width. The conversation takes what is left, rather
695+ than the window's width minus the list's: the list costs a little more than
696+ its 320 (its page has padding of its own), and a second column asking for
697+ more than remains is wrapped onto a row below painting the whole
698+ conversation off the bottom of the window, which reads exactly like a
699+ conversation that has gone missing."
700+ []
701+ [:hbox {:spacing 0 :wrap false}
702+ [:vbox {:key :list :width-request sidebar-width :fill-height true}
703+ [chats-screen]]
704+ [:vbox {:key :chat :fill-height true}
705+ (if @s/current
706+ [chat-screen]
707+ [no-chat-pane])]])
708+
663709 ;; ---------------------------------------------------------------- discover
664710
665711 (defn discover-screen []
@@ -719,13 +765,17 @@
719765 (cond
720766 @s/lightbox [lightbox-screen]
721767 @s/image-picker [image-picker-screen]
722- :else
723- (case @s/screen
724- :connect [connect-screen]
725- :chat [chat-screen]
726- :discover [discover-screen]
727- :settings [settings-screen]
728- [chats-screen])))
768+ ;; Wide enough for both, and on one of the two screens that are halves of
769+ ;; the same thing: the list and the conversation it opens. Discover and
770+ ;; settings stay whole screens they are somewhere else, not the other
771+ ;; half of here.
772+ (and (s/wide?) (contains? #{:chats :chat} @s/screen)) [split-screen]
773+ :else (case @s/screen
774+ :connect [connect-screen]
775+ :chat [chat-screen]
776+ :discover [discover-screen]
777+ :settings [settings-screen]
778+ [chats-screen])))
729779
730780 (defn -main [& _]
731781 ;; Before the window: the rooms this client has been in, and a saved sign-in
@@ -739,4 +789,11 @@
739789 ;; user should be looking at while this happens, and if it fails, the
740790 ;; error lands somewhere visible.
741791 (vidya/after! 150 s/connect!))
792+ ;; The window's width, into a ratom, a few times a second. Polled rather than
793+ ;; delivered: the backend reports a size by writing it onto the window node,
794+ ;; and only what a component derefs re-renders so the layout follows a drag
795+ ;; of the window's edge without every frame touching the tree.
796+ (vidya/every! 200 #(let [w (vidya/window-width)]
797+ (when (not= w @s/window-width)
798+ (reset! s/window-width w))))
742799 (ui/run app :title "frq" :width 520 :height 860))
@@ -584,7 +584,13 @@
584 ;; bottom of a screen wants is above it, not beneath it.584 ;; bottom of a screen wants is above it, not beneath it.
585 [:vbox {:spacing 8 :margin 12 :margin-bottom 4}585 [:vbox {:spacing 8 :margin 12 :margin-bottom 4}
586 [:hbox {:spacing 8}586 [:hbox {:spacing 8}
587- [:button {:label "← Chats" :on-click #(reset! s/screen :chats)}]587+ ;; The way back to the list, on a window with room for one thing at a
588+ ;; time. Beside the list there is nothing to go back to, so the button
589+ ;; goes in a wrapper of its own, since a child that comes and goes
590+ ;; would otherwise renumber the row for the reconciler.
591+ [:vbox {:key :back}
592+ (when-not (s/wide?)
593+ [:button {:label "← Chats" :on-click #(reset! s/screen :chats)}])]
588 [:title {:label (or name "Chat")}]]594 [:title {:label (or name "Chat")}]]
589 [error-note]595 [error-note]
590 ;; :reserve leaves room for everything below: the jump button's row, the596 ;; :reserve leaves room for everything below: the jump button's row, the
@@ -660,6 +666,46 @@
660 :on-activate s/send-draft!}]666 :on-activate s/send-draft!}]
661 [:button {:label "Send" :kind :primary :on-click s/send-draft!}]]]))667 [:button {:label "Send" :kind :primary :on-click s/send-draft!}]]]))
662 668
669+;; ---------------------------------------------------------------- split
670+
671+(def ^:private sidebar-width 320)
672+
673+(defn- no-chat-pane
674+ "What fills the second pane before a conversation has been picked. The pane
675+ is there either way a list that widened into two columns and back again as
676+ channels were opened would be a worse answer than an empty half."
677+ []
678+ [:vbox {:spacing 8 :margin 12}
679+ [:title {:label "frq"}]
680+ [:card {} [:dim-label {:label "Pick a conversation on the left."}]]])
681+
682+(defn split-screen
683+ "The chats list and the conversation side by side, for a window wide enough
684+ to hold both.
685+
686+ Same components as the narrow layout, in a row instead of one at a time: the
687+ list keeps its own scroll and its own tab bar, and the conversation keeps the
688+ compose bar pinned under a backlog that scrolls on its own.
689+
690+ Both panes take `:fill-height`: a column in a row is otherwise as tall as the
691+ row, which at the moment it is placed is one button and the list and the
692+ message backlog both size themselves against the height they are handed.
693+
694+ Only the list is given a width. The conversation takes what is left, rather
695+ than the window's width minus the list's: the list costs a little more than
696+ its 320 (its page has padding of its own), and a second column asking for
697+ more than remains is wrapped onto a row below painting the whole
698+ conversation off the bottom of the window, which reads exactly like a
699+ conversation that has gone missing."
700+ []
701+ [:hbox {:spacing 0 :wrap false}
702+ [:vbox {:key :list :width-request sidebar-width :fill-height true}
703+ [chats-screen]]
704+ [:vbox {:key :chat :fill-height true}
705+ (if @s/current
706+ [chat-screen]
707+ [no-chat-pane])]])
708+
663 ;; ---------------------------------------------------------------- discover709 ;; ---------------------------------------------------------------- discover
664 710
665 (defn discover-screen []711 (defn discover-screen []
@@ -719,13 +765,17 @@
719 (cond765 (cond
720 @s/lightbox [lightbox-screen]766 @s/lightbox [lightbox-screen]
721 @s/image-picker [image-picker-screen]767 @s/image-picker [image-picker-screen]
722- :else768+ ;; Wide enough for both, and on one of the two screens that are halves of
723- (case @s/screen769+ ;; the same thing: the list and the conversation it opens. Discover and
724- :connect [connect-screen]770+ ;; settings stay whole screens they are somewhere else, not the other
725- :chat [chat-screen]771+ ;; half of here.
726- :discover [discover-screen]772+ (and (s/wide?) (contains? #{:chats :chat} @s/screen)) [split-screen]
727- :settings [settings-screen]773+ :else (case @s/screen
728- [chats-screen])))774+ :connect [connect-screen]
775+ :chat [chat-screen]
776+ :discover [discover-screen]
777+ :settings [settings-screen]
778+ [chats-screen])))
729 779
730 (defn -main [& _]780 (defn -main [& _]
731 ;; Before the window: the rooms this client has been in, and a saved sign-in781 ;; Before the window: the rooms this client has been in, and a saved sign-in
@@ -739,4 +789,11 @@
739 ;; user should be looking at while this happens, and if it fails, the789 ;; user should be looking at while this happens, and if it fails, the
740 ;; error lands somewhere visible.790 ;; error lands somewhere visible.
741 (vidya/after! 150 s/connect!))791 (vidya/after! 150 s/connect!))
792+ ;; The window's width, into a ratom, a few times a second. Polled rather than
793+ ;; delivered: the backend reports a size by writing it onto the window node,
794+ ;; and only what a component derefs re-renders so the layout follows a drag
795+ ;; of the window's edge without every frame touching the tree.
796+ (vidya/every! 200 #(let [w (vidya/window-width)]
797+ (when (not= w @s/window-width)
798+ (reset! s/window-width w))))
742 (ui/run app :title "frq" :width 520 :height 860))799 (ui/run app :title "frq" :width 520 :height 860))
modified src/frq/state.jolt +31 -2
@@ -88,6 +88,32 @@
8888 (defonce lightbox (atom nil)) ; {:path :url}
8989 (defonce join-input (atom ""))
9090
91+;; The window's content width in points, polled from the backend a few times a
92+;; second. The app is laid out for a phone-width window, and this is what lets
93+;; a wide one be more than a phone with margins: past `wide-width` the channel
94+;; list and the conversation are both on screen instead of taking turns.
95+(defonce window-width (atom 0))
96+
97+;; Where the second pane starts paying for itself. Below this a 300pt list
98+;; beside a conversation leaves the messages narrower than the phone layout
99+;; they were written for.
100+(def wide-width 900)
101+
102+(defn wide?
103+ "True while the window has room for the list and a conversation at once."
104+ []
105+ (>= @window-width wide-width))
106+
107+(defn chat-visible?
108+ "Whether the conversation in `current` is on screen.
109+
110+ On a narrow window that is the chat screen alone. On a wide one the chats
111+ screen shows it too, in the pane beside the list — so this, and not the
112+ screen, is what decides whether an arriving line counts as unread."
113+ []
114+ (or (= :chat @screen)
115+ (and (wide?) (= :chats @screen))))
116+
91117 ;; Whether the chat view is showing the newest line, and a counter the view
92118 ;; watches to be told to go back to it. A counter rather than a flag: a flag
93119 ;; would need clearing, and there is no frame in which to clear it.
@@ -150,7 +176,7 @@
150176 (swap! channels
151177 (fn [m]
152178 (let [m (ensure-channel m channel)
153- viewing? (and (= :chat @screen) (= channel @current))]
179+ viewing? (and (chat-visible?) (= channel @current))]
154180 (-> m
155181 (update-in [channel :messages] conj
156182 {:from from :text text :system? (= "*" from)
@@ -170,7 +196,10 @@
170196 buffer stays), and opening one is a request to be in it."
171197 [name]
172198 (reset! current name)
173- (reset! screen :chat)
199+ ;; On a wide window the conversation lives in the chats screen's second
200+ ;; pane, beside the list; :chat is the narrow window's way of showing it
201+ ;; instead of the list, and there is nothing there to trade it for.
202+ (reset! screen (if (wide?) :chats :chat))
174203 ;; A picker belongs to the message it was opened on; carrying it into another
175204 ;; buffer would offer to react to something that is no longer on screen.
176205 (reset! reacting nil)
@@ -88,6 +88,32 @@
88 (defonce lightbox (atom nil)) ; {:path :url}88 (defonce lightbox (atom nil)) ; {:path :url}
89 (defonce join-input (atom ""))89 (defonce join-input (atom ""))
90 90
91+;; The window's content width in points, polled from the backend a few times a
92+;; second. The app is laid out for a phone-width window, and this is what lets
93+;; a wide one be more than a phone with margins: past `wide-width` the channel
94+;; list and the conversation are both on screen instead of taking turns.
95+(defonce window-width (atom 0))
96+
97+;; Where the second pane starts paying for itself. Below this a 300pt list
98+;; beside a conversation leaves the messages narrower than the phone layout
99+;; they were written for.
100+(def wide-width 900)
101+
102+(defn wide?
103+ "True while the window has room for the list and a conversation at once."
104+ []
105+ (>= @window-width wide-width))
106+
107+(defn chat-visible?
108+ "Whether the conversation in `current` is on screen.
109+
110+ On a narrow window that is the chat screen alone. On a wide one the chats
111+ screen shows it too, in the pane beside the list — so this, and not the
112+ screen, is what decides whether an arriving line counts as unread."
113+ []
114+ (or (= :chat @screen)
115+ (and (wide?) (= :chats @screen))))
116+
91 ;; Whether the chat view is showing the newest line, and a counter the view117 ;; Whether the chat view is showing the newest line, and a counter the view
92 ;; watches to be told to go back to it. A counter rather than a flag: a flag118 ;; watches to be told to go back to it. A counter rather than a flag: a flag
93 ;; would need clearing, and there is no frame in which to clear it.119 ;; would need clearing, and there is no frame in which to clear it.
@@ -150,7 +176,7 @@
150 (swap! channels176 (swap! channels
151 (fn [m]177 (fn [m]
152 (let [m (ensure-channel m channel)178 (let [m (ensure-channel m channel)
153- viewing? (and (= :chat @screen) (= channel @current))]179+ viewing? (and (chat-visible?) (= channel @current))]
154 (-> m180 (-> m
155 (update-in [channel :messages] conj181 (update-in [channel :messages] conj
156 {:from from :text text :system? (= "*" from)182 {:from from :text text :system? (= "*" from)
@@ -170,7 +196,10 @@
170 buffer stays), and opening one is a request to be in it."196 buffer stays), and opening one is a request to be in it."
171 [name]197 [name]
172 (reset! current name)198 (reset! current name)
173- (reset! screen :chat)199+ ;; On a wide window the conversation lives in the chats screen's second
200+ ;; pane, beside the list; :chat is the narrow window's way of showing it
201+ ;; instead of the list, and there is nothing there to trade it for.
202+ (reset! screen (if (wide?) :chats :chat))
174 ;; A picker belongs to the message it was opened on; carrying it into another203 ;; A picker belongs to the message it was opened on; carrying it into another
175 ;; buffer would offer to react to something that is no longer on screen.204 ;; buffer would offer to react to something that is no longer on screen.
176 (reset! reacting nil)205 (reset! reacting nil)