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

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>
nandi committed 2026-09-11T22:03:23-07:00 Browse files
8c903d8 parent: ff774d1
modified flutter/src/frq/main.cljd +67 -2
@@ -226,6 +226,55 @@
226226 [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]]
227227 [:dim-label {:label "The chats screen is next: it wants rooms, messages and avatars, none of which are ported yet."}]])
228228
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+
229278 (defn ^:async main []
230279 (m/WidgetsFlutterBinding.ensureInitialized)
231280 ;; Layout errors do not come back as exceptions — they happen after the
@@ -251,7 +300,22 @@
251300 :leave-channel! (fn [name]
252301 (when-let [c @conn] (net/send-line! c (str "PART " name)))
253302 (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)))))})
255319 (f/run
256320 (m/MaterialApp .title "frq" .theme (t/app-theme))
257321 .home
@@ -277,7 +341,7 @@
277341 c-screen cells/screen]
278342 :watch [st c-status er c-error cn c-connecting am c-mode
279343 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]
281345 ;; No scroll view around the screen. A `:page` scrolls itself now, and a
282346 ;; `:vbox :fill-height` wants the bounded height the Scaffold gives it —
283347 ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat
@@ -285,4 +349,5 @@
285349 (h/render (case @cells/screen
286350 :connect [connect/connect-screen]
287351 :chats [chats/chats-screen]
352+ :chat [chat-screen]
288353 [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 the280 ;; 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/run319 (f/run
256 (m/MaterialApp .title "frq" .theme (t/app-theme))320 (m/MaterialApp .title "frq" .theme (t/app-theme))
257 .home321 .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-mode342 :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-tls343 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 a345 ;; 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 sat347 ;; wrapping the tree took that away, and `:scroll`'s Expanded then sat
@@ -285,4 +349,5 @@
285 (h/render (case @cells/screen349 (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]))))))