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

Take what is left: fill-height, and the chats screen on the phone

Length::Fill was the whole of it, in two directions. `:fill-height` down a
column and a width-less `:entry` across a row are the same instruction — take
the space that is left — and in Flutter that is Expanded, not a bigger
mainAxisSize. A band that says only `max` is handed loose constraints by its
parent, asks for infinity, and takes the screen down with it.

So `flexed` wraps whichever children fill: `fills-column?` for a column,
`fills-row?` for a row, and a Row holding one is `max` so it has width to
divide. `:page` scrolls itself, `:vbox` honours `:margin` and `:fill-height`,
and nothing wraps the screen any more — a scroll view around the tree is
precisely what takes away the bounded height a fill-height band needs.

The phone draws frq.app's chats screen now: the title with the nick, the join
and search boxes, #test with a real preview off the wire, Open and Close, and
the tab bar pinned to the bottom by the band above it. The same file the
desktop draws, verified again in the TUI both narrow and wide.

A little of apply-msg! comes with it — JOIN makes a room and PRIVMSG gives it
a last line, which is what the list reads. Not a port of that thousand-line
reducer; the two messages the conversation list needs.

The method is the part worth keeping. Four attempts at this went past by
deploying a guess, because a layout error is invisible to every instinct: it
happens after the build, so no try sees it, `(catch Object ...)` does not
catch it, the red box does not appear and the log stays empty.
FlutterError.onError is where they go. Installing that handler in frq.main
should have been the first move rather than the tenth, and it stays in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-11T21:56:17-07:00 Browse files
ff774d1 parent: 478a726
modified flutter/README.md +20 -11
@@ -143,17 +143,26 @@ metrics in `frq.metrics` and the things a screen cannot do itself behind
143143 `frq.actions`. `frq.app` requires both and the desktop draws them — verified in
144144 the TUI, including the conversation list with its rooms and previews.
145145
146-The phone draws the connect screen. It does not draw the chats one yet, and
147-the gap is in `frq.hiccup` rather than in the screen: `:fill-height` is iced's
148-`Length::Fill`, the space left over, which is Flutter's `Expanded` — not a
149-taller `mainAxisSize`. Getting that onto the right children without breaking
150-the bands around them is unfinished; the attempt blanked the screen and was
151-reverted rather than shipped half-working.
152-
153-`:entry` is the same lesson twice. An entry with no `:width-request` fills its
154-row, which in a Row is `Expanded`; the connect screen's server boxes carry
155-widths and came out fine, and the chats screen's join box carries none and did
156-not.
146+The phone draws both.
147+
148+`Length::Fill` is the whole of what the renderer was missing, in two
149+directions. `:fill-height` down a column and a width-less `:entry` across a
150+row are the same instruction — *take what is left* — and that is Flutter's
151+`Expanded`, not a bigger `mainAxisSize`. A band that says only `max` is handed
152+loose constraints by its parent, asks for infinity, and takes the screen with
153+it.
154+
155+So `flexed` wraps whichever children fill, with `fills-column?` for a column
156+and `fills-row?` for a row, and a Row holding one is `max` so it has width to
157+divide. A `:page` scrolls itself and a `:vbox :fill-height` takes the bounded
158+height the Scaffold gives it — which is why nothing wraps the screen any more:
159+a scroll view around the tree is exactly what takes that bound away.
160+
161+What made this expensive was looking for it as an exception. A layout error
162+happens after the build: no `try` sees it, `(catch Object ...)` sees it, the
163+red error box does not appear, and the log stays empty. `FlutterError.onError`
164+is where they go, and installing that handler in `frq.main` should have been
165+the first move rather than the tenth.
157166
158167 ## The order to do the rest in
159168
@@ -143,17 +143,26 @@ metrics in `frq.metrics` and the things a screen cannot do itself behind
143 `frq.actions`. `frq.app` requires both and the desktop draws them — verified in143 `frq.actions`. `frq.app` requires both and the desktop draws them — verified in
144 the TUI, including the conversation list with its rooms and previews.144 the TUI, including the conversation list with its rooms and previews.
145 145
146-The phone draws the connect screen. It does not draw the chats one yet, and146+The phone draws both.
147-the gap is in `frq.hiccup` rather than in the screen: `:fill-height` is iced's147+
148-`Length::Fill`, the space left over, which is Flutter's `Expanded` — not a148+`Length::Fill` is the whole of what the renderer was missing, in two
149-taller `mainAxisSize`. Getting that onto the right children without breaking149+directions. `:fill-height` down a column and a width-less `:entry` across a
150-the bands around them is unfinished; the attempt blanked the screen and was150+row are the same instruction — *take what is left* — and that is Flutter's
151-reverted rather than shipped half-working.151+`Expanded`, not a bigger `mainAxisSize`. A band that says only `max` is handed
152-152+loose constraints by its parent, asks for infinity, and takes the screen with
153-`:entry` is the same lesson twice. An entry with no `:width-request` fills its153+it.
154-row, which in a Row is `Expanded`; the connect screen's server boxes carry154+
155-widths and came out fine, and the chats screen's join box carries none and did155+So `flexed` wraps whichever children fill, with `fills-column?` for a column
156-not.156+and `fills-row?` for a row, and a Row holding one is `max` so it has width to
157+divide. A `:page` scrolls itself and a `:vbox :fill-height` takes the bounded
158+height the Scaffold gives it — which is why nothing wraps the screen any more:
159+a scroll view around the tree is exactly what takes that bound away.
160+
161+What made this expensive was looking for it as an exception. A layout error
162+happens after the build: no `try` sees it, `(catch Object ...)` sees it, the
163+red error box does not appear, and the log stays empty. `FlutterError.onError`
164+is where they go, and installing that handler in `frq.main` should have been
165+the first move rather than the tenth.
157 166
158 ## The order to do the rest in167 ## The order to do the rest in
159 168
modified flutter/src/frq/hiccup.cljd +59 -5
@@ -70,6 +70,29 @@
7070
7171 (declare render)
7272
73+(defn- fills-row?
74+ "Whether a node takes the width its row has left over.
75+
76+ jolt-cosmic gives an entry Length::Fixed(w) when it has a width and
77+ Length::Fill otherwise; Fill in a Row is Flutter's Expanded. A TextField that
78+ is neither is offered unbounded width, which is a layout error."
79+ [node]
80+ (and (vector? node)
81+ (= :entry (first node))
82+ (let [p (second node)]
83+ (or (not (map? p)) (nil? (:width-request p)) (:hexpand p)))))
84+
85+(defn- fills-column?
86+ "Whether a node takes the height its column has left over.
87+
88+ `:fill-height` is Length::Fill down the other axis Expanded, not a taller
89+ mainAxisSize. `:scroll` is excluded: it returns an Expanded of its own."
90+ [node]
91+ (and (vector? node)
92+ (not= :scroll (first node))
93+ (let [p (second node)]
94+ (and (map? p) (:fill-height p)))))
95+
7396 (defn- children
7497 "Flatten seqs, drop nils. `(for [...] ...)` in a component yields a seq in
7598 the child position and glimmer splices it; so does this."
@@ -83,6 +106,19 @@
83106
84107 ;; ------------------------------------------------------------------ text
85108
109+(defn- flexed
110+ "Children of a Flex, with the ones that fill wrapped in Expanded.
111+
112+ `fills?` says which, so a Row asks about width and a Column about height."
113+ [fills? nodes]
114+ (persistent!
115+ (reduce (fn [acc n]
116+ (cond (nil? n) acc
117+ (seq? n) (reduce conj! acc (flexed fills? n))
118+ (fills? n) (conj! acc (m/Expanded .child (render n)))
119+ :else (conj! acc (render n))))
120+ (transient []) nodes)))
121+
86122 (defn- txt
87123 [ctx s size color & {:keys [weight]}]
88124 (m/Text (str s)
@@ -144,7 +180,19 @@
144180 .children cs))]
145181 (case tag
146182 :vbox
147- (let [c (col (dbl (:spacing p) 0.0) kids)]
183+ ;; `:fill-height` is what pins a tab bar to the bottom: the band above
184+ ;; it asks for the rest of the screen, and a column sized to its
185+ ;; contents has no rest to give.
186+ (let [c (m/Column
187+ .crossAxisAlignment m/CrossAxisAlignment.start
188+ .mainAxisSize (if (:fill-height p)
189+ m/MainAxisSize.max
190+ m/MainAxisSize.min)
191+ .spacing (dbl (:spacing p) 0.0)
192+ .children (flexed fills-column? (body node)))
193+ c (if-let [mg (:margin p)]
194+ (m/Padding .padding (m/EdgeInsets.all (dbl mg 0.0)) .child c)
195+ c)]
148196 (if-let [w (:width-request p)]
149197 (m/SizedBox .width (dbl w 0.0) .child c)
150198 c))
@@ -157,21 +205,27 @@
157205 ;; The page looked blank and nothing was logged, because nothing was
158206 ;; wrong: the content was exactly where it had been asked to go.
159207 :page
160- (m/Align
208+ (m/SingleChildScrollView
209+ .child
210+ (m/Align
161211 .alignment m/Alignment.topCenter
162212 .heightFactor 1.0
163213 .child (m/ConstrainedBox
164214 .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0))
165215 .child (m/Padding
166216 .padding (m/EdgeInsets.all t/space-s)
167- .child (col t/space-xs kids))))
217+ .child (col t/space-xs kids)))))
168218
169219 :hbox
170220 (m/Row
171221 .crossAxisAlignment m/CrossAxisAlignment.center
172- .mainAxisSize m/MainAxisSize.min
222+ ;; A row holding something that fills has to be given the width to
223+ ;; divide, so it is max rather than min whenever a child asks.
224+ .mainAxisSize (if (some fills-row? (body node))
225+ m/MainAxisSize.max
226+ m/MainAxisSize.min)
173227 .spacing (dbl (:spacing p) 0.0)
174- .children kids)
228+ .children (flexed fills-row? (body node)))
175229
176230 :label (txt ctx (:label p "") t/text-body t/on-bg)
177231 :dim-label (txt ctx (:label p "") t/text-caption t/dim)
@@ -70,6 +70,29 @@
70 70
71 (declare render)71 (declare render)
72 72
73+(defn- fills-row?
74+ "Whether a node takes the width its row has left over.
75+
76+ jolt-cosmic gives an entry Length::Fixed(w) when it has a width and
77+ Length::Fill otherwise; Fill in a Row is Flutter's Expanded. A TextField that
78+ is neither is offered unbounded width, which is a layout error."
79+ [node]
80+ (and (vector? node)
81+ (= :entry (first node))
82+ (let [p (second node)]
83+ (or (not (map? p)) (nil? (:width-request p)) (:hexpand p)))))
84+
85+(defn- fills-column?
86+ "Whether a node takes the height its column has left over.
87+
88+ `:fill-height` is Length::Fill down the other axis Expanded, not a taller
89+ mainAxisSize. `:scroll` is excluded: it returns an Expanded of its own."
90+ [node]
91+ (and (vector? node)
92+ (not= :scroll (first node))
93+ (let [p (second node)]
94+ (and (map? p) (:fill-height p)))))
95+
73 (defn- children96 (defn- children
74 "Flatten seqs, drop nils. `(for [...] ...)` in a component yields a seq in97 "Flatten seqs, drop nils. `(for [...] ...)` in a component yields a seq in
75 the child position and glimmer splices it; so does this."98 the child position and glimmer splices it; so does this."
@@ -83,6 +106,19 @@
83 106
84 ;; ------------------------------------------------------------------ text107 ;; ------------------------------------------------------------------ text
85 108
109+(defn- flexed
110+ "Children of a Flex, with the ones that fill wrapped in Expanded.
111+
112+ `fills?` says which, so a Row asks about width and a Column about height."
113+ [fills? nodes]
114+ (persistent!
115+ (reduce (fn [acc n]
116+ (cond (nil? n) acc
117+ (seq? n) (reduce conj! acc (flexed fills? n))
118+ (fills? n) (conj! acc (m/Expanded .child (render n)))
119+ :else (conj! acc (render n))))
120+ (transient []) nodes)))
121+
86 (defn- txt122 (defn- txt
87 [ctx s size color & {:keys [weight]}]123 [ctx s size color & {:keys [weight]}]
88 (m/Text (str s)124 (m/Text (str s)
@@ -144,7 +180,19 @@
144 .children cs))]180 .children cs))]
145 (case tag181 (case tag
146 :vbox182 :vbox
147- (let [c (col (dbl (:spacing p) 0.0) kids)]183+ ;; `:fill-height` is what pins a tab bar to the bottom: the band above
184+ ;; it asks for the rest of the screen, and a column sized to its
185+ ;; contents has no rest to give.
186+ (let [c (m/Column
187+ .crossAxisAlignment m/CrossAxisAlignment.start
188+ .mainAxisSize (if (:fill-height p)
189+ m/MainAxisSize.max
190+ m/MainAxisSize.min)
191+ .spacing (dbl (:spacing p) 0.0)
192+ .children (flexed fills-column? (body node)))
193+ c (if-let [mg (:margin p)]
194+ (m/Padding .padding (m/EdgeInsets.all (dbl mg 0.0)) .child c)
195+ c)]
148 (if-let [w (:width-request p)]196 (if-let [w (:width-request p)]
149 (m/SizedBox .width (dbl w 0.0) .child c)197 (m/SizedBox .width (dbl w 0.0) .child c)
150 c))198 c))
@@ -157,21 +205,27 @@
157 ;; The page looked blank and nothing was logged, because nothing was205 ;; The page looked blank and nothing was logged, because nothing was
158 ;; wrong: the content was exactly where it had been asked to go.206 ;; wrong: the content was exactly where it had been asked to go.
159 :page207 :page
160- (m/Align208+ (m/SingleChildScrollView
209+ .child
210+ (m/Align
161 .alignment m/Alignment.topCenter211 .alignment m/Alignment.topCenter
162 .heightFactor 1.0212 .heightFactor 1.0
163 .child (m/ConstrainedBox213 .child (m/ConstrainedBox
164 .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0))214 .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0))
165 .child (m/Padding215 .child (m/Padding
166 .padding (m/EdgeInsets.all t/space-s)216 .padding (m/EdgeInsets.all t/space-s)
167- .child (col t/space-xs kids))))217+ .child (col t/space-xs kids)))))
168 218
169 :hbox219 :hbox
170 (m/Row220 (m/Row
171 .crossAxisAlignment m/CrossAxisAlignment.center221 .crossAxisAlignment m/CrossAxisAlignment.center
172- .mainAxisSize m/MainAxisSize.min222+ ;; A row holding something that fills has to be given the width to
223+ ;; divide, so it is max rather than min whenever a child asks.
224+ .mainAxisSize (if (some fills-row? (body node))
225+ m/MainAxisSize.max
226+ m/MainAxisSize.min)
173 .spacing (dbl (:spacing p) 0.0)227 .spacing (dbl (:spacing p) 0.0)
174- .children kids)228+ .children (flexed fills-row? (body node)))
175 229
176 :label (txt ctx (:label p "") t/text-body t/on-bg)230 :label (txt ctx (:label p "") t/text-body t/on-bg)
177 :dim-label (txt ctx (:label p "") t/text-caption t/dim)231 :dim-label (txt ctx (:label p "") t/text-caption t/dim)
modified flutter/src/frq/main.cljd +59 -9
@@ -29,13 +29,39 @@
2929 [frq.store :as store]
3030 [frq.actions :as actions]
3131 [frq.cells :as cells]
32- [frq.screens.connect :as connect]))
32+ [frq.screens.connect :as connect]
33+ [frq.screens.chats :as chats]
34+ [frq.rooms :as rooms]
35+ [frq.irc.parse :as irc]))
3336
3437 (defonce ^:private lines (atom []))
3538 (defonce ^:private conn (atom nil))
3639
3740 (defonce ^:private last-line (atom nil))
3841
42+(defn- room!
43+ "The little of `frq.state/apply-msg!` the conversation list needs: the two
44+ messages that make a room appear and give it a last line."
45+ [m]
46+ (let [cmd (str (:command m))
47+ params (vec (:params m))
48+ who (irc/nick-of (:prefix m))]
49+ (cond
50+ (= "JOIN" cmd)
51+ (let [name (first params)]
52+ (swap! cells/channels update name
53+ #(merge {:name name :messages [] :unread 0} %
54+ {:joined? true :accessed (clock/now-ms)}))
55+ (reset! cells/current name))
56+
57+ (= "PRIVMSG" cmd)
58+ (let [target (first params)
59+ text (last params)
60+ name (if (rooms/dm? target) who target)]
61+ (swap! cells/channels update name
62+ #(-> (merge {:name name :messages [] :unread 0} %)
63+ (update :messages conj {:from who :text text})))))))
64+
3965 (defn- note! [m]
4066 (reset! last-line (str (:command m) " " (last (:params m))))
4167 (swap! lines (fn [v] (vec (take-last 8 (conj v m))))))
@@ -106,6 +132,7 @@
106132 :tls? (boolean @cells/form-tls?)
107133 :on-msg (fn [m]
108134 (note! m)
135+ (room! m)
109136 (let [cmd (str (:command m))
110137 text (str (last (:params m)))]
111138 (cond
@@ -120,7 +147,9 @@
120147 ;; puts the Connect button back
121148 ;; and looks exactly like a
122149 ;; failure.
123- (reset! cells/screen :chats))
150+ (reset! cells/screen :chats)
151+ (when-let [c @conn]
152+ (net/send-line! c "JOIN #test")))
124153
125154 (contains? registration-failed cmd)
126155 (fail! (if (= "433" cmd)
@@ -199,11 +228,30 @@
199228
200229 (defn ^:async main []
201230 (m/WidgetsFlutterBinding.ensureInitialized)
231+ ;; Layout errors do not come back as exceptions — they happen after the
232+ ;; build, so nothing can catch them, and what they leave is a blank screen
233+ ;; and an empty log. Flutter routes them here instead.
234+ (set! (.-onError m/FlutterError)
235+ (fn [details]
236+ (m/debugPrint (str "frq: FLUTTER ERROR " (.-exception details)))
237+ (m/debugPrint (str "frq: LIBRARY " (.-library details)
238+ " CONTEXT " (.-context details)))))
202239 (let [dir (.-path (await (pp/getApplicationSupportDirectory)))]
203240 (host/install! dir)
204241 ;; What the shared screen calls. The desktop installs frq.state's
205242 ;; reducers here; this installs the phone's.
206- (actions/install! {:connect! connect! :disconnect! disconnect!})
243+ (actions/install!
244+ {:connect! connect!
245+ :disconnect! disconnect!
246+ :connected? (fn [] (some? @conn))
247+ :join! (fn [name]
248+ (when-let [c @conn]
249+ (when (seq (str name))
250+ (net/send-line! c (str "JOIN " name)))))
251+ :leave-channel! (fn [name]
252+ (when-let [c @conn] (net/send-line! c (str "PART " name)))
253+ (swap! cells/channels dissoc name))
254+ :open-channel! (fn [name] (reset! cells/current name))})
207255 (f/run
208256 (m/MaterialApp .title "frq" .theme (t/app-theme))
209257 .home
@@ -230,9 +278,11 @@
230278 :watch [st c-status er c-error cn c-connecting am c-mode
231279 fh c-handle nk c-nick hs c-host pt c-port tl c-tls
232280 ap c-apppw bt c-broker lu c-login ls lines sc c-screen]
233- (m/SingleChildScrollView
234- ;; frq.app's own connect screen, out of common/ — the same file the
235- ;; desktop renders, painted by frq.hiccup in COSMIC's theme.
236- .child (h/render (if (= :connect @cells/screen)
237- [connect/connect-screen]
238- [connected-screen])))))))
281+ ;; No scroll view around the screen. A `:page` scrolls itself now, and a
282+ ;; `:vbox :fill-height` wants the bounded height the Scaffold gives it —
283+ ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat
284+ ;; in an unbounded column.
285+ (h/render (case @cells/screen
286+ :connect [connect/connect-screen]
287+ :chats [chats/chats-screen]
288+ [connected-screen]))))))
@@ -29,13 +29,39 @@
29 [frq.store :as store]29 [frq.store :as store]
30 [frq.actions :as actions]30 [frq.actions :as actions]
31 [frq.cells :as cells]31 [frq.cells :as cells]
32- [frq.screens.connect :as connect]))32+ [frq.screens.connect :as connect]
33+ [frq.screens.chats :as chats]
34+ [frq.rooms :as rooms]
35+ [frq.irc.parse :as irc]))
33 36
34 (defonce ^:private lines (atom []))37 (defonce ^:private lines (atom []))
35 (defonce ^:private conn (atom nil))38 (defonce ^:private conn (atom nil))
36 39
37 (defonce ^:private last-line (atom nil))40 (defonce ^:private last-line (atom nil))
38 41
42+(defn- room!
43+ "The little of `frq.state/apply-msg!` the conversation list needs: the two
44+ messages that make a room appear and give it a last line."
45+ [m]
46+ (let [cmd (str (:command m))
47+ params (vec (:params m))
48+ who (irc/nick-of (:prefix m))]
49+ (cond
50+ (= "JOIN" cmd)
51+ (let [name (first params)]
52+ (swap! cells/channels update name
53+ #(merge {:name name :messages [] :unread 0} %
54+ {:joined? true :accessed (clock/now-ms)}))
55+ (reset! cells/current name))
56+
57+ (= "PRIVMSG" cmd)
58+ (let [target (first params)
59+ text (last params)
60+ name (if (rooms/dm? target) who target)]
61+ (swap! cells/channels update name
62+ #(-> (merge {:name name :messages [] :unread 0} %)
63+ (update :messages conj {:from who :text text})))))))
64+
39 (defn- note! [m]65 (defn- note! [m]
40 (reset! last-line (str (:command m) " " (last (:params m))))66 (reset! last-line (str (:command m) " " (last (:params m))))
41 (swap! lines (fn [v] (vec (take-last 8 (conj v m))))))67 (swap! lines (fn [v] (vec (take-last 8 (conj v m))))))
@@ -106,6 +132,7 @@
106 :tls? (boolean @cells/form-tls?)132 :tls? (boolean @cells/form-tls?)
107 :on-msg (fn [m]133 :on-msg (fn [m]
108 (note! m)134 (note! m)
135+ (room! m)
109 (let [cmd (str (:command m))136 (let [cmd (str (:command m))
110 text (str (last (:params m)))]137 text (str (last (:params m)))]
111 (cond138 (cond
@@ -120,7 +147,9 @@
120 ;; puts the Connect button back147 ;; puts the Connect button back
121 ;; and looks exactly like a148 ;; and looks exactly like a
122 ;; failure.149 ;; failure.
123- (reset! cells/screen :chats))150+ (reset! cells/screen :chats)
151+ (when-let [c @conn]
152+ (net/send-line! c "JOIN #test")))
124 153
125 (contains? registration-failed cmd)154 (contains? registration-failed cmd)
126 (fail! (if (= "433" cmd)155 (fail! (if (= "433" cmd)
@@ -199,11 +228,30 @@
199 228
200 (defn ^:async main []229 (defn ^:async main []
201 (m/WidgetsFlutterBinding.ensureInitialized)230 (m/WidgetsFlutterBinding.ensureInitialized)
231+ ;; Layout errors do not come back as exceptions — they happen after the
232+ ;; build, so nothing can catch them, and what they leave is a blank screen
233+ ;; and an empty log. Flutter routes them here instead.
234+ (set! (.-onError m/FlutterError)
235+ (fn [details]
236+ (m/debugPrint (str "frq: FLUTTER ERROR " (.-exception details)))
237+ (m/debugPrint (str "frq: LIBRARY " (.-library details)
238+ " CONTEXT " (.-context details)))))
202 (let [dir (.-path (await (pp/getApplicationSupportDirectory)))]239 (let [dir (.-path (await (pp/getApplicationSupportDirectory)))]
203 (host/install! dir)240 (host/install! dir)
204 ;; What the shared screen calls. The desktop installs frq.state's241 ;; What the shared screen calls. The desktop installs frq.state's
205 ;; reducers here; this installs the phone's.242 ;; reducers here; this installs the phone's.
206- (actions/install! {:connect! connect! :disconnect! disconnect!})243+ (actions/install!
244+ {:connect! connect!
245+ :disconnect! disconnect!
246+ :connected? (fn [] (some? @conn))
247+ :join! (fn [name]
248+ (when-let [c @conn]
249+ (when (seq (str name))
250+ (net/send-line! c (str "JOIN " name)))))
251+ :leave-channel! (fn [name]
252+ (when-let [c @conn] (net/send-line! c (str "PART " name)))
253+ (swap! cells/channels dissoc name))
254+ :open-channel! (fn [name] (reset! cells/current name))})
207 (f/run255 (f/run
208 (m/MaterialApp .title "frq" .theme (t/app-theme))256 (m/MaterialApp .title "frq" .theme (t/app-theme))
209 .home257 .home
@@ -230,9 +278,11 @@
230 :watch [st c-status er c-error cn c-connecting am c-mode278 :watch [st c-status er c-error cn c-connecting am c-mode
231 fh c-handle nk c-nick hs c-host pt c-port tl c-tls279 fh c-handle nk c-nick hs c-host pt c-port tl c-tls
232 ap c-apppw bt c-broker lu c-login ls lines sc c-screen]280 ap c-apppw bt c-broker lu c-login ls lines sc c-screen]
233- (m/SingleChildScrollView281+ ;; No scroll view around the screen. A `:page` scrolls itself now, and a
234- ;; frq.app's own connect screen, out of common/ — the same file the282+ ;; `:vbox :fill-height` wants the bounded height the Scaffold gives it —
235- ;; desktop renders, painted by frq.hiccup in COSMIC's theme.283+ ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat
236- .child (h/render (if (= :connect @cells/screen)284+ ;; in an unbounded column.
237- [connect/connect-screen]285+ (h/render (case @cells/screen
238- [connected-screen])))))))286+ :connect [connect/connect-screen]
287+ :chats [chats/chats-screen]
288+ [connected-screen]))))))