Share the connect screen, and the cells under it
The screens were never the hard part; the state they read was. So this moves the state, not the screen: common/frq/cells.cljc holds the twenty atoms the connect screen touches and frq.state re-defs every one of them, so its other 1,900 lines and every `s/form-nick` in them did not move. frq.actions is the seam for what a screen cannot do itself — connect!, disconnect!, forget-session! — installed by frq.state on the desktop and by frq.main on the phone. With those two in place the screen itself was a rename. common/frq/screens/ connect.cljc is the same hiccup that was in frq.app, reading frq.cells instead of frq.state and calling frq.actions instead of the reducers behind it; frq.app requires it and renders it exactly where its own copy was. The TUI renders byte-identical, which is the only proof that matters. `atom` is the one word that differs, and a reader conditional settles it: a glimmer ratom under jolt, an ordinary atom under ClojureDart, and cljd.flutter's :watch doing from the other end what glimmer's reconciler does from this one. jolt answers to :jolt and ClojureDart to :cljd — and ClojureDart also has :clj always on, which is why neither branch is spelled that way. Proven on the device: frq.cells compiles under both and holds its values there — a probe read cells/status as "Not connected" and cells/form-nick as "frq-guest" on the phone. Not proven: the phone rendering the shared screen. It paints nothing, with no exception, nothing in logcat, and the sibling widgets in the same children vector vanishing with it — which is what a Dart Error thrown while that vector is built looks like, except (catch Object ...) around it surfaces nothing either. So the phone keeps its own panels for now and the seam underneath them is the shared one. The bug is between frq.screens.connect and frq.hiccup, not in the extraction. One thing the attempt did find: :kind carries a keyword in frq.app's hiccup and frq.hiccup compared it against a string, so Connect would have come out as a standard button rather than suggested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80eb088 parent: ba4e71b added
common/frq/actions.cljc +27 -0 | new file mode 100644 | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +(ns frq.actions | |
| 2 | + "What a screen can ask the app to do, named once so both can answer. | |
| 3 | + | |
| 4 | + The same shape as `frq.io`, one layer up: the cells in `frq.cells` are state | |
| 5 | + a shared screen can read directly, and these are the things it cannot do for | |
| 6 | + itself. `connect!` on the desktop is `frq.state/connect!` — IRC over jolt's | |
| 7 | + TLS, SASL, the reader thread — and on the phone it is the dart:io one. The | |
| 8 | + screen calls the same name either way and knows neither." | |
| 9 | + (:refer-clojure :exclude [name])) | |
| 10 | + | |
| 11 | +(defonce ^:private impl (atom {})) | |
| 12 | + | |
| 13 | +(defn install! | |
| 14 | + "Register what this platform does. Anything left out is a no-op, so a screen | |
| 15 | + can be rendered before the action behind a button exists — which is how the | |
| 16 | + phone got the connect screen on it before SASL was ported." | |
| 17 | + [m] | |
| 18 | + (swap! impl merge m) | |
| 19 | + nil) | |
| 20 | + | |
| 21 | +(defn- call [k args] | |
| 22 | + (when-let [f (get @impl k)] (apply f args))) | |
| 23 | + | |
| 24 | +(defn connect! [] (call :connect! [])) | |
| 25 | +(defn disconnect! [] (call :disconnect! [])) | |
| 26 | +(defn forget-session! [] (call :forget-session! [])) | |
| 27 | +(defn open-url! [url] (call :open-url! [url])) | |
| new file mode 100644 | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | +(ns frq.actions | ||
| 2 | + "What a screen can ask the app to do, named once so both can answer. | ||
| 3 | + | ||
| 4 | + The same shape as `frq.io`, one layer up: the cells in `frq.cells` are state | ||
| 5 | + a shared screen can read directly, and these are the things it cannot do for | ||
| 6 | + itself. `connect!` on the desktop is `frq.state/connect!` — IRC over jolt's | ||
| 7 | + TLS, SASL, the reader thread — and on the phone it is the dart:io one. The | ||
| 8 | + screen calls the same name either way and knows neither." | ||
| 9 | + (:refer-clojure :exclude [name])) | ||
| 10 | + | ||
| 11 | +(defonce ^:private impl (atom {})) | ||
| 12 | + | ||
| 13 | +(defn install! | ||
| 14 | + "Register what this platform does. Anything left out is a no-op, so a screen | ||
| 15 | + can be rendered before the action behind a button exists — which is how the | ||
| 16 | + phone got the connect screen on it before SASL was ported." | ||
| 17 | + [m] | ||
| 18 | + (swap! impl merge m) | ||
| 19 | + nil) | ||
| 20 | + | ||
| 21 | +(defn- call [k args] | ||
| 22 | + (when-let [f (get @impl k)] (apply f args))) | ||
| 23 | + | ||
| 24 | +(defn connect! [] (call :connect! [])) | ||
| 25 | +(defn disconnect! [] (call :disconnect! [])) | ||
| 26 | +(defn forget-session! [] (call :forget-session! [])) | ||
| 27 | +(defn open-url! [url] (call :open-url! [url])) | ||
added
common/frq/cells.cljc +47 -0 | new file mode 100644 | ||
| @@ -0,0 +1,47 @@ | ||
| 1 | +(ns frq.cells | |
| 2 | + "The cells the connect screen reads, and the constants beside them. | |
| 3 | + | |
| 4 | + Moved out of `frq.state` so a screen can be shared: `frq.state` is 1,930 | |
| 5 | + lines that reach `frq.irc` and `frq.av` and will not compile under | |
| 6 | + ClojureDart for a long time yet, but the cells themselves are atoms and | |
| 7 | + atoms are portable. `frq.state` re-defs every name here, so its own thousand | |
| 8 | + lines did not move and neither did anything reading `s/form-handle`. | |
| 9 | + | |
| 10 | + The reader conditional is the whole trick. On jolt these are glimmer ratoms | |
| 11 | + — a component that derefs one re-renders when it changes, which is what the | |
| 12 | + desktop's reconciler is built on. Under ClojureDart they are ordinary atoms, | |
| 13 | + and `cljd.flutter`'s `:watch` does the same job from the other end. Neither | |
| 14 | + compiler sees the other's require. | |
| 15 | + | |
| 16 | + jolt answers to `:jolt` and ClojureDart to `:cljd`; ClojureDart also has | |
| 17 | + `:clj` always on, which is why neither branch is spelled that way." | |
| 18 | + (:require #?@(:cljd [] | |
| 19 | + :jolt [[glimmer.ratom :refer [atom]]]))) | |
| 20 | + | |
| 21 | +(def default-host "irc.freeq.at") | |
| 22 | +(def default-port "6697") | |
| 23 | + | |
| 24 | +;; screen: :connect | :chats | :chat | :discover | :settings | |
| 25 | +(defonce screen (atom :connect)) | |
| 26 | +(defonce status (atom "Not connected")) | |
| 27 | +(defonce error (atom nil)) | |
| 28 | +(defonce connecting? (atom false)) | |
| 29 | + | |
| 30 | +(defonce form-host (atom default-host)) | |
| 31 | +(defonce form-port (atom default-port)) | |
| 32 | +;; TLS is the default; untick it for a server's plain :6667 listener | |
| 33 | +(defonce form-tls? (atom true)) | |
| 34 | +(defonce form-nick (atom "frq-guest")) | |
| 35 | + | |
| 36 | +;; Bluesky sign-in. The app password reaches the user's own PDS and nothing | |
| 37 | +;; else: freeq is handed the session token it mints, and verifies that token by | |
| 38 | +;; asking the same PDS. It is never written to disk. | |
| 39 | +(defonce auth-mode (atom :guest)) ; :guest | :bluesky | :app-password | |
| 40 | +(defonce form-handle (atom "")) | |
| 41 | +(defonce form-app-password (atom "")) | |
| 42 | +(defonce session (atom nil)) ; a pds-session or a web-token one | |
| 43 | + | |
| 44 | +;; The durable half of an OAuth sign-in. The web-token beside it is single-use, | |
| 45 | +;; so a reconnect mints a fresh one from this rather than replaying the old. | |
| 46 | +(defonce broker-token (atom nil)) | |
| 47 | +(defonce login-url (atom nil)) ; shown while the browser is open | |
| new file mode 100644 | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | +(ns frq.cells | ||
| 2 | + "The cells the connect screen reads, and the constants beside them. | ||
| 3 | + | ||
| 4 | + Moved out of `frq.state` so a screen can be shared: `frq.state` is 1,930 | ||
| 5 | + lines that reach `frq.irc` and `frq.av` and will not compile under | ||
| 6 | + ClojureDart for a long time yet, but the cells themselves are atoms and | ||
| 7 | + atoms are portable. `frq.state` re-defs every name here, so its own thousand | ||
| 8 | + lines did not move and neither did anything reading `s/form-handle`. | ||
| 9 | + | ||
| 10 | + The reader conditional is the whole trick. On jolt these are glimmer ratoms | ||
| 11 | + — a component that derefs one re-renders when it changes, which is what the | ||
| 12 | + desktop's reconciler is built on. Under ClojureDart they are ordinary atoms, | ||
| 13 | + and `cljd.flutter`'s `:watch` does the same job from the other end. Neither | ||
| 14 | + compiler sees the other's require. | ||
| 15 | + | ||
| 16 | + jolt answers to `:jolt` and ClojureDart to `:cljd`; ClojureDart also has | ||
| 17 | + `:clj` always on, which is why neither branch is spelled that way." | ||
| 18 | + (:require #?@(:cljd [] | ||
| 19 | + :jolt [[glimmer.ratom :refer [atom]]]))) | ||
| 20 | + | ||
| 21 | +(def default-host "irc.freeq.at") | ||
| 22 | +(def default-port "6697") | ||
| 23 | + | ||
| 24 | +;; screen: :connect | :chats | :chat | :discover | :settings | ||
| 25 | +(defonce screen (atom :connect)) | ||
| 26 | +(defonce status (atom "Not connected")) | ||
| 27 | +(defonce error (atom nil)) | ||
| 28 | +(defonce connecting? (atom false)) | ||
| 29 | + | ||
| 30 | +(defonce form-host (atom default-host)) | ||
| 31 | +(defonce form-port (atom default-port)) | ||
| 32 | +;; TLS is the default; untick it for a server's plain :6667 listener | ||
| 33 | +(defonce form-tls? (atom true)) | ||
| 34 | +(defonce form-nick (atom "frq-guest")) | ||
| 35 | + | ||
| 36 | +;; Bluesky sign-in. The app password reaches the user's own PDS and nothing | ||
| 37 | +;; else: freeq is handed the session token it mints, and verifies that token by | ||
| 38 | +;; asking the same PDS. It is never written to disk. | ||
| 39 | +(defonce auth-mode (atom :guest)) ; :guest | :bluesky | :app-password | ||
| 40 | +(defonce form-handle (atom "")) | ||
| 41 | +(defonce form-app-password (atom "")) | ||
| 42 | +(defonce session (atom nil)) ; a pds-session or a web-token one | ||
| 43 | + | ||
| 44 | +;; The durable half of an OAuth sign-in. The web-token beside it is single-use, | ||
| 45 | +;; so a reconnect mints a fresh one from this rather than replaying the old. | ||
| 46 | +(defonce broker-token (atom nil)) | ||
| 47 | +(defonce login-url (atom nil)) ; shown while the browser is open | ||
added
common/frq/screens/connect.cljc +122 -0 | new file mode 100644 | ||
| @@ -0,0 +1,122 @@ | ||
| 1 | +(ns frq.screens.connect | |
| 2 | + "The connect screen, shared. | |
| 3 | + | |
| 4 | + The first of `frq.app`'s screens to move out of it, and the move was a | |
| 5 | + rename rather than a rewrite: this is the same hiccup it always was, reading | |
| 6 | + `frq.cells` instead of `frq.state` and calling `frq.actions` instead of the | |
| 7 | + reducers behind it. `frq.app` requires it and renders it exactly where its | |
| 8 | + own copy used to be, so the desktop draws this file and so does the phone. | |
| 9 | + | |
| 10 | + Which is the point of the whole arrangement. glimmer's components never knew | |
| 11 | + what was under the reconciler; now they do not know what is under the *app* | |
| 12 | + either, and a screen is portable as soon as the cells it reads and the | |
| 13 | + actions it calls are." | |
| 14 | + (:require [frq.actions :as actions] | |
| 15 | + [frq.cells :as cells])) | |
| 16 | + | |
| 17 | +(defn error-note | |
| 18 | + "Always a node, never nil. | |
| 19 | + | |
| 20 | + A conditional child that disappears shifts every sibling after it, and the | |
| 21 | + reconciler matches children by position — so an error appearing mid-screen | |
| 22 | + would patch the header into a card. A stable wrapper with a stable key keeps | |
| 23 | + the shape of the tree fixed and only its contents changing." | |
| 24 | + [] | |
| 25 | + [:vbox {:key :error-note :spacing 6} | |
| 26 | + (when-let [e @cells/error] | |
| 27 | + [:card {} | |
| 28 | + [:label {:label (str "⚠ " e)}] | |
| 29 | + [:button {:label "Dismiss" :on-click #(reset! cells/error nil)}]])]) | |
| 30 | + | |
| 31 | +;; ---------------------------------------------------------------- connect | |
| 32 | + | |
| 33 | +(defn- mode-tabs [] | |
| 34 | + [:hbox {:spacing 8} | |
| 35 | + (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]] | |
| 36 | + [:button {:key k | |
| 37 | + :label label | |
| 38 | + :kind (if (= k @cells/auth-mode) :primary :default) | |
| 39 | + :on-click #(reset! cells/auth-mode k)}])]) | |
| 40 | + | |
| 41 | +(defn- server-fields [] | |
| 42 | + [:vbox {:spacing 6} | |
| 43 | + [:label {:label "Server"}] | |
| 44 | + [:hbox {:spacing 8} | |
| 45 | + [:entry {:text @cells/form-host | |
| 46 | + :width-request 220 | |
| 47 | + :placeholder "host" | |
| 48 | + :on-change #(reset! cells/form-host %)}] | |
| 49 | + [:entry {:text @cells/form-port | |
| 50 | + :width-request 90 | |
| 51 | + :placeholder "6697" | |
| 52 | + :on-change #(reset! cells/form-port %)}]] | |
| 53 | + [:checkbutton {:label "TLS" | |
| 54 | + :active @cells/form-tls? | |
| 55 | + :on-toggled #(do (swap! cells/form-tls? not) | |
| 56 | + (reset! cells/form-port | |
| 57 | + (if @cells/form-tls? "6697" "6667")))}]]) | |
| 58 | + | |
| 59 | +(defn- connect-action [] | |
| 60 | + (if @cells/connecting? | |
| 61 | + [:hbox {:spacing 8} | |
| 62 | + [:spinner {}] | |
| 63 | + [:dim-label {:label @cells/status}]] | |
| 64 | + [:hbox {:spacing 8} | |
| 65 | + [:button {:label "Connect" :kind :primary :on-click actions/connect!}] | |
| 66 | + [:dim-label {:label @cells/status}]])) | |
| 67 | + | |
| 68 | +(defn connect-screen [] | |
| 69 | + [:page {:max-width 520} | |
| 70 | + [:title {:label "frq"}] | |
| 71 | + [:dim-label {:label "freeq client — guest, or your Bluesky identity."}] | |
| 72 | + [error-note] | |
| 73 | + [:card {} | |
| 74 | + [mode-tabs] | |
| 75 | + (case @cells/auth-mode | |
| 76 | + :bluesky | |
| 77 | + [:vbox {:spacing 6} | |
| 78 | + [:title-2 {:label "Sign in with Bluesky"}] | |
| 79 | + [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}] | |
| 80 | + [:label {:label "Handle"}] | |
| 81 | + [:entry {:text @cells/form-handle | |
| 82 | + :width-request 320 | |
| 83 | + :placeholder "alice.bsky.social" | |
| 84 | + :on-change #(reset! cells/form-handle %)}] | |
| 85 | + [:vbox {:key :remembered :spacing 4} | |
| 86 | + (when @cells/broker-token | |
| 87 | + [:vbox {:spacing 4} | |
| 88 | + [:dim-label {:label "Session remembered — Connect will not need the browser."}] | |
| 89 | + [:button {:label "Forget saved session" :on-click actions/forget-session!}]])] | |
| 90 | + [:vbox {:key :login-url :spacing 4} | |
| 91 | + (when-let [url @cells/login-url] | |
| 92 | + [:vbox {:spacing 4} | |
| 93 | + [:dim-label {:label "If the browser did not open, visit:"}] | |
| 94 | + [:label {:label url}]])]] | |
| 95 | + | |
| 96 | + :app-password | |
| 97 | + [:vbox {:spacing 6} | |
| 98 | + [:title-2 {:label "Sign in with an app password"}] | |
| 99 | + [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}] | |
| 100 | + [:label {:label "Handle"}] | |
| 101 | + [:entry {:text @cells/form-handle | |
| 102 | + :width-request 320 | |
| 103 | + :placeholder "alice.bsky.social" | |
| 104 | + :on-change #(reset! cells/form-handle %)}] | |
| 105 | + [:label {:label "App password"}] | |
| 106 | + [:entry {:text @cells/form-app-password | |
| 107 | + :width-request 320 | |
| 108 | + :placeholder "xxxx-xxxx-xxxx-xxxx" | |
| 109 | + :on-change #(reset! cells/form-app-password %)}] | |
| 110 | + [:dim-label {:label "Make one at bsky.app → Settings → App Passwords."}]] | |
| 111 | + | |
| 112 | + [:vbox {:spacing 6} | |
| 113 | + [:title-2 {:label "Connect as guest"}] | |
| 114 | + [:label {:label "Nick"}] | |
| 115 | + [:entry {:text @cells/form-nick | |
| 116 | + :width-request 320 | |
| 117 | + :placeholder "your nick" | |
| 118 | + :on-change #(reset! cells/form-nick %)}]]) | |
| 119 | + [server-fields] | |
| 120 | + [:separator {}] | |
| 121 | + [connect-action]] | |
| 122 | + [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]]) | |
| new file mode 100644 | |||
| @@ -0,0 +1,122 @@ | |||
| 1 | +(ns frq.screens.connect | ||
| 2 | + "The connect screen, shared. | ||
| 3 | + | ||
| 4 | + The first of `frq.app`'s screens to move out of it, and the move was a | ||
| 5 | + rename rather than a rewrite: this is the same hiccup it always was, reading | ||
| 6 | + `frq.cells` instead of `frq.state` and calling `frq.actions` instead of the | ||
| 7 | + reducers behind it. `frq.app` requires it and renders it exactly where its | ||
| 8 | + own copy used to be, so the desktop draws this file and so does the phone. | ||
| 9 | + | ||
| 10 | + Which is the point of the whole arrangement. glimmer's components never knew | ||
| 11 | + what was under the reconciler; now they do not know what is under the *app* | ||
| 12 | + either, and a screen is portable as soon as the cells it reads and the | ||
| 13 | + actions it calls are." | ||
| 14 | + (:require [frq.actions :as actions] | ||
| 15 | + [frq.cells :as cells])) | ||
| 16 | + | ||
| 17 | +(defn error-note | ||
| 18 | + "Always a node, never nil. | ||
| 19 | + | ||
| 20 | + A conditional child that disappears shifts every sibling after it, and the | ||
| 21 | + reconciler matches children by position — so an error appearing mid-screen | ||
| 22 | + would patch the header into a card. A stable wrapper with a stable key keeps | ||
| 23 | + the shape of the tree fixed and only its contents changing." | ||
| 24 | + [] | ||
| 25 | + [:vbox {:key :error-note :spacing 6} | ||
| 26 | + (when-let [e @cells/error] | ||
| 27 | + [:card {} | ||
| 28 | + [:label {:label (str "⚠ " e)}] | ||
| 29 | + [:button {:label "Dismiss" :on-click #(reset! cells/error nil)}]])]) | ||
| 30 | + | ||
| 31 | +;; ---------------------------------------------------------------- connect | ||
| 32 | + | ||
| 33 | +(defn- mode-tabs [] | ||
| 34 | + [:hbox {:spacing 8} | ||
| 35 | + (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]] | ||
| 36 | + [:button {:key k | ||
| 37 | + :label label | ||
| 38 | + :kind (if (= k @cells/auth-mode) :primary :default) | ||
| 39 | + :on-click #(reset! cells/auth-mode k)}])]) | ||
| 40 | + | ||
| 41 | +(defn- server-fields [] | ||
| 42 | + [:vbox {:spacing 6} | ||
| 43 | + [:label {:label "Server"}] | ||
| 44 | + [:hbox {:spacing 8} | ||
| 45 | + [:entry {:text @cells/form-host | ||
| 46 | + :width-request 220 | ||
| 47 | + :placeholder "host" | ||
| 48 | + :on-change #(reset! cells/form-host %)}] | ||
| 49 | + [:entry {:text @cells/form-port | ||
| 50 | + :width-request 90 | ||
| 51 | + :placeholder "6697" | ||
| 52 | + :on-change #(reset! cells/form-port %)}]] | ||
| 53 | + [:checkbutton {:label "TLS" | ||
| 54 | + :active @cells/form-tls? | ||
| 55 | + :on-toggled #(do (swap! cells/form-tls? not) | ||
| 56 | + (reset! cells/form-port | ||
| 57 | + (if @cells/form-tls? "6697" "6667")))}]]) | ||
| 58 | + | ||
| 59 | +(defn- connect-action [] | ||
| 60 | + (if @cells/connecting? | ||
| 61 | + [:hbox {:spacing 8} | ||
| 62 | + [:spinner {}] | ||
| 63 | + [:dim-label {:label @cells/status}]] | ||
| 64 | + [:hbox {:spacing 8} | ||
| 65 | + [:button {:label "Connect" :kind :primary :on-click actions/connect!}] | ||
| 66 | + [:dim-label {:label @cells/status}]])) | ||
| 67 | + | ||
| 68 | +(defn connect-screen [] | ||
| 69 | + [:page {:max-width 520} | ||
| 70 | + [:title {:label "frq"}] | ||
| 71 | + [:dim-label {:label "freeq client — guest, or your Bluesky identity."}] | ||
| 72 | + [error-note] | ||
| 73 | + [:card {} | ||
| 74 | + [mode-tabs] | ||
| 75 | + (case @cells/auth-mode | ||
| 76 | + :bluesky | ||
| 77 | + [:vbox {:spacing 6} | ||
| 78 | + [:title-2 {:label "Sign in with Bluesky"}] | ||
| 79 | + [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}] | ||
| 80 | + [:label {:label "Handle"}] | ||
| 81 | + [:entry {:text @cells/form-handle | ||
| 82 | + :width-request 320 | ||
| 83 | + :placeholder "alice.bsky.social" | ||
| 84 | + :on-change #(reset! cells/form-handle %)}] | ||
| 85 | + [:vbox {:key :remembered :spacing 4} | ||
| 86 | + (when @cells/broker-token | ||
| 87 | + [:vbox {:spacing 4} | ||
| 88 | + [:dim-label {:label "Session remembered — Connect will not need the browser."}] | ||
| 89 | + [:button {:label "Forget saved session" :on-click actions/forget-session!}]])] | ||
| 90 | + [:vbox {:key :login-url :spacing 4} | ||
| 91 | + (when-let [url @cells/login-url] | ||
| 92 | + [:vbox {:spacing 4} | ||
| 93 | + [:dim-label {:label "If the browser did not open, visit:"}] | ||
| 94 | + [:label {:label url}]])]] | ||
| 95 | + | ||
| 96 | + :app-password | ||
| 97 | + [:vbox {:spacing 6} | ||
| 98 | + [:title-2 {:label "Sign in with an app password"}] | ||
| 99 | + [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}] | ||
| 100 | + [:label {:label "Handle"}] | ||
| 101 | + [:entry {:text @cells/form-handle | ||
| 102 | + :width-request 320 | ||
| 103 | + :placeholder "alice.bsky.social" | ||
| 104 | + :on-change #(reset! cells/form-handle %)}] | ||
| 105 | + [:label {:label "App password"}] | ||
| 106 | + [:entry {:text @cells/form-app-password | ||
| 107 | + :width-request 320 | ||
| 108 | + :placeholder "xxxx-xxxx-xxxx-xxxx" | ||
| 109 | + :on-change #(reset! cells/form-app-password %)}] | ||
| 110 | + [:dim-label {:label "Make one at bsky.app → Settings → App Passwords."}]] | ||
| 111 | + | ||
| 112 | + [:vbox {:spacing 6} | ||
| 113 | + [:title-2 {:label "Connect as guest"}] | ||
| 114 | + [:label {:label "Nick"}] | ||
| 115 | + [:entry {:text @cells/form-nick | ||
| 116 | + :width-request 320 | ||
| 117 | + :placeholder "your nick" | ||
| 118 | + :on-change #(reset! cells/form-nick %)}]]) | ||
| 119 | + [server-fields] | ||
| 120 | + [:separator {}] | ||
| 121 | + [connect-action]] | ||
| 122 | + [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]]) | ||
modified
flutter/src/frq/hiccup.cljd +18 -3 | @@ -98,8 +98,15 @@ | ||
| 98 | 98 | for destructive, and a component-coloured fill for standard — COSMIC's |
| 99 | 99 | standard button is a filled surface, not an outline." |
| 100 | 100 | [ctx p on] |
| 101 | - (let [kind (cond (:destructive p) :destructive | |
| 102 | - (or (:primary p) (= "primary" (:kind p))) :suggested | |
| 101 | + ;; `:kind` carries a keyword in frq.app's own hiccup — `:kind :primary` — | |
| 102 | + ;; and a string was what this compared against, so the connect screen's | |
| 103 | + ;; Connect button came out standard. Both spellings, since `:primary true` | |
| 104 | + ;; is also written. | |
| 105 | + (let [k (:kind p) | |
| 106 | + kind (cond (or (:destructive p) (= :destructive k) (= "destructive" k)) | |
| 107 | + :destructive | |
| 108 | + (or (:primary p) (= :primary k) (= "primary" k)) | |
| 109 | + :suggested | |
| 103 | 110 | :else :standard) |
| 104 | 111 | bg (case kind |
| 105 | 112 | :suggested t/accent |
| @@ -143,8 +150,16 @@ | ||
| 143 | 150 | c)) |
| 144 | 151 | |
| 145 | 152 | ;; `page` is jolt-cosmic's scrollable container, centred and capped. |
| 153 | + ;; | |
| 154 | + ;; Align with a heightFactor rather than Center: a Center inside an | |
| 155 | + ;; unbounded height — which is what a scroll view gives — grows to | |
| 156 | + ;; infinity and centres its content somewhere far below the screen. | |
| 157 | + ;; The page looked blank and nothing was logged, because nothing was | |
| 158 | + ;; wrong: the content was exactly where it had been asked to go. | |
| 146 | 159 | :page |
| 147 | - (m/Center | |
| 160 | + (m/Align | |
| 161 | + .alignment m/Alignment.topCenter | |
| 162 | + .heightFactor 1.0 | |
| 148 | 163 | .child (m/ConstrainedBox |
| 149 | 164 | .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0)) |
| 150 | 165 | .child (m/Padding |
| @@ -98,8 +98,15 @@ | |||
| 98 | for destructive, and a component-coloured fill for standard — COSMIC's | 98 | for destructive, and a component-coloured fill for standard — COSMIC's |
| 99 | standard button is a filled surface, not an outline." | 99 | standard button is a filled surface, not an outline." |
| 100 | [ctx p on] | 100 | [ctx p on] |
| 101 | - (let [kind (cond (:destructive p) :destructive | 101 | + ;; `:kind` carries a keyword in frq.app's own hiccup — `:kind :primary` — |
| 102 | - (or (:primary p) (= "primary" (:kind p))) :suggested | 102 | + ;; and a string was what this compared against, so the connect screen's |
| 103 | + ;; Connect button came out standard. Both spellings, since `:primary true` | ||
| 104 | + ;; is also written. | ||
| 105 | + (let [k (:kind p) | ||
| 106 | + kind (cond (or (:destructive p) (= :destructive k) (= "destructive" k)) | ||
| 107 | + :destructive | ||
| 108 | + (or (:primary p) (= :primary k) (= "primary" k)) | ||
| 109 | + :suggested | ||
| 103 | :else :standard) | 110 | :else :standard) |
| 104 | bg (case kind | 111 | bg (case kind |
| 105 | :suggested t/accent | 112 | :suggested t/accent |
| @@ -143,8 +150,16 @@ | |||
| 143 | c)) | 150 | c)) |
| 144 | 151 | ||
| 145 | ;; `page` is jolt-cosmic's scrollable container, centred and capped. | 152 | ;; `page` is jolt-cosmic's scrollable container, centred and capped. |
| 153 | + ;; | ||
| 154 | + ;; Align with a heightFactor rather than Center: a Center inside an | ||
| 155 | + ;; unbounded height — which is what a scroll view gives — grows to | ||
| 156 | + ;; infinity and centres its content somewhere far below the screen. | ||
| 157 | + ;; The page looked blank and nothing was logged, because nothing was | ||
| 158 | + ;; wrong: the content was exactly where it had been asked to go. | ||
| 146 | :page | 159 | :page |
| 147 | - (m/Center | 160 | + (m/Align |
| 161 | + .alignment m/Alignment.topCenter | ||
| 162 | + .heightFactor 1.0 | ||
| 148 | .child (m/ConstrainedBox | 163 | .child (m/ConstrainedBox |
| 149 | .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0)) | 164 | .constraints (m/BoxConstraints .maxWidth (dbl (:max-width p) 520.0)) |
| 150 | .child (m/Padding | 165 | .child (m/Padding |
modified
flutter/src/frq/main.cljd +73 -49 | @@ -25,54 +25,69 @@ | ||
| 25 | 25 | [frq.net.dart :as net] |
| 26 | 26 | [frq.atproto.dart :as atproto] |
| 27 | 27 | [frq.clock :as clock] |
| 28 | - [frq.store :as store])) | |
| 28 | + [frq.store :as store] | |
| 29 | + [frq.actions :as actions] | |
| 30 | + [frq.cells :as cells] | |
| 31 | + [frq.screens.connect :as connect])) | |
| 29 | 32 | |
| 30 | 33 | (defonce ^:private lines (atom [])) |
| 31 | -(defonce ^:private status (atom "not connected")) | |
| 32 | 34 | (defonce ^:private conn (atom nil)) |
| 33 | - | |
| 34 | -(def ^:private host-name "irc.freeq.at") | |
| 35 | -(def ^:private host-port 6697) | |
| 35 | +(defonce ^:private status (atom "Not connected")) | |
| 36 | 36 | |
| 37 | 37 | (defn- note! [m] |
| 38 | - ;; Bounded, because a backlog is hundreds of lines and this is a proof | |
| 39 | - ;; rather than a message list — `frq.state/apply-msg!` is what turns these | |
| 40 | - ;; into rooms, and it is not portable yet. | |
| 41 | - (swap! lines (fn [v] (vec (take-last 14 (conj v m)))))) | |
| 38 | + (swap! lines (fn [v] (vec (take-last 8 (conj v m)))))) | |
| 39 | + | |
| 40 | +(defn ^:async connect! | |
| 41 | + "What `frq.actions/connect!` is on the phone. | |
| 42 | + | |
| 43 | + The desktop's is `frq.state/connect!` — jolt's TLS, SASL, a reader thread. | |
| 44 | + This is the dart:io one, and it reads the same cells the screen wrote: the | |
| 45 | + host, the port and the TLS tick are `frq.cells`, filled in by the entry and | |
| 46 | + the checkbutton on screen. | |
| 42 | 47 | |
| 43 | -(defn ^:async connect! [] | |
| 44 | - (reset! status (str "connecting to " host-name ":" host-port " over TLS…")) | |
| 48 | + Guest registration only. SASL wants `frq.msgsig`, which is not portable | |
| 49 | + yet — so `frq.actions` answering a name it cannot fully honour is the point | |
| 50 | + of that seam: the screen renders and the button works, and what is behind it | |
| 51 | + grows." | |
| 52 | + [] | |
| 53 | + (reset! cells/connecting? true) | |
| 54 | + (reset! cells/error nil) | |
| 55 | + (reset! status (str "Connecting to " @cells/form-host "…")) | |
| 45 | 56 | (reset! lines []) |
| 46 | 57 | (try |
| 47 | 58 | (let [sock (await (net/connect! |
| 48 | - {:host host-name | |
| 49 | - :port host-port | |
| 50 | - :tls? true | |
| 59 | + {:host @cells/form-host | |
| 60 | + ;; The cell is a string, because it is what an :entry holds. | |
| 61 | + :port (or (parse-long (str @cells/form-port)) 6697) | |
| 62 | + :tls? (boolean @cells/form-tls?) | |
| 51 | 63 | :on-msg (fn [m] |
| 52 | 64 | (note! m) |
| 53 | 65 | (when (= "001" (:command m)) |
| 54 | - (reset! status "registered"))) | |
| 66 | + (reset! cells/connecting? false) | |
| 67 | + (reset! status | |
| 68 | + (str "Connected as " @cells/form-nick)))) | |
| 55 | 69 | :on-close (fn [why] |
| 56 | - (reset! status (str "closed" (when why (str ": " why)))))}))] | |
| 70 | + (reset! cells/connecting? false) | |
| 71 | + (reset! status "Not connected") | |
| 72 | + (when why (reset! cells/error why)))}))] | |
| 57 | 73 | (reset! conn sock) |
| 58 | - (reset! status "TLS up — registering") | |
| 59 | - ;; A guest registration and nothing else. No SASL: that is frq.msgsig | |
| 60 | - ;; and frq.atproto, neither of which is portable yet. | |
| 61 | - (net/send-line! sock "NICK frq-phone") | |
| 62 | - (net/send-line! sock "USER frq-phone 0 * :frq")) | |
| 74 | + (net/send-line! sock (str "NICK " @cells/form-nick)) | |
| 75 | + (net/send-line! sock (str "USER " @cells/form-nick " 0 * :frq"))) | |
| 63 | 76 | (catch Exception e |
| 64 | - (reset! status (str "failed: " e))))) | |
| 77 | + (reset! cells/connecting? false) | |
| 78 | + (reset! status "Not connected") | |
| 79 | + (reset! cells/error (str e))))) | |
| 80 | + | |
| 81 | +(defn- disconnect! [] | |
| 82 | + (when-let [c @conn] (net/close! c)) | |
| 83 | + (reset! conn nil) | |
| 84 | + (reset! cells/connecting? false) | |
| 85 | + (reset! status "Not connected")) | |
| 65 | 86 | |
| 66 | 87 | (defonce ^:private handle (atom "nandi-test.bsky.social")) |
| 67 | 88 | (defonce ^:private identity-out (atom nil)) |
| 68 | 89 | |
| 69 | -(defn ^:async resolve-identity! | |
| 70 | - "handle → DID → PDS, over HTTPS from the phone. | |
| 71 | - | |
| 72 | - The same three calls `frq.atproto` makes on the desktop, off the same | |
| 73 | - `frq.atproto.core`: core says what to ask and what the answer means, and | |
| 74 | - only who waits for it differs." | |
| 75 | - [] | |
| 90 | +(defn ^:async resolve-identity! [] | |
| 76 | 91 | (reset! identity-out ["resolving…"]) |
| 77 | 92 | (try |
| 78 | 93 | (let [h @handle |
| @@ -83,59 +98,68 @@ | ||
| 83 | 98 | (reset! identity-out [(str "failed: " e)])))) |
| 84 | 99 | |
| 85 | 100 | (defn- screen |
| 86 | - "glimmer's tags, painted by `frq.hiccup`. Nothing here is Flutter." | |
| 101 | + "The phone's panels, in glimmer's tags. | |
| 102 | + | |
| 103 | + NOT `frq.screens.connect` yet, and the reason is worth writing down rather | |
| 104 | + than leaving as a TODO: the extraction is done and the desktop renders the | |
| 105 | + shared file, but rendering it here paints nothing at all — no exception, | |
| 106 | + nothing in logcat, and the sibling widgets in the same `children` vector | |
| 107 | + disappear with it, which is what a Dart `Error` thrown while that vector is | |
| 108 | + being built looks like. `(catch Object ...)` around it did not surface one | |
| 109 | + either, so the failure is not where it appears to be. | |
| 110 | + | |
| 111 | + What is already proven: `frq.cells` compiles under both compilers and holds | |
| 112 | + its values here — a probe read `cells/status` as \"Not connected\" and | |
| 113 | + `cells/form-nick` as \"frq-guest\" on the device. So the seam is sound and | |
| 114 | + what is left is one cljd-shaped bug between it and the screen." | |
| 87 | 115 | [] |
| 88 | - [:page {:max-width 560} | |
| 116 | + [:page {:max-width 520} | |
| 89 | 117 | [:title {:label "frq"}] |
| 90 | - [:dim-label {:label "TLS from dart:io, parsed by the desktop's own frq.irc.parse."}] | |
| 118 | + [:dim-label {:label "Painted by frq.hiccup in COSMIC's own theme."}] | |
| 91 | 119 | [:card {} |
| 92 | 120 | [:title-2 {:label "Connection"}] |
| 93 | 121 | [:label {:label @status}] |
| 94 | 122 | [:hbox {:spacing 8} |
| 95 | - [:button {:label "Connect" :primary true :on-click #(connect!)}] | |
| 96 | - [:button {:label "Disconnect" | |
| 97 | - :destructive true | |
| 98 | - :on-click #(do (when-let [c @conn] (net/close! c)) | |
| 99 | - (reset! status "closed"))}]]] | |
| 123 | + [:button {:label "Connect" :kind :primary :on-click #(connect!)}] | |
| 124 | + [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]]] | |
| 100 | 125 | [:card {} |
| 101 | 126 | [:title-2 {:label "What the server said"}] |
| 102 | 127 | (if (empty? @lines) |
| 103 | 128 | [:dim-label {:label "nothing yet"}] |
| 104 | 129 | (for [[i m] (map-indexed vector @lines)] |
| 105 | - [:vbox {:key i :spacing 0} | |
| 106 | - [:label {:label (str (:command m) | |
| 107 | - (when-let [p (seq (:params m))] | |
| 108 | - (str " " (last p))))}]]))] | |
| 130 | + [:label {:key i | |
| 131 | + :label (str (:command m) | |
| 132 | + (when-let [p (seq (:params m))] | |
| 133 | + (str " " (last p))))}]))] | |
| 109 | 134 | [:card {} |
| 110 | 135 | [:title-2 {:label "Identity"}] |
| 111 | 136 | [:entry {:key :handle |
| 112 | 137 | :text @handle |
| 113 | 138 | :placeholder "alice.bsky.social" |
| 114 | 139 | :on-change #(reset! handle %)}] |
| 115 | - [:button {:label "Resolve" :primary true :on-click #(resolve-identity!)}] | |
| 140 | + [:button {:label "Resolve" :kind :primary :on-click #(resolve-identity!)}] | |
| 116 | 141 | (when-let [out @identity-out] |
| 117 | 142 | (for [[i line] (map-indexed vector out)] |
| 118 | 143 | [:label {:key i :label line}]))] |
| 119 | 144 | [:card {} |
| 120 | 145 | [:title-2 {:label "Shared with the desktop"}] |
| 121 | 146 | [:label {:label (str "clock " (clock/clock-time (clock/now-ms)))}] |
| 122 | - [:label {:label (str "store " (if-let [s (store/load-session)] | |
| 123 | - (str "signed in as " (:handle s)) | |
| 124 | - "no saved session"))}]]]) | |
| 147 | + [:label {:label (str "cells " @cells/form-nick " @ " @cells/form-host | |
| 148 | + ":" @cells/form-port)}] | |
| 149 | + [:dim-label {:label "frq.cells, compiled by both."}]]]) | |
| 125 | 150 | |
| 126 | 151 | (defn ^:async main [] |
| 127 | 152 | (m/WidgetsFlutterBinding.ensureInitialized) |
| 128 | 153 | (let [dir (.-path (await (pp/getApplicationSupportDirectory)))] |
| 129 | 154 | (host/install! dir) |
| 155 | + ;; What the shared screen calls. The desktop installs frq.state's | |
| 156 | + ;; reducers here; this installs the phone's. | |
| 157 | + (actions/install! {:connect! connect! :disconnect! disconnect!}) | |
| 130 | 158 | (f/run |
| 131 | 159 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 132 | 160 | .home |
| 133 | 161 | (m/Scaffold .appBar (m/AppBar .title (m/Text "frq"))) |
| 134 | 162 | .body |
| 135 | - ;; One f/widget with the render as its body, rather than `:watch` as a | |
| 136 | - ;; link in the chain. A bare directive in the chain builds nothing to | |
| 137 | - ;; rebuild, so the cells changed and the screen did not — which looks | |
| 138 | - ;; exactly like a button that does not fire. | |
| 139 | 163 | (f/widget |
| 140 | 164 | :watch [st status ls lines io identity-out hh handle] |
| 141 | 165 | (m/SingleChildScrollView |
| @@ -25,54 +25,69 @@ | |||
| 25 | [frq.net.dart :as net] | 25 | [frq.net.dart :as net] |
| 26 | [frq.atproto.dart :as atproto] | 26 | [frq.atproto.dart :as atproto] |
| 27 | [frq.clock :as clock] | 27 | [frq.clock :as clock] |
| 28 | - [frq.store :as store])) | 28 | + [frq.store :as store] |
| 29 | + [frq.actions :as actions] | ||
| 30 | + [frq.cells :as cells] | ||
| 31 | + [frq.screens.connect :as connect])) | ||
| 29 | 32 | ||
| 30 | (defonce ^:private lines (atom [])) | 33 | (defonce ^:private lines (atom [])) |
| 31 | -(defonce ^:private status (atom "not connected")) | ||
| 32 | (defonce ^:private conn (atom nil)) | 34 | (defonce ^:private conn (atom nil)) |
| 33 | - | 35 | +(defonce ^:private status (atom "Not connected")) |
| 34 | -(def ^:private host-name "irc.freeq.at") | ||
| 35 | -(def ^:private host-port 6697) | ||
| 36 | 36 | ||
| 37 | (defn- note! [m] | 37 | (defn- note! [m] |
| 38 | - ;; Bounded, because a backlog is hundreds of lines and this is a proof | 38 | + (swap! lines (fn [v] (vec (take-last 8 (conj v m)))))) |
| 39 | - ;; rather than a message list — `frq.state/apply-msg!` is what turns these | 39 | + |
| 40 | - ;; into rooms, and it is not portable yet. | 40 | +(defn ^:async connect! |
| 41 | - (swap! lines (fn [v] (vec (take-last 14 (conj v m)))))) | 41 | + "What `frq.actions/connect!` is on the phone. |
| 42 | + | ||
| 43 | + The desktop's is `frq.state/connect!` — jolt's TLS, SASL, a reader thread. | ||
| 44 | + This is the dart:io one, and it reads the same cells the screen wrote: the | ||
| 45 | + host, the port and the TLS tick are `frq.cells`, filled in by the entry and | ||
| 46 | + the checkbutton on screen. | ||
| 42 | 47 | ||
| 43 | -(defn ^:async connect! [] | 48 | + Guest registration only. SASL wants `frq.msgsig`, which is not portable |
| 44 | - (reset! status (str "connecting to " host-name ":" host-port " over TLS…")) | 49 | + yet — so `frq.actions` answering a name it cannot fully honour is the point |
| 50 | + of that seam: the screen renders and the button works, and what is behind it | ||
| 51 | + grows." | ||
| 52 | + [] | ||
| 53 | + (reset! cells/connecting? true) | ||
| 54 | + (reset! cells/error nil) | ||
| 55 | + (reset! status (str "Connecting to " @cells/form-host "…")) | ||
| 45 | (reset! lines []) | 56 | (reset! lines []) |
| 46 | (try | 57 | (try |
| 47 | (let [sock (await (net/connect! | 58 | (let [sock (await (net/connect! |
| 48 | - {:host host-name | 59 | + {:host @cells/form-host |
| 49 | - :port host-port | 60 | + ;; The cell is a string, because it is what an :entry holds. |
| 50 | - :tls? true | 61 | + :port (or (parse-long (str @cells/form-port)) 6697) |
| 62 | + :tls? (boolean @cells/form-tls?) | ||
| 51 | :on-msg (fn [m] | 63 | :on-msg (fn [m] |
| 52 | (note! m) | 64 | (note! m) |
| 53 | (when (= "001" (:command m)) | 65 | (when (= "001" (:command m)) |
| 54 | - (reset! status "registered"))) | 66 | + (reset! cells/connecting? false) |
| 67 | + (reset! status | ||
| 68 | + (str "Connected as " @cells/form-nick)))) | ||
| 55 | :on-close (fn [why] | 69 | :on-close (fn [why] |
| 56 | - (reset! status (str "closed" (when why (str ": " why)))))}))] | 70 | + (reset! cells/connecting? false) |
| 71 | + (reset! status "Not connected") | ||
| 72 | + (when why (reset! cells/error why)))}))] | ||
| 57 | (reset! conn sock) | 73 | (reset! conn sock) |
| 58 | - (reset! status "TLS up — registering") | 74 | + (net/send-line! sock (str "NICK " @cells/form-nick)) |
| 59 | - ;; A guest registration and nothing else. No SASL: that is frq.msgsig | 75 | + (net/send-line! sock (str "USER " @cells/form-nick " 0 * :frq"))) |
| 60 | - ;; and frq.atproto, neither of which is portable yet. | ||
| 61 | - (net/send-line! sock "NICK frq-phone") | ||
| 62 | - (net/send-line! sock "USER frq-phone 0 * :frq")) | ||
| 63 | (catch Exception e | 76 | (catch Exception e |
| 64 | - (reset! status (str "failed: " e))))) | 77 | + (reset! cells/connecting? false) |
| 78 | + (reset! status "Not connected") | ||
| 79 | + (reset! cells/error (str e))))) | ||
| 80 | + | ||
| 81 | +(defn- disconnect! [] | ||
| 82 | + (when-let [c @conn] (net/close! c)) | ||
| 83 | + (reset! conn nil) | ||
| 84 | + (reset! cells/connecting? false) | ||
| 85 | + (reset! status "Not connected")) | ||
| 65 | 86 | ||
| 66 | (defonce ^:private handle (atom "nandi-test.bsky.social")) | 87 | (defonce ^:private handle (atom "nandi-test.bsky.social")) |
| 67 | (defonce ^:private identity-out (atom nil)) | 88 | (defonce ^:private identity-out (atom nil)) |
| 68 | 89 | ||
| 69 | -(defn ^:async resolve-identity! | 90 | +(defn ^:async resolve-identity! [] |
| 70 | - "handle → DID → PDS, over HTTPS from the phone. | ||
| 71 | - | ||
| 72 | - The same three calls `frq.atproto` makes on the desktop, off the same | ||
| 73 | - `frq.atproto.core`: core says what to ask and what the answer means, and | ||
| 74 | - only who waits for it differs." | ||
| 75 | - [] | ||
| 76 | (reset! identity-out ["resolving…"]) | 91 | (reset! identity-out ["resolving…"]) |
| 77 | (try | 92 | (try |
| 78 | (let [h @handle | 93 | (let [h @handle |
| @@ -83,59 +98,68 @@ | |||
| 83 | (reset! identity-out [(str "failed: " e)])))) | 98 | (reset! identity-out [(str "failed: " e)])))) |
| 84 | 99 | ||
| 85 | (defn- screen | 100 | (defn- screen |
| 86 | - "glimmer's tags, painted by `frq.hiccup`. Nothing here is Flutter." | 101 | + "The phone's panels, in glimmer's tags. |
| 102 | + | ||
| 103 | + NOT `frq.screens.connect` yet, and the reason is worth writing down rather | ||
| 104 | + than leaving as a TODO: the extraction is done and the desktop renders the | ||
| 105 | + shared file, but rendering it here paints nothing at all — no exception, | ||
| 106 | + nothing in logcat, and the sibling widgets in the same `children` vector | ||
| 107 | + disappear with it, which is what a Dart `Error` thrown while that vector is | ||
| 108 | + being built looks like. `(catch Object ...)` around it did not surface one | ||
| 109 | + either, so the failure is not where it appears to be. | ||
| 110 | + | ||
| 111 | + What is already proven: `frq.cells` compiles under both compilers and holds | ||
| 112 | + its values here — a probe read `cells/status` as \"Not connected\" and | ||
| 113 | + `cells/form-nick` as \"frq-guest\" on the device. So the seam is sound and | ||
| 114 | + what is left is one cljd-shaped bug between it and the screen." | ||
| 87 | [] | 115 | [] |
| 88 | - [:page {:max-width 560} | 116 | + [:page {:max-width 520} |
| 89 | [:title {:label "frq"}] | 117 | [:title {:label "frq"}] |
| 90 | - [:dim-label {:label "TLS from dart:io, parsed by the desktop's own frq.irc.parse."}] | 118 | + [:dim-label {:label "Painted by frq.hiccup in COSMIC's own theme."}] |
| 91 | [:card {} | 119 | [:card {} |
| 92 | [:title-2 {:label "Connection"}] | 120 | [:title-2 {:label "Connection"}] |
| 93 | [:label {:label @status}] | 121 | [:label {:label @status}] |
| 94 | [:hbox {:spacing 8} | 122 | [:hbox {:spacing 8} |
| 95 | - [:button {:label "Connect" :primary true :on-click #(connect!)}] | 123 | + [:button {:label "Connect" :kind :primary :on-click #(connect!)}] |
| 96 | - [:button {:label "Disconnect" | 124 | + [:button {:label "Disconnect" :destructive true :on-click #(disconnect!)}]]] |
| 97 | - :destructive true | ||
| 98 | - :on-click #(do (when-let [c @conn] (net/close! c)) | ||
| 99 | - (reset! status "closed"))}]]] | ||
| 100 | [:card {} | 125 | [:card {} |
| 101 | [:title-2 {:label "What the server said"}] | 126 | [:title-2 {:label "What the server said"}] |
| 102 | (if (empty? @lines) | 127 | (if (empty? @lines) |
| 103 | [:dim-label {:label "nothing yet"}] | 128 | [:dim-label {:label "nothing yet"}] |
| 104 | (for [[i m] (map-indexed vector @lines)] | 129 | (for [[i m] (map-indexed vector @lines)] |
| 105 | - [:vbox {:key i :spacing 0} | 130 | + [:label {:key i |
| 106 | - [:label {:label (str (:command m) | 131 | + :label (str (:command m) |
| 107 | - (when-let [p (seq (:params m))] | 132 | + (when-let [p (seq (:params m))] |
| 108 | - (str " " (last p))))}]]))] | 133 | + (str " " (last p))))}]))] |
| 109 | [:card {} | 134 | [:card {} |
| 110 | [:title-2 {:label "Identity"}] | 135 | [:title-2 {:label "Identity"}] |
| 111 | [:entry {:key :handle | 136 | [:entry {:key :handle |
| 112 | :text @handle | 137 | :text @handle |
| 113 | :placeholder "alice.bsky.social" | 138 | :placeholder "alice.bsky.social" |
| 114 | :on-change #(reset! handle %)}] | 139 | :on-change #(reset! handle %)}] |
| 115 | - [:button {:label "Resolve" :primary true :on-click #(resolve-identity!)}] | 140 | + [:button {:label "Resolve" :kind :primary :on-click #(resolve-identity!)}] |
| 116 | (when-let [out @identity-out] | 141 | (when-let [out @identity-out] |
| 117 | (for [[i line] (map-indexed vector out)] | 142 | (for [[i line] (map-indexed vector out)] |
| 118 | [:label {:key i :label line}]))] | 143 | [:label {:key i :label line}]))] |
| 119 | [:card {} | 144 | [:card {} |
| 120 | [:title-2 {:label "Shared with the desktop"}] | 145 | [:title-2 {:label "Shared with the desktop"}] |
| 121 | [:label {:label (str "clock " (clock/clock-time (clock/now-ms)))}] | 146 | [:label {:label (str "clock " (clock/clock-time (clock/now-ms)))}] |
| 122 | - [:label {:label (str "store " (if-let [s (store/load-session)] | 147 | + [:label {:label (str "cells " @cells/form-nick " @ " @cells/form-host |
| 123 | - (str "signed in as " (:handle s)) | 148 | + ":" @cells/form-port)}] |
| 124 | - "no saved session"))}]]]) | 149 | + [:dim-label {:label "frq.cells, compiled by both."}]]]) |
| 125 | 150 | ||
| 126 | (defn ^:async main [] | 151 | (defn ^:async main [] |
| 127 | (m/WidgetsFlutterBinding.ensureInitialized) | 152 | (m/WidgetsFlutterBinding.ensureInitialized) |
| 128 | (let [dir (.-path (await (pp/getApplicationSupportDirectory)))] | 153 | (let [dir (.-path (await (pp/getApplicationSupportDirectory)))] |
| 129 | (host/install! dir) | 154 | (host/install! dir) |
| 155 | + ;; What the shared screen calls. The desktop installs frq.state's | ||
| 156 | + ;; reducers here; this installs the phone's. | ||
| 157 | + (actions/install! {:connect! connect! :disconnect! disconnect!}) | ||
| 130 | (f/run | 158 | (f/run |
| 131 | (m/MaterialApp .title "frq" .theme (t/app-theme)) | 159 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 132 | .home | 160 | .home |
| 133 | (m/Scaffold .appBar (m/AppBar .title (m/Text "frq"))) | 161 | (m/Scaffold .appBar (m/AppBar .title (m/Text "frq"))) |
| 134 | .body | 162 | .body |
| 135 | - ;; One f/widget with the render as its body, rather than `:watch` as a | ||
| 136 | - ;; link in the chain. A bare directive in the chain builds nothing to | ||
| 137 | - ;; rebuild, so the cells changed and the screen did not — which looks | ||
| 138 | - ;; exactly like a button that does not fire. | ||
| 139 | (f/widget | 163 | (f/widget |
| 140 | :watch [st status ls lines io identity-out hh handle] | 164 | :watch [st status ls lines io identity-out hh handle] |
| 141 | (m/SingleChildScrollView | 165 | (m/SingleChildScrollView |
modified
src/frq/app.clj +4 -106 | @@ -26,24 +26,14 @@ | ||
| 26 | 26 | [frq.media :as media] |
| 27 | 27 | [frq.platform :as platform] |
| 28 | 28 | [frq.profile :as profile] |
| 29 | + ;; The connect screen lives in common/ now — the same file the | |
| 30 | + ;; phone renders. It reads frq.cells and calls frq.actions, and | |
| 31 | + ;; this requires it exactly where its own copy used to be. | |
| 32 | + [frq.screens.connect :as connect :refer [connect-screen error-note]] | |
| 29 | 33 | [frq.state :as s])) |
| 30 | 34 | |
| 31 | 35 | ;; ---------------------------------------------------------------- pieces |
| 32 | 36 | |
| 33 | -(defn error-note | |
| 34 | - "Always a node, never nil. | |
| 35 | - | |
| 36 | - A conditional child that disappears shifts every sibling after it, and the | |
| 37 | - reconciler matches children by position — so an error appearing mid-screen | |
| 38 | - would patch the header into a card. A stable wrapper with a stable key keeps | |
| 39 | - the shape of the tree fixed and only its contents changing." | |
| 40 | - [] | |
| 41 | - [:vbox {:key :error-note :spacing 6} | |
| 42 | - (when-let [e @s/error] | |
| 43 | - [:card {} | |
| 44 | - [:label {:label (str "⚠ " e)}] | |
| 45 | - [:button {:label "Dismiss" :on-click #(reset! s/error nil)}]])]) | |
| 46 | - | |
| 47 | 37 | (defn tab-bar [] |
| 48 | 38 | [:hbox {:spacing 8} |
| 49 | 39 | (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]] |
| @@ -52,98 +42,6 @@ | ||
| 52 | 42 | :kind (if (= k @s/screen) :primary :default) |
| 53 | 43 | :on-click #(reset! s/screen k)}])]) |
| 54 | 44 | |
| 55 | -;; ---------------------------------------------------------------- connect | |
| 56 | - | |
| 57 | -(defn- mode-tabs [] | |
| 58 | - [:hbox {:spacing 8} | |
| 59 | - (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]] | |
| 60 | - [:button {:key k | |
| 61 | - :label label | |
| 62 | - :kind (if (= k @s/auth-mode) :primary :default) | |
| 63 | - :on-click #(reset! s/auth-mode k)}])]) | |
| 64 | - | |
| 65 | -(defn- server-fields [] | |
| 66 | - [:vbox {:spacing 6} | |
| 67 | - [:label {:label "Server"}] | |
| 68 | - [:hbox {:spacing 8} | |
| 69 | - [:entry {:text @s/form-host | |
| 70 | - :width-request 220 | |
| 71 | - :placeholder "host" | |
| 72 | - :on-change #(reset! s/form-host %)}] | |
| 73 | - [:entry {:text @s/form-port | |
| 74 | - :width-request 90 | |
| 75 | - :placeholder "6697" | |
| 76 | - :on-change #(reset! s/form-port %)}]] | |
| 77 | - [:checkbutton {:label "TLS" | |
| 78 | - :active @s/form-tls? | |
| 79 | - :on-toggled #(do (swap! s/form-tls? not) | |
| 80 | - (reset! s/form-port | |
| 81 | - (if @s/form-tls? "6697" "6667")))}]]) | |
| 82 | - | |
| 83 | -(defn- connect-action [] | |
| 84 | - (if @s/connecting? | |
| 85 | - [:hbox {:spacing 8} | |
| 86 | - [:spinner {}] | |
| 87 | - [:dim-label {:label @s/status}]] | |
| 88 | - [:hbox {:spacing 8} | |
| 89 | - [:button {:label "Connect" :kind :primary :on-click s/connect!}] | |
| 90 | - [:dim-label {:label @s/status}]])) | |
| 91 | - | |
| 92 | -(defn connect-screen [] | |
| 93 | - [:page {:max-width 520} | |
| 94 | - [:title {:label "frq"}] | |
| 95 | - [:dim-label {:label "freeq client — guest, or your Bluesky identity."}] | |
| 96 | - [error-note] | |
| 97 | - [:card {} | |
| 98 | - [mode-tabs] | |
| 99 | - (case @s/auth-mode | |
| 100 | - :bluesky | |
| 101 | - [:vbox {:spacing 6} | |
| 102 | - [:title-2 {:label "Sign in with Bluesky"}] | |
| 103 | - [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}] | |
| 104 | - [:label {:label "Handle"}] | |
| 105 | - [:entry {:text @s/form-handle | |
| 106 | - :width-request 320 | |
| 107 | - :placeholder "alice.bsky.social" | |
| 108 | - :on-change #(reset! s/form-handle %)}] | |
| 109 | - [:vbox {:key :remembered :spacing 4} | |
| 110 | - (when @s/broker-token | |
| 111 | - [:vbox {:spacing 4} | |
| 112 | - [:dim-label {:label "Session remembered — Connect will not need the browser."}] | |
| 113 | - [:button {:label "Forget saved session" :on-click s/forget-session!}]])] | |
| 114 | - [:vbox {:key :login-url :spacing 4} | |
| 115 | - (when-let [url @s/login-url] | |
| 116 | - [:vbox {:spacing 4} | |
| 117 | - [:dim-label {:label "If the browser did not open, visit:"}] | |
| 118 | - [:label {:label url}]])]] | |
| 119 | - | |
| 120 | - :app-password | |
| 121 | - [:vbox {:spacing 6} | |
| 122 | - [:title-2 {:label "Sign in with an app password"}] | |
| 123 | - [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}] | |
| 124 | - [:label {:label "Handle"}] | |
| 125 | - [:entry {:text @s/form-handle | |
| 126 | - :width-request 320 | |
| 127 | - :placeholder "alice.bsky.social" | |
| 128 | - :on-change #(reset! s/form-handle %)}] | |
| 129 | - [:label {:label "App password"}] | |
| 130 | - [:entry {:text @s/form-app-password | |
| 131 | - :width-request 320 | |
| 132 | - :placeholder "xxxx-xxxx-xxxx-xxxx" | |
| 133 | - :on-change #(reset! s/form-app-password %)}] | |
| 134 | - [:dim-label {:label "Make one at bsky.app → Settings → App Passwords."}]] | |
| 135 | - | |
| 136 | - [:vbox {:spacing 6} | |
| 137 | - [:title-2 {:label "Connect as guest"}] | |
| 138 | - [:label {:label "Nick"}] | |
| 139 | - [:entry {:text @s/form-nick | |
| 140 | - :width-request 320 | |
| 141 | - :placeholder "your nick" | |
| 142 | - :on-change #(reset! s/form-nick %)}]]) | |
| 143 | - [server-fields] | |
| 144 | - [:separator {}] | |
| 145 | - [connect-action]] | |
| 146 | - [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]]) | |
| 147 | 45 | |
| 148 | 46 | ;; Whether the backend under this tree is a terminal, set by `frq.tui` before |
| 149 | 47 | ;; the first paint and never again. Two things in a message hang on it, and |
| @@ -26,24 +26,14 @@ | |||
| 26 | [frq.media :as media] | 26 | [frq.media :as media] |
| 27 | [frq.platform :as platform] | 27 | [frq.platform :as platform] |
| 28 | [frq.profile :as profile] | 28 | [frq.profile :as profile] |
| 29 | + ;; The connect screen lives in common/ now — the same file the | ||
| 30 | + ;; phone renders. It reads frq.cells and calls frq.actions, and | ||
| 31 | + ;; this requires it exactly where its own copy used to be. | ||
| 32 | + [frq.screens.connect :as connect :refer [connect-screen error-note]] | ||
| 29 | [frq.state :as s])) | 33 | [frq.state :as s])) |
| 30 | 34 | ||
| 31 | ;; ---------------------------------------------------------------- pieces | 35 | ;; ---------------------------------------------------------------- pieces |
| 32 | 36 | ||
| 33 | -(defn error-note | ||
| 34 | - "Always a node, never nil. | ||
| 35 | - | ||
| 36 | - A conditional child that disappears shifts every sibling after it, and the | ||
| 37 | - reconciler matches children by position — so an error appearing mid-screen | ||
| 38 | - would patch the header into a card. A stable wrapper with a stable key keeps | ||
| 39 | - the shape of the tree fixed and only its contents changing." | ||
| 40 | - [] | ||
| 41 | - [:vbox {:key :error-note :spacing 6} | ||
| 42 | - (when-let [e @s/error] | ||
| 43 | - [:card {} | ||
| 44 | - [:label {:label (str "⚠ " e)}] | ||
| 45 | - [:button {:label "Dismiss" :on-click #(reset! s/error nil)}]])]) | ||
| 46 | - | ||
| 47 | (defn tab-bar [] | 37 | (defn tab-bar [] |
| 48 | [:hbox {:spacing 8} | 38 | [:hbox {:spacing 8} |
| 49 | (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]] | 39 | (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]] |
| @@ -52,98 +42,6 @@ | |||
| 52 | :kind (if (= k @s/screen) :primary :default) | 42 | :kind (if (= k @s/screen) :primary :default) |
| 53 | :on-click #(reset! s/screen k)}])]) | 43 | :on-click #(reset! s/screen k)}])]) |
| 54 | 44 | ||
| 55 | -;; ---------------------------------------------------------------- connect | ||
| 56 | - | ||
| 57 | -(defn- mode-tabs [] | ||
| 58 | - [:hbox {:spacing 8} | ||
| 59 | - (for [[k label] [[:guest "Guest"] [:bluesky "Bluesky"] [:app-password "App password"]]] | ||
| 60 | - [:button {:key k | ||
| 61 | - :label label | ||
| 62 | - :kind (if (= k @s/auth-mode) :primary :default) | ||
| 63 | - :on-click #(reset! s/auth-mode k)}])]) | ||
| 64 | - | ||
| 65 | -(defn- server-fields [] | ||
| 66 | - [:vbox {:spacing 6} | ||
| 67 | - [:label {:label "Server"}] | ||
| 68 | - [:hbox {:spacing 8} | ||
| 69 | - [:entry {:text @s/form-host | ||
| 70 | - :width-request 220 | ||
| 71 | - :placeholder "host" | ||
| 72 | - :on-change #(reset! s/form-host %)}] | ||
| 73 | - [:entry {:text @s/form-port | ||
| 74 | - :width-request 90 | ||
| 75 | - :placeholder "6697" | ||
| 76 | - :on-change #(reset! s/form-port %)}]] | ||
| 77 | - [:checkbutton {:label "TLS" | ||
| 78 | - :active @s/form-tls? | ||
| 79 | - :on-toggled #(do (swap! s/form-tls? not) | ||
| 80 | - (reset! s/form-port | ||
| 81 | - (if @s/form-tls? "6697" "6667")))}]]) | ||
| 82 | - | ||
| 83 | -(defn- connect-action [] | ||
| 84 | - (if @s/connecting? | ||
| 85 | - [:hbox {:spacing 8} | ||
| 86 | - [:spinner {}] | ||
| 87 | - [:dim-label {:label @s/status}]] | ||
| 88 | - [:hbox {:spacing 8} | ||
| 89 | - [:button {:label "Connect" :kind :primary :on-click s/connect!}] | ||
| 90 | - [:dim-label {:label @s/status}]])) | ||
| 91 | - | ||
| 92 | -(defn connect-screen [] | ||
| 93 | - [:page {:max-width 520} | ||
| 94 | - [:title {:label "frq"}] | ||
| 95 | - [:dim-label {:label "freeq client — guest, or your Bluesky identity."}] | ||
| 96 | - [error-note] | ||
| 97 | - [:card {} | ||
| 98 | - [mode-tabs] | ||
| 99 | - (case @s/auth-mode | ||
| 100 | - :bluesky | ||
| 101 | - [:vbox {:spacing 6} | ||
| 102 | - [:title-2 {:label "Sign in with Bluesky"}] | ||
| 103 | - [:dim-label {:label "Opens your browser for AT Protocol OAuth. freeq's broker hands back a token; no password passes through frq."}] | ||
| 104 | - [:label {:label "Handle"}] | ||
| 105 | - [:entry {:text @s/form-handle | ||
| 106 | - :width-request 320 | ||
| 107 | - :placeholder "alice.bsky.social" | ||
| 108 | - :on-change #(reset! s/form-handle %)}] | ||
| 109 | - [:vbox {:key :remembered :spacing 4} | ||
| 110 | - (when @s/broker-token | ||
| 111 | - [:vbox {:spacing 4} | ||
| 112 | - [:dim-label {:label "Session remembered — Connect will not need the browser."}] | ||
| 113 | - [:button {:label "Forget saved session" :on-click s/forget-session!}]])] | ||
| 114 | - [:vbox {:key :login-url :spacing 4} | ||
| 115 | - (when-let [url @s/login-url] | ||
| 116 | - [:vbox {:spacing 4} | ||
| 117 | - [:dim-label {:label "If the browser did not open, visit:"}] | ||
| 118 | - [:label {:label url}]])]] | ||
| 119 | - | ||
| 120 | - :app-password | ||
| 121 | - [:vbox {:spacing 6} | ||
| 122 | - [:title-2 {:label "Sign in with an app password"}] | ||
| 123 | - [:dim-label {:label "No browser. Your app password goes to your own PDS; freeq is handed the session it mints."}] | ||
| 124 | - [:label {:label "Handle"}] | ||
| 125 | - [:entry {:text @s/form-handle | ||
| 126 | - :width-request 320 | ||
| 127 | - :placeholder "alice.bsky.social" | ||
| 128 | - :on-change #(reset! s/form-handle %)}] | ||
| 129 | - [:label {:label "App password"}] | ||
| 130 | - [:entry {:text @s/form-app-password | ||
| 131 | - :width-request 320 | ||
| 132 | - :placeholder "xxxx-xxxx-xxxx-xxxx" | ||
| 133 | - :on-change #(reset! s/form-app-password %)}] | ||
| 134 | - [:dim-label {:label "Make one at bsky.app → Settings → App Passwords."}]] | ||
| 135 | - | ||
| 136 | - [:vbox {:spacing 6} | ||
| 137 | - [:title-2 {:label "Connect as guest"}] | ||
| 138 | - [:label {:label "Nick"}] | ||
| 139 | - [:entry {:text @s/form-nick | ||
| 140 | - :width-request 320 | ||
| 141 | - :placeholder "your nick" | ||
| 142 | - :on-change #(reset! s/form-nick %)}]]) | ||
| 143 | - [server-fields] | ||
| 144 | - [:separator {}] | ||
| 145 | - [connect-action]] | ||
| 146 | - [:dim-label {:label "TLS rides jolt's OpenSSL bindings; untick it for a plain :6667 listener. Sign-in needs TLS, so it is desktop-only."}]]) | ||
| 147 | 45 | ||
| 148 | ;; Whether the backend under this tree is a terminal, set by `frq.tui` before | 46 | ;; Whether the backend under this tree is a terminal, set by `frq.tui` before |
| 149 | ;; the first paint and never again. Two things in a message hang on it, and | 47 | ;; the first paint and never again. Two things in a message hang on it, and |
modified
src/frq/state.clj +36 -27 | @@ -6,6 +6,8 @@ | ||
| 6 | 6 | is the only place a wire message turns into UI state." |
| 7 | 7 | (:require [clojure.string :as str] |
| 8 | 8 | [glimmer.ratom :as r :refer [atom]] |
| 9 | + [frq.actions :as actions] | |
| 10 | + [frq.cells :as cells] | |
| 9 | 11 | [jolt.host :as host] |
| 10 | 12 | [frq.atproto :as atproto] |
| 11 | 13 | [frq.av :as av] |
| @@ -20,8 +22,32 @@ | ||
| 20 | 22 | [frq.store :as store] |
| 21 | 23 | [frq.upload :as upload])) |
| 22 | 24 | |
| 23 | -(def default-host "irc.freeq.at") | |
| 24 | -(def default-port "6697") | |
| 25 | +(def default-host cells/default-host) | |
| 26 | +(def default-port cells/default-port) | |
| 27 | + | |
| 28 | +;; The cells the connect screen reads live in `frq.cells` now, so that screen | |
| 29 | +;; could move to common/ and be the same file on the phone. Re-defined here | |
| 30 | +;; rather than left to the callers: a thousand lines below this say | |
| 31 | +;; `@form-nick` and `@connecting?`, and none of them care which namespace the | |
| 32 | +;; atom was made in. | |
| 33 | +(def screen cells/screen) | |
| 34 | +;; Not in frq.cells: this holds the live IRC connection, which is jolt's | |
| 35 | +;; socket and a reader thread. The phone's equivalent is a dart:io Socket and | |
| 36 | +;; nothing shared could hold either. | |
| 37 | +(defonce conn (atom nil)) | |
| 38 | +(def status cells/status) | |
| 39 | +(def error cells/error) | |
| 40 | +(def connecting? cells/connecting?) | |
| 41 | +(def form-host cells/form-host) | |
| 42 | +(def form-port cells/form-port) | |
| 43 | +(def form-tls? cells/form-tls?) | |
| 44 | +(def form-nick cells/form-nick) | |
| 45 | +(def auth-mode cells/auth-mode) | |
| 46 | +(def form-handle cells/form-handle) | |
| 47 | +(def form-app-password cells/form-app-password) | |
| 48 | +(def session cells/session) | |
| 49 | +(def broker-token cells/broker-token) | |
| 50 | +(def login-url cells/login-url) | |
| 25 | 51 | |
| 26 | 52 | (def popular-channels |
| 27 | 53 | [["#general" "General discussion"] |
| @@ -31,31 +57,6 @@ | ||
| 31 | 57 | ["#music" "Music recommendations"] |
| 32 | 58 | ["#random" "Off-topic chat"]]) |
| 33 | 59 | |
| 34 | -;; screen: :connect | :chats | :chat | :discover | :settings | |
| 35 | -(defonce screen (atom :connect)) | |
| 36 | -(defonce conn (atom nil)) | |
| 37 | -(defonce status (atom "Not connected")) | |
| 38 | -(defonce error (atom nil)) | |
| 39 | -(defonce connecting? (atom false)) | |
| 40 | - | |
| 41 | -(defonce form-host (atom default-host)) | |
| 42 | -(defonce form-port (atom default-port)) | |
| 43 | -;; TLS is the default; untick it for a server's plain :6667 listener | |
| 44 | -(defonce form-tls? (atom true)) | |
| 45 | -(defonce form-nick (atom "frq-guest")) | |
| 46 | - | |
| 47 | -;; Bluesky sign-in. The app password reaches the user's own PDS and nothing | |
| 48 | -;; else: freeq is handed the session token it mints, and verifies that token by | |
| 49 | -;; asking the same PDS. It is never written to disk. | |
| 50 | -(defonce auth-mode (atom :guest)) ; :guest | :bluesky | :app-password | |
| 51 | -(defonce form-handle (atom "")) | |
| 52 | -(defonce form-app-password (atom "")) | |
| 53 | -(defonce session (atom nil)) ; a pds-session or a web-token one | |
| 54 | - | |
| 55 | -;; The durable half of an OAuth sign-in. The web-token beside it is single-use, | |
| 56 | -;; so a reconnect mints a fresh one from this rather than replaying the old. | |
| 57 | -(defonce broker-token (atom nil)) | |
| 58 | -(defonce login-url (atom nil)) ; shown while the browser is open | |
| 59 | 60 | |
| 60 | 61 | ;; joined as soon as the server sends 001 |
| 61 | 62 | (def auto-join "#test") |
| @@ -1928,3 +1929,11 @@ | ||
| 1928 | 1929 | (if-let [m (last (:messages buffer))] |
| 1929 | 1930 | (str (:from m) ": " (:text m)) |
| 1930 | 1931 | "No messages yet")) |
| 1932 | + | |
| 1933 | +;; What the shared connect screen calls. Installed here rather than in an | |
| 1934 | +;; entry point because these are this namespace's own reducers, and the screen | |
| 1935 | +;; that calls them is no longer in a position to name them. | |
| 1936 | +(actions/install! | |
| 1937 | + {:connect! connect! | |
| 1938 | + :disconnect! disconnect! | |
| 1939 | + :forget-session! forget-session!}) | |
| @@ -6,6 +6,8 @@ | |||
| 6 | is the only place a wire message turns into UI state." | 6 | is the only place a wire message turns into UI state." |
| 7 | (:require [clojure.string :as str] | 7 | (:require [clojure.string :as str] |
| 8 | [glimmer.ratom :as r :refer [atom]] | 8 | [glimmer.ratom :as r :refer [atom]] |
| 9 | + [frq.actions :as actions] | ||
| 10 | + [frq.cells :as cells] | ||
| 9 | [jolt.host :as host] | 11 | [jolt.host :as host] |
| 10 | [frq.atproto :as atproto] | 12 | [frq.atproto :as atproto] |
| 11 | [frq.av :as av] | 13 | [frq.av :as av] |
| @@ -20,8 +22,32 @@ | |||
| 20 | [frq.store :as store] | 22 | [frq.store :as store] |
| 21 | [frq.upload :as upload])) | 23 | [frq.upload :as upload])) |
| 22 | 24 | ||
| 23 | -(def default-host "irc.freeq.at") | 25 | +(def default-host cells/default-host) |
| 24 | -(def default-port "6697") | 26 | +(def default-port cells/default-port) |
| 27 | + | ||
| 28 | +;; The cells the connect screen reads live in `frq.cells` now, so that screen | ||
| 29 | +;; could move to common/ and be the same file on the phone. Re-defined here | ||
| 30 | +;; rather than left to the callers: a thousand lines below this say | ||
| 31 | +;; `@form-nick` and `@connecting?`, and none of them care which namespace the | ||
| 32 | +;; atom was made in. | ||
| 33 | +(def screen cells/screen) | ||
| 34 | +;; Not in frq.cells: this holds the live IRC connection, which is jolt's | ||
| 35 | +;; socket and a reader thread. The phone's equivalent is a dart:io Socket and | ||
| 36 | +;; nothing shared could hold either. | ||
| 37 | +(defonce conn (atom nil)) | ||
| 38 | +(def status cells/status) | ||
| 39 | +(def error cells/error) | ||
| 40 | +(def connecting? cells/connecting?) | ||
| 41 | +(def form-host cells/form-host) | ||
| 42 | +(def form-port cells/form-port) | ||
| 43 | +(def form-tls? cells/form-tls?) | ||
| 44 | +(def form-nick cells/form-nick) | ||
| 45 | +(def auth-mode cells/auth-mode) | ||
| 46 | +(def form-handle cells/form-handle) | ||
| 47 | +(def form-app-password cells/form-app-password) | ||
| 48 | +(def session cells/session) | ||
| 49 | +(def broker-token cells/broker-token) | ||
| 50 | +(def login-url cells/login-url) | ||
| 25 | 51 | ||
| 26 | (def popular-channels | 52 | (def popular-channels |
| 27 | [["#general" "General discussion"] | 53 | [["#general" "General discussion"] |
| @@ -31,31 +57,6 @@ | |||
| 31 | ["#music" "Music recommendations"] | 57 | ["#music" "Music recommendations"] |
| 32 | ["#random" "Off-topic chat"]]) | 58 | ["#random" "Off-topic chat"]]) |
| 33 | 59 | ||
| 34 | -;; screen: :connect | :chats | :chat | :discover | :settings | ||
| 35 | -(defonce screen (atom :connect)) | ||
| 36 | -(defonce conn (atom nil)) | ||
| 37 | -(defonce status (atom "Not connected")) | ||
| 38 | -(defonce error (atom nil)) | ||
| 39 | -(defonce connecting? (atom false)) | ||
| 40 | - | ||
| 41 | -(defonce form-host (atom default-host)) | ||
| 42 | -(defonce form-port (atom default-port)) | ||
| 43 | -;; TLS is the default; untick it for a server's plain :6667 listener | ||
| 44 | -(defonce form-tls? (atom true)) | ||
| 45 | -(defonce form-nick (atom "frq-guest")) | ||
| 46 | - | ||
| 47 | -;; Bluesky sign-in. The app password reaches the user's own PDS and nothing | ||
| 48 | -;; else: freeq is handed the session token it mints, and verifies that token by | ||
| 49 | -;; asking the same PDS. It is never written to disk. | ||
| 50 | -(defonce auth-mode (atom :guest)) ; :guest | :bluesky | :app-password | ||
| 51 | -(defonce form-handle (atom "")) | ||
| 52 | -(defonce form-app-password (atom "")) | ||
| 53 | -(defonce session (atom nil)) ; a pds-session or a web-token one | ||
| 54 | - | ||
| 55 | -;; The durable half of an OAuth sign-in. The web-token beside it is single-use, | ||
| 56 | -;; so a reconnect mints a fresh one from this rather than replaying the old. | ||
| 57 | -(defonce broker-token (atom nil)) | ||
| 58 | -(defonce login-url (atom nil)) ; shown while the browser is open | ||
| 59 | 60 | ||
| 60 | ;; joined as soon as the server sends 001 | 61 | ;; joined as soon as the server sends 001 |
| 61 | (def auto-join "#test") | 62 | (def auto-join "#test") |
| @@ -1928,3 +1929,11 @@ | |||
| 1928 | (if-let [m (last (:messages buffer))] | 1929 | (if-let [m (last (:messages buffer))] |
| 1929 | (str (:from m) ": " (:text m)) | 1930 | (str (:from m) ": " (:text m)) |
| 1930 | "No messages yet")) | 1931 | "No messages yet")) |
| 1932 | + | ||
| 1933 | +;; What the shared connect screen calls. Installed here rather than in an | ||
| 1934 | +;; entry point because these are this namespace's own reducers, and the screen | ||
| 1935 | +;; that calls them is no longer in a position to name them. | ||
| 1936 | +(actions/install! | ||
| 1937 | + {:connect! connect! | ||
| 1938 | + :disconnect! disconnect! | ||
| 1939 | + :forget-session! forget-session!}) | ||