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>
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 | ||
| 143 | 143 | `frq.actions`. `frq.app` requires both and the desktop draws them — verified in |
| 144 | 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, 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. | |
| 157 | 166 | |
| 158 | 167 | ## The order to do the rest in |
| 159 | 168 | |
| @@ -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 in | 143 | `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, and | 146 | +The phone draws both. |
| 147 | -the gap is in `frq.hiccup` rather than in the screen: `:fill-height` is iced's | 147 | + |
| 148 | -`Length::Fill`, the space left over, which is Flutter's `Expanded` — not a | 148 | +`Length::Fill` is the whole of what the renderer was missing, in two |
| 149 | -taller `mainAxisSize`. Getting that onto the right children without breaking | 149 | +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 was | 150 | +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 its | 153 | +it. |
| 154 | -row, which in a Row is `Expanded`; the connect screen's server boxes carry | 154 | + |
| 155 | -widths and came out fine, and the chats screen's join box carries none and did | 155 | +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 in | 167 | ## The order to do the rest in |
| 159 | 168 | ||
modified
flutter/src/frq/hiccup.cljd +59 -5 | @@ -70,6 +70,29 @@ | ||
| 70 | 70 | |
| 71 | 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 | 96 | (defn- children |
| 74 | 97 | "Flatten seqs, drop nils. `(for [...] ...)` in a component yields a seq in |
| 75 | 98 | the child position and glimmer splices it; so does this." |
| @@ -83,6 +106,19 @@ | ||
| 83 | 106 | |
| 84 | 107 | ;; ------------------------------------------------------------------ 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 | 122 | (defn- txt |
| 87 | 123 | [ctx s size color & {:keys [weight]}] |
| 88 | 124 | (m/Text (str s) |
| @@ -144,7 +180,19 @@ | ||
| 144 | 180 | .children cs))] |
| 145 | 181 | (case tag |
| 146 | 182 | :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 | 196 | (if-let [w (:width-request p)] |
| 149 | 197 | (m/SizedBox .width (dbl w 0.0) .child c) |
| 150 | 198 | c)) |
| @@ -157,21 +205,27 @@ | ||
| 157 | 205 | ;; The page looked blank and nothing was logged, because nothing was |
| 158 | 206 | ;; wrong: the content was exactly where it had been asked to go. |
| 159 | 207 | :page |
| 160 | - (m/Align | |
| 208 | + (m/SingleChildScrollView | |
| 209 | + .child | |
| 210 | + (m/Align | |
| 161 | 211 | .alignment m/Alignment.topCenter |
| 162 | 212 | .heightFactor 1.0 |
| 163 | 213 | .child (m/ConstrainedBox |
| 164 | 214 | .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0)) |
| 165 | 215 | .child (m/Padding |
| 166 | 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 | 219 | :hbox |
| 170 | 220 | (m/Row |
| 171 | 221 | .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) | |
| 173 | 227 | .spacing (dbl (:spacing p) 0.0) |
| 174 | - .children kids) | |
| 228 | + .children (flexed fills-row? (body node))) | |
| 175 | 229 | |
| 176 | 230 | :label (txt ctx (:label p "") t/text-body t/on-bg) |
| 177 | 231 | :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- children | 96 | (defn- children |
| 74 | "Flatten seqs, drop nils. `(for [...] ...)` in a component yields a seq in | 97 | "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 | ;; ------------------------------------------------------------------ text | 107 | ;; ------------------------------------------------------------------ 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- txt | 122 | (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 tag | 181 | (case tag |
| 146 | :vbox | 182 | :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 was | 205 | ;; 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 | :page | 207 | :page |
| 160 | - (m/Align | 208 | + (m/SingleChildScrollView |
| 209 | + .child | ||
| 210 | + (m/Align | ||
| 161 | .alignment m/Alignment.topCenter | 211 | .alignment m/Alignment.topCenter |
| 162 | .heightFactor 1.0 | 212 | .heightFactor 1.0 |
| 163 | .child (m/ConstrainedBox | 213 | .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/Padding | 215 | .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 | :hbox | 219 | :hbox |
| 170 | (m/Row | 220 | (m/Row |
| 171 | .crossAxisAlignment m/CrossAxisAlignment.center | 221 | .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) | ||
| 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 @@ | ||
| 29 | 29 | [frq.store :as store] |
| 30 | 30 | [frq.actions :as actions] |
| 31 | 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 | 37 | (defonce ^:private lines (atom [])) |
| 35 | 38 | (defonce ^:private conn (atom nil)) |
| 36 | 39 | |
| 37 | 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 | 65 | (defn- note! [m] |
| 40 | 66 | (reset! last-line (str (:command m) " " (last (:params m)))) |
| 41 | 67 | (swap! lines (fn [v] (vec (take-last 8 (conj v m)))))) |
| @@ -106,6 +132,7 @@ | ||
| 106 | 132 | :tls? (boolean @cells/form-tls?) |
| 107 | 133 | :on-msg (fn [m] |
| 108 | 134 | (note! m) |
| 135 | + (room! m) | |
| 109 | 136 | (let [cmd (str (:command m)) |
| 110 | 137 | text (str (last (:params m)))] |
| 111 | 138 | (cond |
| @@ -120,7 +147,9 @@ | ||
| 120 | 147 | ;; puts the Connect button back |
| 121 | 148 | ;; and looks exactly like a |
| 122 | 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 | 154 | (contains? registration-failed cmd) |
| 126 | 155 | (fail! (if (= "433" cmd) |
| @@ -199,11 +228,30 @@ | ||
| 199 | 228 | |
| 200 | 229 | (defn ^:async main [] |
| 201 | 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 | 239 | (let [dir (.-path (await (pp/getApplicationSupportDirectory)))] |
| 203 | 240 | (host/install! dir) |
| 204 | 241 | ;; What the shared screen calls. The desktop installs frq.state's |
| 205 | 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 | 255 | (f/run |
| 208 | 256 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 209 | 257 | .home |
| @@ -230,9 +278,11 @@ | ||
| 230 | 278 | :watch [st c-status er c-error cn c-connecting am c-mode |
| 231 | 279 | fh c-handle nk c-nick hs c-host pt c-port tl c-tls |
| 232 | 280 | 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 | (cond | 138 | (cond |
| @@ -120,7 +147,9 @@ | |||
| 120 | ;; puts the Connect button back | 147 | ;; puts the Connect button back |
| 121 | ;; and looks exactly like a | 148 | ;; 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's | 241 | ;; 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/run | 255 | (f/run |
| 208 | (m/MaterialApp .title "frq" .theme (t/app-theme)) | 256 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 209 | .home | 257 | .home |
| @@ -230,9 +278,11 @@ | |||
| 230 | :watch [st c-status er c-error cn c-connecting am c-mode | 278 | :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-tls | 279 | 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/SingleChildScrollView | 281 | + ;; 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 the | 282 | + ;; `: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])))))) | ||