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

main.cljd · 542 lines · 25.5 KBGDScript3 Blame HistoryRaw
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago1(ns frq.main
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago2 "The Flutter entry point: what `frq.cosmic` is to the jolt half.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago3
4 Two jobs, and the first one is why `frq.io.dart` cannot install itself the
5 way `frq.io.jolt` does: the app's storage directory arrives from
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago6 path_provider as a Future, so `main` is async, awaits it, and installs the
7 host before a widget is built. Everything under ../common asks `frq.io` for
8 the filesystem, so nothing shared can run until that has happened.
9
10 `ensureInitialized` first, and not as a formality: path_provider is a
11 platform channel, and a channel used before the binding exists throws.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago12
13 The screens themselves are not written. `frq.app` is 1,834 lines of hiccup
14 over glimmer's widget tags, and glimmer is not here — Flutter brings its own
15 reconciler, so those screens are a rewrite against cljd.flutter rather than a
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago16 port. What this paints is the proof the shared half is alive under a second
17 compiler: the clock and the saved session, read through exactly the
18 namespaces the desktop reads them through."
Tell the truth about TLS on the phone, and stop the guest spinner ba9e7a9 nandi 8d ago19 (:require ["dart:async" :as async]
20 ["package:flutter/material.dart" :as m]
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago21 ["package:path_provider/path_provider.dart" :as pp]
22 [cljd.flutter :as f]
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 8d ago23 [frq.hiccup :as h]
Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 8d ago24 [frq.theme :as t]
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago25 [frq.io.dart :as host]
Sign in with Bluesky on the phone, browser and all 939d2ac nandi 7d ago26 ;; The seam, not the implementation above: `open-url!` is asked for
27 ;; the same way the shared half asks for it.
28 [frq.io :as fio]
Sign on the phone: msgsig shared, crypto seamed d8fadee nandi 7d ago29 [frq.crypto.dart :as crypto-dart]
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago30 [frq.net.dart :as net]
Resolve an identity from the phone, over the shared AT Protocol core f735240 nandi 8d ago31 [frq.atproto.dart :as atproto]
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago32 [frq.clock :as clock]
Share the connect screen, and the cells under it 80eb088 nandi 8d ago33 [frq.store :as store]
34 [frq.actions :as actions]
35 [frq.cells :as cells]
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago36 [frq.screens.connect :as connect]
37 [frq.screens.chats :as chats]
Draw the conversation on the phone c0121fe nandi 7d ago38 [frq.screens.chat :as chat]
Share Discover and Settings, and the frame around them 49fb82f nandi 7d ago39 [frq.screens.settings :as settings]
Share the root, and with it the last of the screens 433c3c8 nandi 7d ago40 [frq.screens.app :as screens]
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago41 [frq.rooms :as rooms]
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago42 [frq.members :as members]
43 [frq.oauth.core :as oauth]
Sign in with Bluesky on the phone, browser and all 939d2ac nandi 7d ago44 [frq.oauth.dart :as oauth-dart]
Sign in from the phone with the handshake both halves now share dd67233 nandi 7d ago45 [frq.irc.parse :as irc]
46 [frq.irc.handshake :as handshake]))
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 8d ago47
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago48(defonce ^:private lines (atom []))
49(defonce ^:private conn (atom nil))
Paint glimmer's hiccup with Flutter, rather than porting the screens bc877e5 nandi 8d ago50
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago51(defonce ^:private last-line (atom nil))
52
Ask for the backlog on the phone, as the desktop does 7dff3cf nandi 7d ago53(def ^:private history-limit
54 "How many lines of backlog to ask a room for, as `frq.state` asks for them."
55 100)
56
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago57(defn- room!
58 "The little of `frq.state/apply-msg!` the conversation list needs: the two
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago59 messages that make a room appear and give it a last line, and everything
60 that says who is in it.
61
62 The membership half is `frq.members`, unchanged from what the desktop folds
63 the server is the one deciding who is in a room, and it tells both halves
64 in the same words."
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago65 [m]
66 (let [cmd (str (:command m))
67 params (vec (:params m))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago68 who (irc/nick-of (:prefix m))
69 me? (= who (str @cells/form-nick))
70 ensure (fn [m name] (update m name #(merge {:name name :messages [] :unread 0} %)))]
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago71 (cond
72 (= "JOIN" cmd)
73 (let [name (first params)]
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago74 (swap! cells/channels
75 #(-> (ensure % name)
76 (members/add-user name who)
77 (cond-> me? (update name merge {:joined? true
78 :accessed (clock/now-ms)}))))
79 ;; Only our own JOIN opens the room. Someone else arriving in a
80 ;; channel used to move the reader into it.
81 (when me? (reset! cells/current name)))
82
83 ;; 353 is the roll, in as many lines as it takes; 366 ends it.
84 (= "353" cmd)
85 (let [name (nth params 2 nil)]
86 (when name
87 (swap! cells/channels #(members/with-names (ensure % name) name (last params)))))
88
89 (= "366" cmd)
90 (let [name (nth params 1 nil)]
Ask for the backlog on the phone, as the desktop does 7dff3cf nandi 7d ago91 (when name
92 (swap! cells/channels members/names-done name)
93 ;; End of NAMES, and the only moment this client knows a room is
94 ;; fully arrived. freeq re-joins an authenticated user's channels at
95 ;; registration and leaves the backlog for the client to ask for, so
96 ;; a room that reaches here with an empty buffer has no history
97 ;; coming unless we ask which is why nothing but new lines ever
98 ;; appeared here. Same request `frq.state` makes on 366, and the
99 ;; replayed lines come back as ordinary PRIVMSGs the branch below
100 ;; already folds.
101 (when (and @conn (empty? (get-in @cells/channels [name :messages])))
102 (net/send-line! @conn
103 (str "CHATHISTORY LATEST " name " * " history-limit)))))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago104
105 (= "PART" cmd)
106 (swap! cells/channels members/remove-user (first params) who)
107
108 (= "KICK" cmd)
109 (swap! cells/channels members/remove-user (first params) (nth params 1 nil))
110
111 (= "QUIT" cmd)
112 (swap! cells/channels members/remove-everywhere who)
113
114 (= "NICK" cmd)
115 (do (swap! cells/channels members/rename-user who (last params))
116 ;; Our own rename is the server settling what we are called it
117 ;; hands a guest a name of its choosing, so this is the usual way
118 ;; the nick on screen becomes the real one.
119 (when me? (reset! cells/form-nick (last params))))
120
121 (= "MODE" cmd)
122 (swap! cells/channels members/with-mode
123 (first params) (nth params 1 "") (drop 2 params))
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago124
125 (= "PRIVMSG" cmd)
126 (let [target (first params)
127 text (last params)
128 name (if (rooms/dm? target) who target)]
129 (swap! cells/channels update name
130 #(-> (merge {:name name :messages [] :unread 0} %)
131 (update :messages conj {:from who :text text})))))))
132
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago133(defn- note! [m]
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago134 (reset! last-line (str (:command m) " " (last (:params m))))
Share the connect screen, and the cells under it 80eb088 nandi 8d ago135 (swap! lines (fn [v] (vec (take-last 8 (conj v m))))))
136
Tell the truth about TLS on the phone, and stop the guest spinner ba9e7a9 nandi 8d ago137(def ^:private registration-failed
138 "Numerics that mean registration will never complete.
139
140 Only 001 used to clear `connecting?`, so anything the server said instead of
141 it spun for ever with no message and the likeliest of them is the dullest:
142 433, the nick is already in use, which is what a second phone or a session
143 left open elsewhere gets."
144 #{"431" ; no nickname given
145 "432" ; erroneous nickname
146 "433" ; nickname in use
147 "436" ; nick collision
148 "464" ; password incorrect
149 "465" ; banned
150 "ERROR"})
151
152(defonce ^:private attempt (atom 0))
153
Repaint for every cell, follow the conversation, and send once 8ead4ea nandi 7d ago154;; Ticked whenever any cell changes, so one `:watch` covers all of them.
155;;
156;; What this replaced was a list of cells named by hand in the widget below,
157;; and everything not on it was a control that flipped its cell and repainted
158;; nothing People, Overview and hide join/part all did.
159(defonce ^:private repaint (atom 0))
160
161(defn- watch-cells!
162 "Every cell in `frq.cells` ticking `repaint` when it changes.
163
164 Called from `main` rather than run from a top-level `defonce`, and that is
165 not a style preference: ClojureDart compiles a `def` to a Dart top-level
166 variable, Dart initialises those on first read, and a `defonce` whose value
167 nothing ever reads simply never runs. The first attempt at this installed no
168 watches at all and printed nothing to say so."
169 []
170 (doseq [c (cells/all-cells)]
171 (add-watch c ::repaint
172 (fn [_ _ old new]
173 (when (not= old new) (swap! repaint inc))
174 nil))))
175
Sign in from the phone with the handshake both halves now share dd67233 nandi 7d ago176(defonce ^:private caps
177 ;; What the server has agreed to on this connection. `frq.irc.handshake`
178 ;; holds none of it: it takes the set and hands a new one back, so the
179 ;; desktop can keep it in an atom on the connection and the phone here.
180 (atom #{}))
181
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago182(defonce ^:private closing?
183 ;; Whether the close about to arrive is one we asked for. Without it a tap
184 ;; on Disconnect comes back as "closed by the server", which is both untrue
185 ;; and alarming.
186 (atom false))
187
Tell the truth about TLS on the phone, and stop the guest spinner ba9e7a9 nandi 8d ago188(defn- fail! [why]
189 (reset! cells/connecting? false)
190 (reset! cells/status "Not connected")
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago191 (reset! cells/screen :connect)
Tell the truth about TLS on the phone, and stop the guest spinner ba9e7a9 nandi 8d ago192 (reset! cells/error why))
193
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago194(defn ^:async bluesky-session!
195 "A web-token session from freeq's auth broker.
196
197 Only the half that needs no browser. A saved broker token is durable and the
198 web-token minted from it is single-use, so a return visit is one POST to
199 /session and `frq.oauth.core` already says what to send and what the answer
200 means. Getting the *first* broker token is the other half: the browser hands
201 it back to a loopback listener on the desktop, and a phone has nowhere for
202 that to land it wants an Android app link, which is a decision about what
203 the broker will redirect to and not a porting problem. So that case says so
204 rather than quietly connecting as somebody else."
205 []
206 (if-let [bt @cells/broker-token]
207 (do
208 (reset! cells/status "Resuming your session…")
209 (let [tokens (oauth/refresh-session-parse
210 bt
211 (await (atproto/fetch
212 (oauth/refresh-session-req oauth/default-broker bt))))]
213 (reset! cells/broker-token (:broker-token tokens))
214 ;; On every sign-in and not only the first: /session can hand back a
215 ;; rotated broker token, and the old one may stop working the moment
216 ;; it does.
217 (store/save-session! tokens)
218 (assoc tokens :kind :web-token)))
Sign in with Bluesky on the phone, browser and all 939d2ac nandi 7d ago219 ;; No saved token: the browser leg. `frq.oauth.dart` binds the loopback,
220 ;; and what comes back through it is the first broker token.
221 (let [tokens (await (oauth-dart/await-callback!
222 oauth/default-broker
223 (str @cells/form-handle)
224 (fn [url]
225 (reset! cells/login-url url)
226 (reset! cells/status
227 (if (fio/open-url! url)
228 "Waiting for the browser…"
229 "Open the sign-in link below to continue"))
230 nil)))]
231 (reset! cells/login-url nil)
232 (reset! cells/broker-token (:broker-token tokens))
233 (store/save-session! tokens)
234 (assoc tokens :kind :web-token))))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago235
236(defn ^:async sign-in!
237 "Fill `cells/session` for the mode that was chosen, or say why not.
238
239 True when the connection may go ahead. A guest carries no session, and must
240 not carry the last one either: a leftover would have `frq.irc.handshake` ask
241 for sasl and authenticate as whoever signed in before."
242 []
243 (case @cells/auth-mode
244 :guest (do (reset! cells/session nil) true)
245 (try
246 (reset! cells/connecting? true)
247 (reset! cells/error nil)
248 (reset! cells/session
249 (await (if (= :bluesky @cells/auth-mode)
250 (bluesky-session!)
251 (do (reset! cells/status "Signing in…")
252 (atproto/create-session @cells/form-handle
253 @cells/form-app-password)))))
Sign in with Bluesky on the phone, browser and all 939d2ac nandi 7d ago254 ;; An authenticated connection still needs a nick the DID is the
255 ;; identity, the nick is only what the channel calls you. Same order the
256 ;; desktop picks it in: what the broker said, else the first label of the
257 ;; handle, else whatever is in the box.
258 (let [sess @cells/session
259 nick (or (:nick sess)
260 (first (.split (str (or (:handle sess) "")) "."))
261 nil)]
262 (when (seq (str (or (:handle sess) "")))
263 (reset! cells/form-handle (:handle sess)))
264 (when (seq (str (or nick "")))
265 (reset! cells/form-nick nick))
266 ;; The password did its work at the PDS; do not keep it.
267 (reset! cells/form-app-password ""))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago268 true
269 (catch Object e
270 (reset! cells/session nil)
271 ;; The message, not the exception: `frq.atproto.core` puts the body it
272 ;; could not read in the ex-data, and a PDS that answers a resolve with
273 ;; an HTML error page puts the whole page there.
274 (fail! (str "Sign-in failed: " (or (ex-message e) e)))
275 false))))
276
Share the connect screen, and the cells under it 80eb088 nandi 8d ago277(defn ^:async connect!
278 "What `frq.actions/connect!` is on the phone.
279
280 The desktop's is `frq.state/connect!` — jolt's TLS, SASL, a reader thread.
281 This is the dart:io one, and it reads the same cells the screen wrote: the
282 host, the port and the TLS tick are `frq.cells`, filled in by the entry and
283 the checkbutton on screen.
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago284
Sign in from the phone with the handshake both halves now share dd67233 nandi 7d ago285 Guest and app-password both. The handshake itself is not here: it is
286 `frq.irc.handshake`, which answers each line with the lines to send back,
287 and all this does is write them."
Share the connect screen, and the cells under it 80eb088 nandi 8d ago288 []
Tell the truth about TLS on the phone, and stop the guest spinner ba9e7a9 nandi 8d ago289 ;; Close whatever was open first. Connecting twice left the old socket
290 ;; holding the nick, so the second attempt got 433 from its own predecessor.
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago291 (when-let [c @conn]
292 (reset! closing? true)
293 (net/close! c)
294 (reset! conn nil))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago295 ;; Whatever identity was asked for, settled before the socket opens. Both
296 ;; of the signed-in modes are an HTTPS round trip that has nothing to do
297 ;; with IRC, and a failure in either must stop here: connecting anyway lands
298 ;; us on the server as a guest, which looks like a success and is not the
299 ;; one that was asked for. Bluesky did exactly that until it was asked.
300 (when (await (sign-in!))
301 (let [n (swap! attempt inc)]
Sign in from the phone with the handshake both halves now share dd67233 nandi 7d ago302 (reset! caps #{})
303 (reset! cells/connecting? true)
304 (reset! cells/error nil)
305 (reset! cells/status (str "Connecting to " @cells/form-host ""))
306 (reset! lines [])
307 ;; A watchdog, because "no answer at all" is a real outcome: a TLS
308 ;; handshake that hangs, or a server that accepts the socket and says
309 ;; nothing, leaves every callback below unfired.
310 (.then (async/Future.delayed (Duration .seconds 20))
311 (fn [_]
312 (when (and @cells/connecting? (= n @attempt))
313 (fail! "No answer from the server after 20s"))))
314 (try
315 (let [sock (await (net/connect!
316 {:host @cells/form-host
317 ;; The cell is a string, because it is what an
318 ;; :entry holds.
319 :port (or (parse-long (str @cells/form-port)) 6697)
320 :tls? (boolean @cells/form-tls?)
321 :on-msg (fn [m]
322 (note! m)
323 (room! m)
324 ;; Capability negotiation and SASL, out of
325 ;; common/ the same steps the desktop
326 ;; takes, and all this does is write what
327 ;; they answer with.
328 (let [{:keys [send] next-caps :caps}
329 (handshake/step {:session @cells/session
330 :caps @caps}
331 m)]
332 (reset! caps next-caps)
333 (doseq [line send]
334 (when-let [c @conn] (net/send-line! c line))))
335 (let [cmd (str (:command m))
336 text (str (last (:params m)))]
337 (cond
338 (= "001" cmd)
339 (do (reset! cells/connecting? false)
340 (reset! cells/error nil)
341 (reset! cells/status
342 (str "Connected as " @cells/form-nick))
343 ;; Off the connect screen, as
344 ;; frq.state does on 001. Without
345 ;; this a successful connect just
346 ;; puts the Connect button back
347 ;; and looks exactly like a
348 ;; failure.
349 (reset! cells/screen :chats)
350 (when-let [c @conn]
351 (net/send-line! c "JOIN #test")))
352
353 (contains? registration-failed cmd)
354 (fail! (if (= "433" cmd)
355 (str "Nick " @cells/form-nick
356 " is already in use — try another")
357 (str cmd " " text)))
358
359 ;; Something is happening; say so
360 ;; rather than sit on one message.
361 @cells/connecting?
362 (reset! cells/status (str "" cmd)))))
363 :on-close (fn [why]
364 (reset! conn nil)
365 ;; Say what the last thing seen was. A
366 ;; connection that registers and then
367 ;; drops is a different bug from one
368 ;; that never registers, and only the
369 ;; last line apart tells them.
370 (if @closing?
371 (reset! closing? false)
372 (fail! (str (or why "closed by the server")
373 " (after " (count @lines)
374 " lines, last: " @last-line ")"))))}))]
375 (reset! conn sock)
376 ;; CAP first, then registration the order the server expects, and the
377 ;; order `frq.irc` uses.
378 (net/send-line! sock "CAP LS 302")
379 (let [nick (if-let [h (:handle @cells/session)] h @cells/form-nick)]
380 (net/send-line! sock (str "NICK " nick))
381 (net/send-line! sock (str "USER " nick " 0 * :frq"))))
382 ;; Object, not Exception. Dart keeps Error and Exception in separate
383 ;; hierarchies, so a TypeError is not an Exception and would leave
384 ;; `connecting?` true for ever.
385 (catch Object e
386 (fail! (str e)))))))
Share the connect screen, and the cells under it 80eb088 nandi 8d ago387
388(defn- disconnect! []
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago389 (reset! closing? true)
Share the connect screen, and the cells under it 80eb088 nandi 8d ago390 (when-let [c @conn] (net/close! c))
391 (reset! conn nil)
392 (reset! cells/connecting? false)
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago393 (reset! cells/status "Not connected")
394 (reset! cells/screen :connect))
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago395
Resolve an identity from the phone, over the shared AT Protocol core f735240 nandi 8d ago396(defonce ^:private handle (atom "nandi-test.bsky.social"))
397(defonce ^:private identity-out (atom nil))
398
Share the connect screen, and the cells under it 80eb088 nandi 8d ago399(defn ^:async resolve-identity! []
Resolve an identity from the phone, over the shared AT Protocol core f735240 nandi 8d ago400 (reset! identity-out ["resolving…"])
401 (try
402 (let [h @handle
403 did (await (atproto/resolve-handle h))
404 pds (await (atproto/pds-endpoint did))]
405 (reset! identity-out [(str "did " did) (str "pds " pds)]))
406 (catch Exception e
407 (reset! identity-out [(str "failed: " e)]))))
408
Go somewhere when the connect succeeds 19a2ec6 nandi 8d ago409(defn- connected-screen
410 "Where a connect lands, until `frq.app`'s chats screen is portable.
411
412 Not much, and honest about it: the status, what the server said, and the way
413 back. The desktop goes to :chats here that screen reads rooms, messages,
414 avatars and the media plane, which is most of what is not ported yet."
415 []
416 [:page {:max-width 520}
417 [:title {:label "frq"}]
418 [:status {:live true :label @cells/status}]
419 [:card {}
420 [:title-2 {:label "What the server said"}]
421 (if (empty? @lines)
422 [:dim-label {:label "nothing yet"}]
423 (for [[i m] (map-indexed vector @lines)]
424 [:label {:key i
425 :label (str (:command m)
426 (when-let [p (seq (:params m))]
427 (str " " (last p))))}]))]
428 [:hbox {:spacing 8}
429 [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]]
430 [:dim-label {:label "The chats screen is next: it wants rooms, messages and avatars, none of which are ported yet."}]])
431
Open a channel, and give it somewhere to open onto 8c903d8 nandi 7d ago432(defn- send-draft! []
Draw the conversation on the phone c0121fe nandi 7d ago433 (let [text (str @cells/draft)
Open a channel, and give it somewhere to open onto 8c903d8 nandi 7d ago434 room (str @cells/current)]
435 (when (and (seq text) (seq room) @conn)
436 (net/send-line! @conn (str "PRIVMSG " room " :" text))
Repaint for every cell, follow the conversation, and send once 8ead4ea nandi 7d ago437 ;; Echoed locally only when the server will not echo it back. With
438 ;; `echo-message` negotiated it does that is what the cap is for, and
439 ;; it is how a client learns the msgid of its own line so adding one
440 ;; here as well put every sent message in the room twice.
441 (when-not (handshake/acked? @caps "echo-message")
442 (swap! cells/channels update room
443 #(update % :messages conj {:from @cells/form-nick :text text})))
Draw the conversation on the phone c0121fe nandi 7d ago444 (reset! cells/draft ""))))
Open a channel, and give it somewhere to open onto 8c903d8 nandi 7d ago445
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago446(defn ^:async main []
447 (m/WidgetsFlutterBinding.ensureInitialized)
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago448 ;; Layout errors do not come back as exceptions they happen after the
449 ;; build, so nothing can catch them, and what they leave is a blank screen
450 ;; and an empty log. Flutter routes them here instead.
451 (set! (.-onError m/FlutterError)
452 (fn [details]
453 (m/debugPrint (str "frq: FLUTTER ERROR " (.-exception details)))
454 (m/debugPrint (str "frq: LIBRARY " (.-library details)
455 " CONTEXT " (.-context details)))))
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago456 (let [dir (.-path (await (pp/getApplicationSupportDirectory)))]
457 (host/install! dir)
Sign on the phone: msgsig shared, crypto seamed d8fadee nandi 7d ago458 ;; Ed25519 for the reactions freeq will not take on trust. Verified
459 ;; against RFC 8032 test 1 on the device: the same public key and the
460 ;; same signature OpenSSL gives on the desktop, so a signature minted
461 ;; here verifies the same way at the server.
462 (crypto-dart/install!)
Repaint for every cell, follow the conversation, and send once 8ead4ea nandi 7d ago463 ;; Before any widget is built: a cell that changes before its watch is on
464 ;; is a change the screen never hears about.
465 (watch-cells!)
Sign in with Bluesky on the phone, browser and all 939d2ac nandi 7d ago466 ;; A sign-in that already happened. Only the durable broker token comes
467 ;; back the connection mints a fresh web-token from it so this is not
468 ;; a session, it is the means to ask for one.
469 (when-let [saved (store/load-session)]
470 (reset! cells/broker-token (:broker-token saved))
471 (when (seq (str (or (:handle saved) "")))
472 (reset! cells/form-handle (:handle saved)))
473 (when (seq (str (or (:nick saved) "")))
474 (reset! cells/form-nick (:nick saved)))
475 (reset! cells/auth-mode :bluesky)
476 (reset! cells/status (str "Signed in as " (:handle saved) " — Connect to resume")))
Share the connect screen, and the cells under it 80eb088 nandi 8d ago477 ;; What the shared screen calls. The desktop installs frq.state's
478 ;; reducers here; this installs the phone's.
Take what is left: fill-height, and the chats screen on the phone ff774d1 nandi 7d ago479 (actions/install!
480 {:connect! connect!
481 :disconnect! disconnect!
482 :connected? (fn [] (some? @conn))
483 :join! (fn [name]
484 (when-let [c @conn]
485 (when (seq (str name))
486 (net/send-line! c (str "JOIN " name)))))
487 :leave-channel! (fn [name]
488 (when-let [c @conn] (net/send-line! c (str "PART " name)))
489 (swap! cells/channels dissoc name))
Draw the conversation on the phone c0121fe nandi 7d ago490 :send-draft! send-draft!
491 :wide? (fn [] false)
492 :desktop? (fn [] false)
493 :mine? (fn [from] (= (str from) (str @cells/form-nick)))
494 :message-by-id (fn [room id]
495 (->> (get-in @cells/channels [room :messages])
496 (filter #(= id (:id %)))
497 first))
Share who is in the room, and stop Bluesky signing in as somebody else c6506f2 nandi 7d ago498 :member-count (fn [room] (members/member-count @cells/channels room))
499 :member-list (fn [room] (members/member-list @cells/channels room))
Draw the conversation on the phone c0121fe nandi 7d ago500 :toggle-users! (fn [] (swap! cells/show-users? not))
501 :toggle-overview! (fn [] (swap! cells/overview? not))
502 :toggle-chat-list! (fn [] (swap! cells/hide-chat-list? not))
Share Discover and Settings, and the frame around them 49fb82f nandi 7d ago503 :toggle-hide-join-part! (fn [] (swap! cells/hide-join-part? not))
504 :quit! (fn [] nil)
505 :forget-session! (fn []
506 (reset! cells/broker-token nil)
507 (reset! cells/session nil))
Draw the conversation on the phone c0121fe nandi 7d ago508 :jump-to-present! (fn [] (reset! cells/at-present? true))
Open a channel, and give it somewhere to open onto 8c903d8 nandi 7d ago509 :open-channel! (fn [name]
510 (reset! cells/current name)
511 ;; `frq.state/open-channel!` does this and more: it
512 ;; marks the buffer read, remembers the room list and
513 ;; asks for NAMES. What it also does, and what made
514 ;; Open look like a no-op here, is go to the
515 ;; conversation and join it on the way, because a row
516 ;; can outlive the membership behind it.
517 (swap! cells/channels update name
518 #(merge {:name name :messages [] :unread 0} %))
519 (reset! cells/screen :chat)
520 (let [buffer (get @cells/channels name)]
521 (when (and @conn
522 (.startsWith (str name) "#")
523 (not (:joined? buffer)))
524 (net/send-line! @conn (str "JOIN " name)))))})
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago525 (f/run
Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 8d ago526 (m/MaterialApp .title "frq" .theme (t/app-theme))
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago527 .home
Say "frq" once, and let the guest sign-in finish 232a455 nandi 8d ago528 ;; No AppBar. `connect-screen` opens with [:title {:label "frq"}] the
529 ;; desktop has no bar above it either, the window's title is the
530 ;; window's — so a bar here says "frq" twice.
531 (m/Scaffold)
Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 8d ago532 .body
Say "frq" once, and let the guest sign-in finish 232a455 nandi 8d ago533 m/SafeArea
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago534 (f/widget
Repaint for every cell, follow the conversation, and send once 8ead4ea nandi 7d ago535 ;; One watch, not twenty. `lines` is a local atom and not a cell, so it
536 ;; is named beside the tick; everything under `frq.cells` arrives
537 ;; through `repaint`.
538 :watch [tick repaint ls lines]
Share the root, and with it the last of the screens 433c3c8 nandi 7d ago539 (h/render
540 ;; `frq.screens.app` decides which screen shows, the same way it does
541 ;; on the desktop. The phone was switching by hand until this moved.
542 [screens/app])))))