Open a channel, and give it somewhere to open onto
Open was a no-op because the phone's `open-channel!` was one line of the desktop's: it set `current` and stopped. `frq.state`'s also goes to the conversation — and joins the channel on the way, because a row can outlive the membership behind it, so opening one is a request to be in it. Both are here now, with the buffer made if the list had never seen it. And somewhere to land. `frq.app`'s chat screen is 230 lines over about thirty cells — replies, edits, reactions, the emoji picker, avatars, the people panel, a call bar — and porting it is its own piece of work. What the list opens onto meanwhile is the part that matters: who said what, a way back, and a box to answer in. `draft` is phone-local rather than shared, and deliberately so: the shared one carries a reply, an edit and an attachment with it, and that is the chat screen's port rather than this. Send composes a PRIVMSG and echoes it locally, since the server does not send our own lines back. It is not verified on the device: #test has other people in it and posting to it was not what was asked for. Verified: Open goes to #test and shows its backlog with senders, the tab bar and compose bar stay put, and the TUI is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8c903d8 parent: ff774d1 modified
flutter/src/frq/main.cljd +67 -2 | @@ -226,6 +226,55 @@ | ||
| 226 | 226 | [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]] |
| 227 | 227 | [:dim-label {:label "The chats screen is next: it wants rooms, messages and avatars, none of which are ported yet."}]]) |
| 228 | 228 | |
| 229 | +(defonce ^:private draft | |
| 230 | + ;; Phone-local for now. `frq.state/draft` is the shared one this becomes | |
| 231 | + ;; when `frq.app`'s chat screen is ported — it carries replies, edits and an | |
| 232 | + ;; attachment with it, and none of that is here. | |
| 233 | + (atom "")) | |
| 234 | + | |
| 235 | +(defn- send-draft! [] | |
| 236 | + (let [text (str @draft) | |
| 237 | + room (str @cells/current)] | |
| 238 | + (when (and (seq text) (seq room) @conn) | |
| 239 | + (net/send-line! @conn (str "PRIVMSG " room " :" text)) | |
| 240 | + ;; Echoed locally: the server does not send our own PRIVMSG back. | |
| 241 | + (swap! cells/channels update room | |
| 242 | + #(update % :messages conj {:from @cells/form-nick :text text})) | |
| 243 | + (reset! draft "")))) | |
| 244 | + | |
| 245 | +(defn- chat-screen | |
| 246 | + "One conversation, until `frq.app`'s own is ported. | |
| 247 | + | |
| 248 | + That one is 230 lines over about thirty cells — replies, edits, reactions, | |
| 249 | + the emoji picker, avatars, the people panel and a call bar. This is the part | |
| 250 | + the list opens onto: who said what, and a box to answer in." | |
| 251 | + [] | |
| 252 | + (let [room (str @cells/current) | |
| 253 | + buffer (get @cells/channels room)] | |
| 254 | + [:vbox {:fill-height true :margin 12 :spacing 8} | |
| 255 | + [:vbox {:key :head :spacing 8} | |
| 256 | + [:hbox {:spacing 8} | |
| 257 | + [:button {:label "← Chats" :on-click #(reset! cells/screen :chats)}] | |
| 258 | + [:title-2 {:label room}]] | |
| 259 | + [:separator {}]] | |
| 260 | + [:vbox {:key :list :fill-height true} | |
| 261 | + [:scroll {:spacing 8} | |
| 262 | + (if (empty? (:messages buffer)) | |
| 263 | + [:dim-label {:label "No messages yet"}] | |
| 264 | + (for [[i m] (map-indexed vector (:messages buffer))] | |
| 265 | + [:vbox {:key i :spacing 2} | |
| 266 | + [:dim-label {:label (str (:from m))}] | |
| 267 | + [:label {:label (str (:text m))}]]))]] | |
| 268 | + [:vbox {:key :foot :spacing 8} | |
| 269 | + [:separator {}] | |
| 270 | + [:hbox {:spacing 8} | |
| 271 | + [:button {:label "Send" :kind :primary :on-click #(send-draft!)}] | |
| 272 | + [:entry {:key :draft | |
| 273 | + :text @draft | |
| 274 | + :placeholder "Message" | |
| 275 | + :on-change #(reset! draft %) | |
| 276 | + :on-activate #(send-draft!)}]]]])) | |
| 277 | + | |
| 229 | 278 | (defn ^:async main [] |
| 230 | 279 | (m/WidgetsFlutterBinding.ensureInitialized) |
| 231 | 280 | ;; Layout errors do not come back as exceptions — they happen after the |
| @@ -251,7 +300,22 @@ | ||
| 251 | 300 | :leave-channel! (fn [name] |
| 252 | 301 | (when-let [c @conn] (net/send-line! c (str "PART " name))) |
| 253 | 302 | (swap! cells/channels dissoc name)) |
| 254 | - :open-channel! (fn [name] (reset! cells/current name))}) | |
| 303 | + :open-channel! (fn [name] | |
| 304 | + (reset! cells/current name) | |
| 305 | + ;; `frq.state/open-channel!` does this and more: it | |
| 306 | + ;; marks the buffer read, remembers the room list and | |
| 307 | + ;; asks for NAMES. What it also does, and what made | |
| 308 | + ;; Open look like a no-op here, is go to the | |
| 309 | + ;; conversation — and join it on the way, because a row | |
| 310 | + ;; can outlive the membership behind it. | |
| 311 | + (swap! cells/channels update name | |
| 312 | + #(merge {:name name :messages [] :unread 0} %)) | |
| 313 | + (reset! cells/screen :chat) | |
| 314 | + (let [buffer (get @cells/channels name)] | |
| 315 | + (when (and @conn | |
| 316 | + (.startsWith (str name) "#") | |
| 317 | + (not (:joined? buffer))) | |
| 318 | + (net/send-line! @conn (str "JOIN " name)))))}) | |
| 255 | 319 | (f/run |
| 256 | 320 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 257 | 321 | .home |
| @@ -277,7 +341,7 @@ | ||
| 277 | 341 | c-screen cells/screen] |
| 278 | 342 | :watch [st c-status er c-error cn c-connecting am c-mode |
| 279 | 343 | fh c-handle nk c-nick hs c-host pt c-port tl c-tls |
| 280 | - ap c-apppw bt c-broker lu c-login ls lines sc c-screen] | |
| 344 | + ap c-apppw bt c-broker lu c-login ls lines sc c-screen dr draft ch cells/channels cu cells/current] | |
| 281 | 345 | ;; No scroll view around the screen. A `:page` scrolls itself now, and a |
| 282 | 346 | ;; `:vbox :fill-height` wants the bounded height the Scaffold gives it — |
| 283 | 347 | ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat |
| @@ -285,4 +349,5 @@ | ||
| 285 | 349 | (h/render (case @cells/screen |
| 286 | 350 | :connect [connect/connect-screen] |
| 287 | 351 | :chats [chats/chats-screen] |
| 352 | + :chat [chat-screen] | |
| 288 | 353 | [connected-screen])))))) |
| @@ -226,6 +226,55 @@ | |||
| 226 | [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]] | 226 | [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]] |
| 227 | [:dim-label {:label "The chats screen is next: it wants rooms, messages and avatars, none of which are ported yet."}]]) | 227 | [:dim-label {:label "The chats screen is next: it wants rooms, messages and avatars, none of which are ported yet."}]]) |
| 228 | 228 | ||
| 229 | +(defonce ^:private draft | ||
| 230 | + ;; Phone-local for now. `frq.state/draft` is the shared one this becomes | ||
| 231 | + ;; when `frq.app`'s chat screen is ported — it carries replies, edits and an | ||
| 232 | + ;; attachment with it, and none of that is here. | ||
| 233 | + (atom "")) | ||
| 234 | + | ||
| 235 | +(defn- send-draft! [] | ||
| 236 | + (let [text (str @draft) | ||
| 237 | + room (str @cells/current)] | ||
| 238 | + (when (and (seq text) (seq room) @conn) | ||
| 239 | + (net/send-line! @conn (str "PRIVMSG " room " :" text)) | ||
| 240 | + ;; Echoed locally: the server does not send our own PRIVMSG back. | ||
| 241 | + (swap! cells/channels update room | ||
| 242 | + #(update % :messages conj {:from @cells/form-nick :text text})) | ||
| 243 | + (reset! draft "")))) | ||
| 244 | + | ||
| 245 | +(defn- chat-screen | ||
| 246 | + "One conversation, until `frq.app`'s own is ported. | ||
| 247 | + | ||
| 248 | + That one is 230 lines over about thirty cells — replies, edits, reactions, | ||
| 249 | + the emoji picker, avatars, the people panel and a call bar. This is the part | ||
| 250 | + the list opens onto: who said what, and a box to answer in." | ||
| 251 | + [] | ||
| 252 | + (let [room (str @cells/current) | ||
| 253 | + buffer (get @cells/channels room)] | ||
| 254 | + [:vbox {:fill-height true :margin 12 :spacing 8} | ||
| 255 | + [:vbox {:key :head :spacing 8} | ||
| 256 | + [:hbox {:spacing 8} | ||
| 257 | + [:button {:label "← Chats" :on-click #(reset! cells/screen :chats)}] | ||
| 258 | + [:title-2 {:label room}]] | ||
| 259 | + [:separator {}]] | ||
| 260 | + [:vbox {:key :list :fill-height true} | ||
| 261 | + [:scroll {:spacing 8} | ||
| 262 | + (if (empty? (:messages buffer)) | ||
| 263 | + [:dim-label {:label "No messages yet"}] | ||
| 264 | + (for [[i m] (map-indexed vector (:messages buffer))] | ||
| 265 | + [:vbox {:key i :spacing 2} | ||
| 266 | + [:dim-label {:label (str (:from m))}] | ||
| 267 | + [:label {:label (str (:text m))}]]))]] | ||
| 268 | + [:vbox {:key :foot :spacing 8} | ||
| 269 | + [:separator {}] | ||
| 270 | + [:hbox {:spacing 8} | ||
| 271 | + [:button {:label "Send" :kind :primary :on-click #(send-draft!)}] | ||
| 272 | + [:entry {:key :draft | ||
| 273 | + :text @draft | ||
| 274 | + :placeholder "Message" | ||
| 275 | + :on-change #(reset! draft %) | ||
| 276 | + :on-activate #(send-draft!)}]]]])) | ||
| 277 | + | ||
| 229 | (defn ^:async main [] | 278 | (defn ^:async main [] |
| 230 | (m/WidgetsFlutterBinding.ensureInitialized) | 279 | (m/WidgetsFlutterBinding.ensureInitialized) |
| 231 | ;; Layout errors do not come back as exceptions — they happen after the | 280 | ;; Layout errors do not come back as exceptions — they happen after the |
| @@ -251,7 +300,22 @@ | |||
| 251 | :leave-channel! (fn [name] | 300 | :leave-channel! (fn [name] |
| 252 | (when-let [c @conn] (net/send-line! c (str "PART " name))) | 301 | (when-let [c @conn] (net/send-line! c (str "PART " name))) |
| 253 | (swap! cells/channels dissoc name)) | 302 | (swap! cells/channels dissoc name)) |
| 254 | - :open-channel! (fn [name] (reset! cells/current name))}) | 303 | + :open-channel! (fn [name] |
| 304 | + (reset! cells/current name) | ||
| 305 | + ;; `frq.state/open-channel!` does this and more: it | ||
| 306 | + ;; marks the buffer read, remembers the room list and | ||
| 307 | + ;; asks for NAMES. What it also does, and what made | ||
| 308 | + ;; Open look like a no-op here, is go to the | ||
| 309 | + ;; conversation — and join it on the way, because a row | ||
| 310 | + ;; can outlive the membership behind it. | ||
| 311 | + (swap! cells/channels update name | ||
| 312 | + #(merge {:name name :messages [] :unread 0} %)) | ||
| 313 | + (reset! cells/screen :chat) | ||
| 314 | + (let [buffer (get @cells/channels name)] | ||
| 315 | + (when (and @conn | ||
| 316 | + (.startsWith (str name) "#") | ||
| 317 | + (not (:joined? buffer))) | ||
| 318 | + (net/send-line! @conn (str "JOIN " name)))))}) | ||
| 255 | (f/run | 319 | (f/run |
| 256 | (m/MaterialApp .title "frq" .theme (t/app-theme)) | 320 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 257 | .home | 321 | .home |
| @@ -277,7 +341,7 @@ | |||
| 277 | c-screen cells/screen] | 341 | c-screen cells/screen] |
| 278 | :watch [st c-status er c-error cn c-connecting am c-mode | 342 | :watch [st c-status er c-error cn c-connecting am c-mode |
| 279 | fh c-handle nk c-nick hs c-host pt c-port tl c-tls | 343 | fh c-handle nk c-nick hs c-host pt c-port tl c-tls |
| 280 | - ap c-apppw bt c-broker lu c-login ls lines sc c-screen] | 344 | + ap c-apppw bt c-broker lu c-login ls lines sc c-screen dr draft ch cells/channels cu cells/current] |
| 281 | ;; No scroll view around the screen. A `:page` scrolls itself now, and a | 345 | ;; 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 — | 346 | ;; `:vbox :fill-height` wants the bounded height the Scaffold gives it — |
| 283 | ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat | 347 | ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat |
| @@ -285,4 +349,5 @@ | |||
| 285 | (h/render (case @cells/screen | 349 | (h/render (case @cells/screen |
| 286 | :connect [connect/connect-screen] | 350 | :connect [connect/connect-screen] |
| 287 | :chats [chats/chats-screen] | 351 | :chats [chats/chats-screen] |
| 352 | + :chat [chat-screen] | ||
| 288 | [connected-screen])))))) | 353 | [connected-screen])))))) |