Keep the Flutter room list between runs
The Flutter half registered with the Bluesky handle as its IRC nick while the sign-in had already put the derived *nick* in `form-nick`. So the channel called us alice.bsky.social and the client thought it was alice, and every "is this me?" test against a prefix came back false: our own JOIN echo stopped setting `:joined?`, every room wore a "not joined" badge, Open re-sent a JOIN for a room we were already in, and `mine?` never recognised our own lines. It dials with the nick now, as the desktop does. Under that was the missing half of the room list. `frq.store` already reads and writes rooms.edn and is already shared, but only `frq.state` ever called it — this side started every launch with an empty list, adopted whatever the server announced, and asked for #test on top. So: read the file at startup as empty buffers in their saved order, write it when the list changes, ask to be in what it says at 001, and land on the room the reader left rather than #test. With that the desktop's strictness can come across too — a JOIN for a room the file does not hold is the server inventing a membership, and we part it again, unless this is the first session and the file has yet to be told anything. Three more divergences the port exposed: - `:joining?`, because the saved list is asked for at 001 and the last room opened straight after, and a second JOIN in that window is what makes the server replay nothing. - Memberships now drop on a lost connection. Left set, they made a reconnect ask for nothing. - `:leave-channel!` PARTs channels only and clears `current`; the Join box goes through `open-room!`, so `@nick` opens a DM and a typed room exists before its own echo is judged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c945c1f parent: 479ec83 modified
flutter/src/frq/main.cljd +303 -42 | @@ -129,6 +129,198 @@ | ||
| 129 | 129 | (reset! cells/channels (:channels out)) |
| 130 | 130 | (:result out))) |
| 131 | 131 | |
| 132 | +;; ------------------------------------------------------------------- rooms | |
| 133 | +;; | |
| 134 | +;; `rooms.edn` is the authority for what rooms there are, and until now only | |
| 135 | +;; the desktop kept one: this half started every launch with an empty list, | |
| 136 | +;; adopted whatever the server announced, and asked for `#test` on top. Which | |
| 137 | +;; looked like the room list forgetting itself between runs, because it was. | |
| 138 | +;; | |
| 139 | +;; The file and its format are `frq.store`, which is already shared — the port | |
| 140 | +;; is the half that was missing here: read it at startup, write it when the | |
| 141 | +;; list changes, and ask to be in what it says at 001. | |
| 142 | + | |
| 143 | +(def ^:private auto-join | |
| 144 | + "The room a client with nothing saved lands in, as on the desktop." | |
| 145 | + "#test") | |
| 146 | + | |
| 147 | +;; Whether `rooms.edn` is the authority yet. | |
| 148 | +;; | |
| 149 | +;; It is not, the first time this version runs: freeq re-joins an | |
| 150 | +;; authenticated user's channels at registration, so on connect the server | |
| 151 | +;; announces every room it has you in — and a client that parted everything | |
| 152 | +;; not already in its file would walk out of all of them before the file had | |
| 153 | +;; ever been told they existed. So the first connect adopts what the server | |
| 154 | +;; says and writes it down, and every connect after that is the strict one. | |
| 155 | +(defonce ^:private room-list-owned? (atom false)) | |
| 156 | + | |
| 157 | +;; And whether this session is the adopting one, decided at 001. | |
| 158 | +(defonce ^:private adopting-rooms? (atom false)) | |
| 159 | + | |
| 160 | +;; What the record on disk says that this client has no cell for: the read | |
| 161 | +;; markers, mostly. Flutter has no unread machinery yet, so writing only the | |
| 162 | +;; fields it knows would quietly drop the marker a desktop run had put there | |
| 163 | +;; and hand the reader their whole history as new. Read once, merged back | |
| 164 | +;; under whatever we do know. | |
| 165 | +(defonce ^:private room-extras (atom {})) | |
| 166 | + | |
| 167 | +(defonce ^:private rooms-saved-at (atom 0)) | |
| 168 | + | |
| 169 | +(defn- save-prefs! | |
| 170 | + "Keep the one flag that has to outlive the run: whether this client has | |
| 171 | + taken the room list over from the server. Merged into whatever else | |
| 172 | + `prefs.edn` holds — the display toggles are the desktop's to write, and a | |
| 173 | + whole-map write from here would drop them." | |
| 174 | + [] | |
| 175 | + (store/save-prefs! (assoc (store/load-prefs) | |
| 176 | + :room-list-owned? @room-list-owned?)) | |
| 177 | + nil) | |
| 178 | + | |
| 179 | +(defn- restore-prefs! [] | |
| 180 | + (reset! room-list-owned? (boolean (:room-list-owned? (store/load-prefs)))) | |
| 181 | + nil) | |
| 182 | + | |
| 183 | +(defn- room-records | |
| 184 | + "The rooms as they go on disk: most recently used first, each carrying | |
| 185 | + whatever the file already said about it that this client does not hold." | |
| 186 | + [] | |
| 187 | + (->> (vals @cells/channels) | |
| 188 | + (sort-by #(- (:accessed % 0))) | |
| 189 | + (mapv (fn [b] | |
| 190 | + (let [name (:name b)] | |
| 191 | + (merge (get @room-extras name) | |
| 192 | + {:name name | |
| 193 | + :kind (or (:kind b) | |
| 194 | + (if (rooms/dm? name) :dm :channel))})))))) | |
| 195 | + | |
| 196 | +(defn- remember-rooms! | |
| 197 | + "Write the room list out. Throttled, because a room's `:accessed` moves | |
| 198 | + every time one is opened and the write is synchronous here — `force?` is for | |
| 199 | + the moments worth paying for, which is the list itself changing. | |
| 200 | + | |
| 201 | + A late write costs at most the rooms touched since the last one, which come | |
| 202 | + back in a slightly stale order. Losing one entirely is the thing to avoid, | |
| 203 | + which is what `force?` is for." | |
| 204 | + ([] (remember-rooms! false)) | |
| 205 | + ([force?] | |
| 206 | + (let [now (clock/now-ms)] | |
| 207 | + (when (or force? (> (- now @rooms-saved-at) 5000)) | |
| 208 | + (reset! rooms-saved-at now) | |
| 209 | + (store/save-rooms! (room-records))) | |
| 210 | + nil))) | |
| 211 | + | |
| 212 | +(defn- restore-rooms! | |
| 213 | + "Bring back the rooms of earlier runs, in the order they were last used. | |
| 214 | + | |
| 215 | + Empty buffers, not memberships: opening one is what joins it, and the list | |
| 216 | + is the part worth keeping — the messages in them come from the server, which | |
| 217 | + replays them on the CHATHISTORY this client asks for at 366. | |
| 218 | + | |
| 219 | + `:accessed` is an ascending count over the file read backwards, so the saved | |
| 220 | + order survives and a room opened *this* run — stamped with the wall clock — | |
| 221 | + still sorts above every one of them." | |
| 222 | + [] | |
| 223 | + (when-let [saved (seq (store/load-rooms))] | |
| 224 | + (reset! room-extras | |
| 225 | + (into {} (map (fn [r] [(:name r) (dissoc r :name :kind)]) saved))) | |
| 226 | + (swap! cells/channels | |
| 227 | + (fn [m] | |
| 228 | + (reduce (fn [acc [i room]] | |
| 229 | + (let [name (:name room)] | |
| 230 | + (if (contains? acc name) | |
| 231 | + acc | |
| 232 | + (assoc acc name | |
| 233 | + {:name name | |
| 234 | + :messages [] | |
| 235 | + :unread 0 | |
| 236 | + :joined? false | |
| 237 | + :users {} | |
| 238 | + :kind (or (:kind room) | |
| 239 | + (if (rooms/dm? name) :dm :channel)) | |
| 240 | + :accessed (inc i)})))) | |
| 241 | + m | |
| 242 | + ;; Oldest first, so the count ascends with the order. | |
| 243 | + (map-indexed vector (reverse saved))))) | |
| 244 | + (count saved))) | |
| 245 | + | |
| 246 | +(defn- ask-to-join! | |
| 247 | + "Send the JOIN, once. `:joining?` as well as `:joined?`, because the echo | |
| 248 | + takes a round trip: a second JOIN sent in the meantime is what makes the | |
| 249 | + server replay nothing, and with the saved list asked for at 001 and the last | |
| 250 | + room opened straight after, that window is every launch." | |
| 251 | + [name] | |
| 252 | + (when-let [c @conn] | |
| 253 | + (when (and (.startsWith (str name) "#") | |
| 254 | + (let [b (get @cells/channels name)] | |
| 255 | + (and (not (:joined? b)) (not (:joining? b))))) | |
| 256 | + (swap! cells/channels assoc-in [name :joining?] true) | |
| 257 | + (net/send-line! c (str "JOIN " name)))) | |
| 258 | + nil) | |
| 259 | + | |
| 260 | +(defn- open-room! | |
| 261 | + "Show a buffer, joining it on the way. A row can outlive the membership | |
| 262 | + behind it — a disconnect drops every channel and the buffer stays — so | |
| 263 | + opening one is a request to be in it. | |
| 264 | + | |
| 265 | + Ensuring the buffer *before* the JOIN is what keeps the strict rule in the | |
| 266 | + JOIN handler from throwing the room straight back out: a channel we asked | |
| 267 | + for is one the list already holds by the time the echo lands." | |
| 268 | + [name] | |
| 269 | + (reset! cells/current name) | |
| 270 | + ;; An edit belongs to a line in the buffer being left: carried across, the | |
| 271 | + ;; next Send would rewrite a message nobody in this room can see. The box | |
| 272 | + ;; empties with it, because what is in it is a copy of that line — | |
| 273 | + ;; `frq.state/open-channel!` does this too. | |
| 274 | + (when (and @cells/editing (not= name (:channel @cells/editing))) | |
| 275 | + (reset! cells/editing nil) | |
| 276 | + (reset! cells/draft "")) | |
| 277 | + (swap! cells/channels update name | |
| 278 | + #(merge {:name name :messages [] :unread 0 | |
| 279 | + :kind (if (rooms/dm? name) :dm :channel)} | |
| 280 | + %)) | |
| 281 | + ;; The wall clock, so a room opened this run sorts above every one | |
| 282 | + ;; `restore-rooms!` counted in. | |
| 283 | + (swap! cells/channels assoc-in [name :accessed] (clock/now-ms)) | |
| 284 | + (reset! cells/screen :chat) | |
| 285 | + ;; Worth a write of its own: the order the list is read in is the order | |
| 286 | + ;; rooms were last opened, and this is the moment it changes. | |
| 287 | + (remember-rooms! true) | |
| 288 | + (ask-to-join! name) | |
| 289 | + nil) | |
| 290 | + | |
| 291 | +(defn- join-saved-rooms! | |
| 292 | + "Ask to be in every channel the file says we are in. | |
| 293 | + | |
| 294 | + The server re-joins an authenticated user's channels itself and gets it | |
| 295 | + wrong in both directions — it forgets rooms and announces ones that are not | |
| 296 | + ours. This is the half that answers the forgetting. A JOIN for a channel the | |
| 297 | + server has already put us in is answered with the membership we already | |
| 298 | + have, so asking twice costs nothing. | |
| 299 | + | |
| 300 | + DMs are not asked for: there is nothing to join in a conversation with a | |
| 301 | + person, the buffer is the whole of it." | |
| 302 | + [] | |
| 303 | + (when @conn | |
| 304 | + (doseq [[name _] @cells/channels] (ask-to-join! name)) | |
| 305 | + nil)) | |
| 306 | + | |
| 307 | +(defn- forget-memberships! | |
| 308 | + "The buffers survive a lost connection, the memberships do not. | |
| 309 | + | |
| 310 | + Leaving `:joined?` set across a drop is what made a reconnect ask for | |
| 311 | + nothing: `ask-to-join!` reads it, every room still claimed to be joined, and | |
| 312 | + the list came back full of rooms we were no longer in. The users go with it — | |
| 313 | + the roll is the server's and it will send a fresh one with the NAMES that | |
| 314 | + follow the re-JOIN." | |
| 315 | + [] | |
| 316 | + (swap! cells/channels | |
| 317 | + (fn [m] | |
| 318 | + (reduce-kv (fn [acc k v] | |
| 319 | + (assoc acc k (assoc v :joined? false :joining? false | |
| 320 | + :users {}))) | |
| 321 | + {} m))) | |
| 322 | + nil) | |
| 323 | + | |
| 132 | 324 | (def ^:private history-limit |
| 133 | 325 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." |
| 134 | 326 | 100) |
| @@ -150,14 +342,32 @@ | ||
| 150 | 342 | (cond |
| 151 | 343 | (= "JOIN" cmd) |
| 152 | 344 | (let [name (first params)] |
| 153 | - (swap! cells/channels | |
| 154 | - #(-> (ensure % name) | |
| 155 | - (members/add-user name who) | |
| 156 | - (cond-> me? (update name merge {:joined? true | |
| 157 | - :accessed (clock/now-ms)})))) | |
| 158 | - ;; Only our own JOIN opens the room. Someone else arriving in a | |
| 159 | - ;; channel used to move the reader into it. | |
| 160 | - (when me? (reset! cells/current name))) | |
| 345 | + (if me? | |
| 346 | + ;; Ours if the file says so — restored at startup, or asked for | |
| 347 | + ;; since. Anything else is the server putting us somewhere we did | |
| 348 | + ;; not ask to be, which it does: it announces memberships that are | |
| 349 | + ;; not real, and adding them is how a list nobody chose fills up. | |
| 350 | + ;; | |
| 351 | + ;; So we leave again, unless this is the session that is still | |
| 352 | + ;; adopting — on the first connect the file has not been told | |
| 353 | + ;; anything yet, and parting then would be leaving every room we are | |
| 354 | + ;; actually in. | |
| 355 | + (if (and (not (contains? @cells/channels name)) | |
| 356 | + (not @adopting-rooms?)) | |
| 357 | + (when-let [c @conn] (net/send-line! c (str "PART " name))) | |
| 358 | + (do (swap! cells/channels | |
| 359 | + #(-> (ensure % name) | |
| 360 | + (members/add-user name who) | |
| 361 | + (update name merge {:joined? true | |
| 362 | + :joining? false}))) | |
| 363 | + ;; Adopted or asked for, it is ours now and the file should | |
| 364 | + ;; say so before the next connect judges it. | |
| 365 | + (remember-rooms! true))) | |
| 366 | + ;; Somebody else arriving in a room we do not hold is not a reason | |
| 367 | + ;; to start holding it: `add-user` builds the buffer it is given, | |
| 368 | + ;; which would put a refused room straight back in the list. | |
| 369 | + (when (contains? @cells/channels name) | |
| 370 | + (swap! cells/channels members/add-user name who)))) | |
| 161 | 371 | |
| 162 | 372 | ;; 353 is the roll, in as many lines as it takes; 366 ends it. |
| 163 | 373 | (= "353" cmd) |
| @@ -280,7 +490,12 @@ | ||
| 280 | 490 | add (swap! cells/channels reactions/update-reaction |
| 281 | 491 | buffer msgid add who true) |
| 282 | 492 | gone (swap! cells/channels reactions/update-reaction |
| 283 | - buffer msgid gone who false)))))) | |
| 493 | + buffer msgid gone who false)))) | |
| 494 | + ;; Whatever that message did to the list, write it down. Throttled, so a | |
| 495 | + ;; busy channel does not cost a file write per line — but a DM arriving | |
| 496 | + ;; from someone new is a room that exists only here until this runs, and | |
| 497 | + ;; the old client lost exactly those between runs. | |
| 498 | + (remember-rooms!))) | |
| 284 | 499 | |
| 285 | 500 | (defn- note! [m] |
| 286 | 501 | (reset! last-line (str (:command m) " " (last (:params m)))) |
| @@ -492,8 +707,37 @@ | ||
| 492 | 707 | ;; and looks exactly like a |
| 493 | 708 | ;; failure. |
| 494 | 709 | (reset! cells/screen :chats) |
| 495 | - (when-let [c @conn] | |
| 496 | - (net/send-line! c "JOIN #test"))) | |
| 710 | + ;; This session decides once | |
| 711 | + ;; whether it is the one that | |
| 712 | + ;; takes the room list over | |
| 713 | + ;; from the server. Before the | |
| 714 | + ;; flag is set the file has | |
| 715 | + ;; never been told what we are | |
| 716 | + ;; in, so the server's answer | |
| 717 | + ;; is adopted rather than | |
| 718 | + ;; argued with. | |
| 719 | + (reset! adopting-rooms? | |
| 720 | + (not @room-list-owned?)) | |
| 721 | + (when-not @room-list-owned? | |
| 722 | + (reset! room-list-owned? true) | |
| 723 | + (save-prefs!)) | |
| 724 | + ;; What the file says we are in, | |
| 725 | + ;; we ask to be in. A room the | |
| 726 | + ;; server has forgotten is one | |
| 727 | + ;; that would otherwise quietly | |
| 728 | + ;; stop existing. | |
| 729 | + (join-saved-rooms!) | |
| 730 | + ;; And back where the reader | |
| 731 | + ;; left off, rather than into | |
| 732 | + ;; #test on top of a list they | |
| 733 | + ;; already had. The chats | |
| 734 | + ;; screen stays underneath, so | |
| 735 | + ;; Back goes to the list rather | |
| 736 | + ;; than out of the app. | |
| 737 | + (if-let [last-room | |
| 738 | + (:name (first (rooms/channel-list)))] | |
| 739 | + (actions/open-channel! last-room) | |
| 740 | + (actions/join! auto-join))) | |
| 497 | 741 | |
| 498 | 742 | (contains? registration-failed cmd) |
| 499 | 743 | (fail! (if (= "433" cmd) |
| @@ -507,6 +751,7 @@ | ||
| 507 | 751 | (reset! cells/status (str "… " cmd))))) |
| 508 | 752 | :on-close (fn [why] |
| 509 | 753 | (reset! conn nil) |
| 754 | + (forget-memberships!) | |
| 510 | 755 | ;; Say what the last thing seen was. A |
| 511 | 756 | ;; connection that registers and then |
| 512 | 757 | ;; drops is a different bug from one |
| @@ -521,7 +766,16 @@ | ||
| 521 | 766 | ;; CAP first, then registration — the order the server expects, and the |
| 522 | 767 | ;; order `frq.irc` uses. |
| 523 | 768 | (net/send-line! sock "CAP LS 302") |
| 524 | - (let [nick (if-let [h (:handle @cells/session)] h @cells/form-nick)] | |
| 769 | + ;; `form-nick` and nothing else. The handle went on the wire here, | |
| 770 | + ;; and the sign-in had already put the *nick* in the cell — so the | |
| 771 | + ;; channel called us alice.bsky.social while the client thought it | |
| 772 | + ;; was alice, and every test of "is this me?" against a prefix came | |
| 773 | + ;; back false. Our own JOIN echo stopped setting `:joined?`, so | |
| 774 | + ;; every room wore a "not joined" badge and Open re-sent a JOIN for | |
| 775 | + ;; a room we were already in. The DID is the identity; the nick is | |
| 776 | + ;; only what the channel calls us, and both halves have to agree on | |
| 777 | + ;; it. Same nick the desktop dials with. | |
| 778 | + (let [nick (str @cells/form-nick)] | |
| 525 | 779 | (net/send-line! sock (str "NICK " nick)) |
| 526 | 780 | (net/send-line! sock (str "USER " nick " 0 * :frq")))) |
| 527 | 781 | ;; Object, not Exception. Dart keeps Error and Exception in separate |
| @@ -534,6 +788,7 @@ | ||
| 534 | 788 | (reset! closing? true) |
| 535 | 789 | (when-let [c @conn] (net/close! c)) |
| 536 | 790 | (reset! conn nil) |
| 791 | + (forget-memberships!) | |
| 537 | 792 | (reset! cells/connecting? false) |
| 538 | 793 | (reset! cells/status "Not connected") |
| 539 | 794 | (reset! cells/screen :connect)) |
| @@ -771,6 +1026,11 @@ | ||
| 771 | 1026 | ;; Before any widget is built: a cell that changes before its watch is on |
| 772 | 1027 | ;; is a change the screen never hears about. |
| 773 | 1028 | (watch-cells!) |
| 1029 | + ;; The room list of earlier runs, and the flag that says whether it is the | |
| 1030 | + ;; authority yet. Before the saved session, because that connects on its | |
| 1031 | + ;; own the moment it is restored and 001 reads both of these. | |
| 1032 | + (restore-prefs!) | |
| 1033 | + (restore-rooms!) | |
| 774 | 1034 | ;; A sign-in that already happened. Only the durable broker token comes |
| 775 | 1035 | ;; back — the connection mints a fresh web-token from it — so this is not |
| 776 | 1036 | ;; a session, it is the means to ask for one. |
| @@ -800,13 +1060,38 @@ | ||
| 800 | 1060 | {:connect! connect! |
| 801 | 1061 | :disconnect! disconnect! |
| 802 | 1062 | :connected? (fn [] (some? @conn)) |
| 1063 | + ;; The Join box, and the same one box for both things the desktop's | |
| 1064 | + ;; `join!` does: `@nick` opens a DM, anything else is a channel and | |
| 1065 | + ;; gets its `#` if it came without one. Through `open-room!` rather | |
| 1066 | + ;; than straight onto the wire, because a bare JOIN left the buffer | |
| 1067 | + ;; unbuilt — and the rule in the JOIN handler would then read our own | |
| 1068 | + ;; echo as a room the server had invented and part it again. | |
| 803 | 1069 | :join! (fn [name] |
| 804 | - (when-let [c @conn] | |
| 805 | - (when (seq (str name)) | |
| 806 | - (net/send-line! c (str "JOIN " name))))) | |
| 1070 | + (let [name (.trim (str name))] | |
| 1071 | + (cond | |
| 1072 | + (.startsWith name "@") | |
| 1073 | + (let [who (.substring name 1)] | |
| 1074 | + (when (seq who) (open-room! who))) | |
| 1075 | + | |
| 1076 | + (seq name) | |
| 1077 | + (open-room! (if (.startsWith name "#") name (str "#" name))) | |
| 1078 | + | |
| 1079 | + :else nil))) | |
| 807 | 1080 | :leave-channel! (fn [name] |
| 808 | - (when-let [c @conn] (net/send-line! c (str "PART " name))) | |
| 809 | - (swap! cells/channels dissoc name)) | |
| 1081 | + ;; Channels only, as on the desktop. There is nothing | |
| 1082 | + ;; to part in a conversation with a person — the | |
| 1083 | + ;; buffer is the whole of it — and `PART alice` is a | |
| 1084 | + ;; line the server answers with an error. | |
| 1085 | + (when (and @conn (.startsWith (str name) "#")) | |
| 1086 | + (net/send-line! @conn (str "PART " name))) | |
| 1087 | + (swap! cells/channels dissoc name) | |
| 1088 | + (when (= name @cells/current) | |
| 1089 | + (reset! cells/current nil) | |
| 1090 | + (reset! cells/screen :chats)) | |
| 1091 | + ;; The only way a room leaves the file. Everything | |
| 1092 | + ;; else adds one, so without this the list is a thing | |
| 1093 | + ;; that only grows. | |
| 1094 | + (remember-rooms! true)) | |
| 810 | 1095 | :send-draft! send-draft! |
| 811 | 1096 | |
| 812 | 1097 | ;; Pictures. The chooser is the platform's, so the browsing screen the |
| @@ -953,31 +1238,7 @@ | ||
| 953 | 1238 | (reset! cells/broker-token nil) |
| 954 | 1239 | (reset! cells/session nil)) |
| 955 | 1240 | :jump-to-present! (fn [] (reset! cells/at-present? true)) |
| 956 | - :open-channel! (fn [name] | |
| 957 | - (reset! cells/current name) | |
| 958 | - ;; An edit belongs to a line in the buffer being left: | |
| 959 | - ;; carried across, the next Send would rewrite a | |
| 960 | - ;; message nobody in this room can see. The box empties | |
| 961 | - ;; with it, because what is in it is a copy of that | |
| 962 | - ;; line — `frq.state/open-channel!` does this too. | |
| 963 | - (when (and @cells/editing | |
| 964 | - (not= name (:channel @cells/editing))) | |
| 965 | - (reset! cells/editing nil) | |
| 966 | - (reset! cells/draft "")) | |
| 967 | - ;; `frq.state/open-channel!` does this and more: it | |
| 968 | - ;; marks the buffer read, remembers the room list and | |
| 969 | - ;; asks for NAMES. What it also does, and what made | |
| 970 | - ;; Open look like a no-op here, is go to the | |
| 971 | - ;; conversation — and join it on the way, because a row | |
| 972 | - ;; can outlive the membership behind it. | |
| 973 | - (swap! cells/channels update name | |
| 974 | - #(merge {:name name :messages [] :unread 0} %)) | |
| 975 | - (reset! cells/screen :chat) | |
| 976 | - (let [buffer (get @cells/channels name)] | |
| 977 | - (when (and @conn | |
| 978 | - (.startsWith (str name) "#") | |
| 979 | - (not (:joined? buffer))) | |
| 980 | - (net/send-line! @conn (str "JOIN " name)))))}) | |
| 1241 | + :open-channel! open-room!}) | |
| 981 | 1242 | (f/run |
| 982 | 1243 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 983 | 1244 | .home |
| @@ -129,6 +129,198 @@ | |||
| 129 | (reset! cells/channels (:channels out)) | 129 | (reset! cells/channels (:channels out)) |
| 130 | (:result out))) | 130 | (:result out))) |
| 131 | 131 | ||
| 132 | +;; ------------------------------------------------------------------- rooms | ||
| 133 | +;; | ||
| 134 | +;; `rooms.edn` is the authority for what rooms there are, and until now only | ||
| 135 | +;; the desktop kept one: this half started every launch with an empty list, | ||
| 136 | +;; adopted whatever the server announced, and asked for `#test` on top. Which | ||
| 137 | +;; looked like the room list forgetting itself between runs, because it was. | ||
| 138 | +;; | ||
| 139 | +;; The file and its format are `frq.store`, which is already shared — the port | ||
| 140 | +;; is the half that was missing here: read it at startup, write it when the | ||
| 141 | +;; list changes, and ask to be in what it says at 001. | ||
| 142 | + | ||
| 143 | +(def ^:private auto-join | ||
| 144 | + "The room a client with nothing saved lands in, as on the desktop." | ||
| 145 | + "#test") | ||
| 146 | + | ||
| 147 | +;; Whether `rooms.edn` is the authority yet. | ||
| 148 | +;; | ||
| 149 | +;; It is not, the first time this version runs: freeq re-joins an | ||
| 150 | +;; authenticated user's channels at registration, so on connect the server | ||
| 151 | +;; announces every room it has you in — and a client that parted everything | ||
| 152 | +;; not already in its file would walk out of all of them before the file had | ||
| 153 | +;; ever been told they existed. So the first connect adopts what the server | ||
| 154 | +;; says and writes it down, and every connect after that is the strict one. | ||
| 155 | +(defonce ^:private room-list-owned? (atom false)) | ||
| 156 | + | ||
| 157 | +;; And whether this session is the adopting one, decided at 001. | ||
| 158 | +(defonce ^:private adopting-rooms? (atom false)) | ||
| 159 | + | ||
| 160 | +;; What the record on disk says that this client has no cell for: the read | ||
| 161 | +;; markers, mostly. Flutter has no unread machinery yet, so writing only the | ||
| 162 | +;; fields it knows would quietly drop the marker a desktop run had put there | ||
| 163 | +;; and hand the reader their whole history as new. Read once, merged back | ||
| 164 | +;; under whatever we do know. | ||
| 165 | +(defonce ^:private room-extras (atom {})) | ||
| 166 | + | ||
| 167 | +(defonce ^:private rooms-saved-at (atom 0)) | ||
| 168 | + | ||
| 169 | +(defn- save-prefs! | ||
| 170 | + "Keep the one flag that has to outlive the run: whether this client has | ||
| 171 | + taken the room list over from the server. Merged into whatever else | ||
| 172 | + `prefs.edn` holds — the display toggles are the desktop's to write, and a | ||
| 173 | + whole-map write from here would drop them." | ||
| 174 | + [] | ||
| 175 | + (store/save-prefs! (assoc (store/load-prefs) | ||
| 176 | + :room-list-owned? @room-list-owned?)) | ||
| 177 | + nil) | ||
| 178 | + | ||
| 179 | +(defn- restore-prefs! [] | ||
| 180 | + (reset! room-list-owned? (boolean (:room-list-owned? (store/load-prefs)))) | ||
| 181 | + nil) | ||
| 182 | + | ||
| 183 | +(defn- room-records | ||
| 184 | + "The rooms as they go on disk: most recently used first, each carrying | ||
| 185 | + whatever the file already said about it that this client does not hold." | ||
| 186 | + [] | ||
| 187 | + (->> (vals @cells/channels) | ||
| 188 | + (sort-by #(- (:accessed % 0))) | ||
| 189 | + (mapv (fn [b] | ||
| 190 | + (let [name (:name b)] | ||
| 191 | + (merge (get @room-extras name) | ||
| 192 | + {:name name | ||
| 193 | + :kind (or (:kind b) | ||
| 194 | + (if (rooms/dm? name) :dm :channel))})))))) | ||
| 195 | + | ||
| 196 | +(defn- remember-rooms! | ||
| 197 | + "Write the room list out. Throttled, because a room's `:accessed` moves | ||
| 198 | + every time one is opened and the write is synchronous here — `force?` is for | ||
| 199 | + the moments worth paying for, which is the list itself changing. | ||
| 200 | + | ||
| 201 | + A late write costs at most the rooms touched since the last one, which come | ||
| 202 | + back in a slightly stale order. Losing one entirely is the thing to avoid, | ||
| 203 | + which is what `force?` is for." | ||
| 204 | + ([] (remember-rooms! false)) | ||
| 205 | + ([force?] | ||
| 206 | + (let [now (clock/now-ms)] | ||
| 207 | + (when (or force? (> (- now @rooms-saved-at) 5000)) | ||
| 208 | + (reset! rooms-saved-at now) | ||
| 209 | + (store/save-rooms! (room-records))) | ||
| 210 | + nil))) | ||
| 211 | + | ||
| 212 | +(defn- restore-rooms! | ||
| 213 | + "Bring back the rooms of earlier runs, in the order they were last used. | ||
| 214 | + | ||
| 215 | + Empty buffers, not memberships: opening one is what joins it, and the list | ||
| 216 | + is the part worth keeping — the messages in them come from the server, which | ||
| 217 | + replays them on the CHATHISTORY this client asks for at 366. | ||
| 218 | + | ||
| 219 | + `:accessed` is an ascending count over the file read backwards, so the saved | ||
| 220 | + order survives and a room opened *this* run — stamped with the wall clock — | ||
| 221 | + still sorts above every one of them." | ||
| 222 | + [] | ||
| 223 | + (when-let [saved (seq (store/load-rooms))] | ||
| 224 | + (reset! room-extras | ||
| 225 | + (into {} (map (fn [r] [(:name r) (dissoc r :name :kind)]) saved))) | ||
| 226 | + (swap! cells/channels | ||
| 227 | + (fn [m] | ||
| 228 | + (reduce (fn [acc [i room]] | ||
| 229 | + (let [name (:name room)] | ||
| 230 | + (if (contains? acc name) | ||
| 231 | + acc | ||
| 232 | + (assoc acc name | ||
| 233 | + {:name name | ||
| 234 | + :messages [] | ||
| 235 | + :unread 0 | ||
| 236 | + :joined? false | ||
| 237 | + :users {} | ||
| 238 | + :kind (or (:kind room) | ||
| 239 | + (if (rooms/dm? name) :dm :channel)) | ||
| 240 | + :accessed (inc i)})))) | ||
| 241 | + m | ||
| 242 | + ;; Oldest first, so the count ascends with the order. | ||
| 243 | + (map-indexed vector (reverse saved))))) | ||
| 244 | + (count saved))) | ||
| 245 | + | ||
| 246 | +(defn- ask-to-join! | ||
| 247 | + "Send the JOIN, once. `:joining?` as well as `:joined?`, because the echo | ||
| 248 | + takes a round trip: a second JOIN sent in the meantime is what makes the | ||
| 249 | + server replay nothing, and with the saved list asked for at 001 and the last | ||
| 250 | + room opened straight after, that window is every launch." | ||
| 251 | + [name] | ||
| 252 | + (when-let [c @conn] | ||
| 253 | + (when (and (.startsWith (str name) "#") | ||
| 254 | + (let [b (get @cells/channels name)] | ||
| 255 | + (and (not (:joined? b)) (not (:joining? b))))) | ||
| 256 | + (swap! cells/channels assoc-in [name :joining?] true) | ||
| 257 | + (net/send-line! c (str "JOIN " name)))) | ||
| 258 | + nil) | ||
| 259 | + | ||
| 260 | +(defn- open-room! | ||
| 261 | + "Show a buffer, joining it on the way. A row can outlive the membership | ||
| 262 | + behind it — a disconnect drops every channel and the buffer stays — so | ||
| 263 | + opening one is a request to be in it. | ||
| 264 | + | ||
| 265 | + Ensuring the buffer *before* the JOIN is what keeps the strict rule in the | ||
| 266 | + JOIN handler from throwing the room straight back out: a channel we asked | ||
| 267 | + for is one the list already holds by the time the echo lands." | ||
| 268 | + [name] | ||
| 269 | + (reset! cells/current name) | ||
| 270 | + ;; An edit belongs to a line in the buffer being left: carried across, the | ||
| 271 | + ;; next Send would rewrite a message nobody in this room can see. The box | ||
| 272 | + ;; empties with it, because what is in it is a copy of that line — | ||
| 273 | + ;; `frq.state/open-channel!` does this too. | ||
| 274 | + (when (and @cells/editing (not= name (:channel @cells/editing))) | ||
| 275 | + (reset! cells/editing nil) | ||
| 276 | + (reset! cells/draft "")) | ||
| 277 | + (swap! cells/channels update name | ||
| 278 | + #(merge {:name name :messages [] :unread 0 | ||
| 279 | + :kind (if (rooms/dm? name) :dm :channel)} | ||
| 280 | + %)) | ||
| 281 | + ;; The wall clock, so a room opened this run sorts above every one | ||
| 282 | + ;; `restore-rooms!` counted in. | ||
| 283 | + (swap! cells/channels assoc-in [name :accessed] (clock/now-ms)) | ||
| 284 | + (reset! cells/screen :chat) | ||
| 285 | + ;; Worth a write of its own: the order the list is read in is the order | ||
| 286 | + ;; rooms were last opened, and this is the moment it changes. | ||
| 287 | + (remember-rooms! true) | ||
| 288 | + (ask-to-join! name) | ||
| 289 | + nil) | ||
| 290 | + | ||
| 291 | +(defn- join-saved-rooms! | ||
| 292 | + "Ask to be in every channel the file says we are in. | ||
| 293 | + | ||
| 294 | + The server re-joins an authenticated user's channels itself and gets it | ||
| 295 | + wrong in both directions — it forgets rooms and announces ones that are not | ||
| 296 | + ours. This is the half that answers the forgetting. A JOIN for a channel the | ||
| 297 | + server has already put us in is answered with the membership we already | ||
| 298 | + have, so asking twice costs nothing. | ||
| 299 | + | ||
| 300 | + DMs are not asked for: there is nothing to join in a conversation with a | ||
| 301 | + person, the buffer is the whole of it." | ||
| 302 | + [] | ||
| 303 | + (when @conn | ||
| 304 | + (doseq [[name _] @cells/channels] (ask-to-join! name)) | ||
| 305 | + nil)) | ||
| 306 | + | ||
| 307 | +(defn- forget-memberships! | ||
| 308 | + "The buffers survive a lost connection, the memberships do not. | ||
| 309 | + | ||
| 310 | + Leaving `:joined?` set across a drop is what made a reconnect ask for | ||
| 311 | + nothing: `ask-to-join!` reads it, every room still claimed to be joined, and | ||
| 312 | + the list came back full of rooms we were no longer in. The users go with it — | ||
| 313 | + the roll is the server's and it will send a fresh one with the NAMES that | ||
| 314 | + follow the re-JOIN." | ||
| 315 | + [] | ||
| 316 | + (swap! cells/channels | ||
| 317 | + (fn [m] | ||
| 318 | + (reduce-kv (fn [acc k v] | ||
| 319 | + (assoc acc k (assoc v :joined? false :joining? false | ||
| 320 | + :users {}))) | ||
| 321 | + {} m))) | ||
| 322 | + nil) | ||
| 323 | + | ||
| 132 | (def ^:private history-limit | 324 | (def ^:private history-limit |
| 133 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." | 325 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." |
| 134 | 100) | 326 | 100) |
| @@ -150,14 +342,32 @@ | |||
| 150 | (cond | 342 | (cond |
| 151 | (= "JOIN" cmd) | 343 | (= "JOIN" cmd) |
| 152 | (let [name (first params)] | 344 | (let [name (first params)] |
| 153 | - (swap! cells/channels | 345 | + (if me? |
| 154 | - #(-> (ensure % name) | 346 | + ;; Ours if the file says so — restored at startup, or asked for |
| 155 | - (members/add-user name who) | 347 | + ;; since. Anything else is the server putting us somewhere we did |
| 156 | - (cond-> me? (update name merge {:joined? true | 348 | + ;; not ask to be, which it does: it announces memberships that are |
| 157 | - :accessed (clock/now-ms)})))) | 349 | + ;; not real, and adding them is how a list nobody chose fills up. |
| 158 | - ;; Only our own JOIN opens the room. Someone else arriving in a | 350 | + ;; |
| 159 | - ;; channel used to move the reader into it. | 351 | + ;; So we leave again, unless this is the session that is still |
| 160 | - (when me? (reset! cells/current name))) | 352 | + ;; adopting — on the first connect the file has not been told |
| 353 | + ;; anything yet, and parting then would be leaving every room we are | ||
| 354 | + ;; actually in. | ||
| 355 | + (if (and (not (contains? @cells/channels name)) | ||
| 356 | + (not @adopting-rooms?)) | ||
| 357 | + (when-let [c @conn] (net/send-line! c (str "PART " name))) | ||
| 358 | + (do (swap! cells/channels | ||
| 359 | + #(-> (ensure % name) | ||
| 360 | + (members/add-user name who) | ||
| 361 | + (update name merge {:joined? true | ||
| 362 | + :joining? false}))) | ||
| 363 | + ;; Adopted or asked for, it is ours now and the file should | ||
| 364 | + ;; say so before the next connect judges it. | ||
| 365 | + (remember-rooms! true))) | ||
| 366 | + ;; Somebody else arriving in a room we do not hold is not a reason | ||
| 367 | + ;; to start holding it: `add-user` builds the buffer it is given, | ||
| 368 | + ;; which would put a refused room straight back in the list. | ||
| 369 | + (when (contains? @cells/channels name) | ||
| 370 | + (swap! cells/channels members/add-user name who)))) | ||
| 161 | 371 | ||
| 162 | ;; 353 is the roll, in as many lines as it takes; 366 ends it. | 372 | ;; 353 is the roll, in as many lines as it takes; 366 ends it. |
| 163 | (= "353" cmd) | 373 | (= "353" cmd) |
| @@ -280,7 +490,12 @@ | |||
| 280 | add (swap! cells/channels reactions/update-reaction | 490 | add (swap! cells/channels reactions/update-reaction |
| 281 | buffer msgid add who true) | 491 | buffer msgid add who true) |
| 282 | gone (swap! cells/channels reactions/update-reaction | 492 | gone (swap! cells/channels reactions/update-reaction |
| 283 | - buffer msgid gone who false)))))) | 493 | + buffer msgid gone who false)))) |
| 494 | + ;; Whatever that message did to the list, write it down. Throttled, so a | ||
| 495 | + ;; busy channel does not cost a file write per line — but a DM arriving | ||
| 496 | + ;; from someone new is a room that exists only here until this runs, and | ||
| 497 | + ;; the old client lost exactly those between runs. | ||
| 498 | + (remember-rooms!))) | ||
| 284 | 499 | ||
| 285 | (defn- note! [m] | 500 | (defn- note! [m] |
| 286 | (reset! last-line (str (:command m) " " (last (:params m)))) | 501 | (reset! last-line (str (:command m) " " (last (:params m)))) |
| @@ -492,8 +707,37 @@ | |||
| 492 | ;; and looks exactly like a | 707 | ;; and looks exactly like a |
| 493 | ;; failure. | 708 | ;; failure. |
| 494 | (reset! cells/screen :chats) | 709 | (reset! cells/screen :chats) |
| 495 | - (when-let [c @conn] | 710 | + ;; This session decides once |
| 496 | - (net/send-line! c "JOIN #test"))) | 711 | + ;; whether it is the one that |
| 712 | + ;; takes the room list over | ||
| 713 | + ;; from the server. Before the | ||
| 714 | + ;; flag is set the file has | ||
| 715 | + ;; never been told what we are | ||
| 716 | + ;; in, so the server's answer | ||
| 717 | + ;; is adopted rather than | ||
| 718 | + ;; argued with. | ||
| 719 | + (reset! adopting-rooms? | ||
| 720 | + (not @room-list-owned?)) | ||
| 721 | + (when-not @room-list-owned? | ||
| 722 | + (reset! room-list-owned? true) | ||
| 723 | + (save-prefs!)) | ||
| 724 | + ;; What the file says we are in, | ||
| 725 | + ;; we ask to be in. A room the | ||
| 726 | + ;; server has forgotten is one | ||
| 727 | + ;; that would otherwise quietly | ||
| 728 | + ;; stop existing. | ||
| 729 | + (join-saved-rooms!) | ||
| 730 | + ;; And back where the reader | ||
| 731 | + ;; left off, rather than into | ||
| 732 | + ;; #test on top of a list they | ||
| 733 | + ;; already had. The chats | ||
| 734 | + ;; screen stays underneath, so | ||
| 735 | + ;; Back goes to the list rather | ||
| 736 | + ;; than out of the app. | ||
| 737 | + (if-let [last-room | ||
| 738 | + (:name (first (rooms/channel-list)))] | ||
| 739 | + (actions/open-channel! last-room) | ||
| 740 | + (actions/join! auto-join))) | ||
| 497 | 741 | ||
| 498 | (contains? registration-failed cmd) | 742 | (contains? registration-failed cmd) |
| 499 | (fail! (if (= "433" cmd) | 743 | (fail! (if (= "433" cmd) |
| @@ -507,6 +751,7 @@ | |||
| 507 | (reset! cells/status (str "… " cmd))))) | 751 | (reset! cells/status (str "… " cmd))))) |
| 508 | :on-close (fn [why] | 752 | :on-close (fn [why] |
| 509 | (reset! conn nil) | 753 | (reset! conn nil) |
| 754 | + (forget-memberships!) | ||
| 510 | ;; Say what the last thing seen was. A | 755 | ;; Say what the last thing seen was. A |
| 511 | ;; connection that registers and then | 756 | ;; connection that registers and then |
| 512 | ;; drops is a different bug from one | 757 | ;; drops is a different bug from one |
| @@ -521,7 +766,16 @@ | |||
| 521 | ;; CAP first, then registration — the order the server expects, and the | 766 | ;; CAP first, then registration — the order the server expects, and the |
| 522 | ;; order `frq.irc` uses. | 767 | ;; order `frq.irc` uses. |
| 523 | (net/send-line! sock "CAP LS 302") | 768 | (net/send-line! sock "CAP LS 302") |
| 524 | - (let [nick (if-let [h (:handle @cells/session)] h @cells/form-nick)] | 769 | + ;; `form-nick` and nothing else. The handle went on the wire here, |
| 770 | + ;; and the sign-in had already put the *nick* in the cell — so the | ||
| 771 | + ;; channel called us alice.bsky.social while the client thought it | ||
| 772 | + ;; was alice, and every test of "is this me?" against a prefix came | ||
| 773 | + ;; back false. Our own JOIN echo stopped setting `:joined?`, so | ||
| 774 | + ;; every room wore a "not joined" badge and Open re-sent a JOIN for | ||
| 775 | + ;; a room we were already in. The DID is the identity; the nick is | ||
| 776 | + ;; only what the channel calls us, and both halves have to agree on | ||
| 777 | + ;; it. Same nick the desktop dials with. | ||
| 778 | + (let [nick (str @cells/form-nick)] | ||
| 525 | (net/send-line! sock (str "NICK " nick)) | 779 | (net/send-line! sock (str "NICK " nick)) |
| 526 | (net/send-line! sock (str "USER " nick " 0 * :frq")))) | 780 | (net/send-line! sock (str "USER " nick " 0 * :frq")))) |
| 527 | ;; Object, not Exception. Dart keeps Error and Exception in separate | 781 | ;; Object, not Exception. Dart keeps Error and Exception in separate |
| @@ -534,6 +788,7 @@ | |||
| 534 | (reset! closing? true) | 788 | (reset! closing? true) |
| 535 | (when-let [c @conn] (net/close! c)) | 789 | (when-let [c @conn] (net/close! c)) |
| 536 | (reset! conn nil) | 790 | (reset! conn nil) |
| 791 | + (forget-memberships!) | ||
| 537 | (reset! cells/connecting? false) | 792 | (reset! cells/connecting? false) |
| 538 | (reset! cells/status "Not connected") | 793 | (reset! cells/status "Not connected") |
| 539 | (reset! cells/screen :connect)) | 794 | (reset! cells/screen :connect)) |
| @@ -771,6 +1026,11 @@ | |||
| 771 | ;; Before any widget is built: a cell that changes before its watch is on | 1026 | ;; Before any widget is built: a cell that changes before its watch is on |
| 772 | ;; is a change the screen never hears about. | 1027 | ;; is a change the screen never hears about. |
| 773 | (watch-cells!) | 1028 | (watch-cells!) |
| 1029 | + ;; The room list of earlier runs, and the flag that says whether it is the | ||
| 1030 | + ;; authority yet. Before the saved session, because that connects on its | ||
| 1031 | + ;; own the moment it is restored and 001 reads both of these. | ||
| 1032 | + (restore-prefs!) | ||
| 1033 | + (restore-rooms!) | ||
| 774 | ;; A sign-in that already happened. Only the durable broker token comes | 1034 | ;; A sign-in that already happened. Only the durable broker token comes |
| 775 | ;; back — the connection mints a fresh web-token from it — so this is not | 1035 | ;; back — the connection mints a fresh web-token from it — so this is not |
| 776 | ;; a session, it is the means to ask for one. | 1036 | ;; a session, it is the means to ask for one. |
| @@ -800,13 +1060,38 @@ | |||
| 800 | {:connect! connect! | 1060 | {:connect! connect! |
| 801 | :disconnect! disconnect! | 1061 | :disconnect! disconnect! |
| 802 | :connected? (fn [] (some? @conn)) | 1062 | :connected? (fn [] (some? @conn)) |
| 1063 | + ;; The Join box, and the same one box for both things the desktop's | ||
| 1064 | + ;; `join!` does: `@nick` opens a DM, anything else is a channel and | ||
| 1065 | + ;; gets its `#` if it came without one. Through `open-room!` rather | ||
| 1066 | + ;; than straight onto the wire, because a bare JOIN left the buffer | ||
| 1067 | + ;; unbuilt — and the rule in the JOIN handler would then read our own | ||
| 1068 | + ;; echo as a room the server had invented and part it again. | ||
| 803 | :join! (fn [name] | 1069 | :join! (fn [name] |
| 804 | - (when-let [c @conn] | 1070 | + (let [name (.trim (str name))] |
| 805 | - (when (seq (str name)) | 1071 | + (cond |
| 806 | - (net/send-line! c (str "JOIN " name))))) | 1072 | + (.startsWith name "@") |
| 1073 | + (let [who (.substring name 1)] | ||
| 1074 | + (when (seq who) (open-room! who))) | ||
| 1075 | + | ||
| 1076 | + (seq name) | ||
| 1077 | + (open-room! (if (.startsWith name "#") name (str "#" name))) | ||
| 1078 | + | ||
| 1079 | + :else nil))) | ||
| 807 | :leave-channel! (fn [name] | 1080 | :leave-channel! (fn [name] |
| 808 | - (when-let [c @conn] (net/send-line! c (str "PART " name))) | 1081 | + ;; Channels only, as on the desktop. There is nothing |
| 809 | - (swap! cells/channels dissoc name)) | 1082 | + ;; to part in a conversation with a person — the |
| 1083 | + ;; buffer is the whole of it — and `PART alice` is a | ||
| 1084 | + ;; line the server answers with an error. | ||
| 1085 | + (when (and @conn (.startsWith (str name) "#")) | ||
| 1086 | + (net/send-line! @conn (str "PART " name))) | ||
| 1087 | + (swap! cells/channels dissoc name) | ||
| 1088 | + (when (= name @cells/current) | ||
| 1089 | + (reset! cells/current nil) | ||
| 1090 | + (reset! cells/screen :chats)) | ||
| 1091 | + ;; The only way a room leaves the file. Everything | ||
| 1092 | + ;; else adds one, so without this the list is a thing | ||
| 1093 | + ;; that only grows. | ||
| 1094 | + (remember-rooms! true)) | ||
| 810 | :send-draft! send-draft! | 1095 | :send-draft! send-draft! |
| 811 | 1096 | ||
| 812 | ;; Pictures. The chooser is the platform's, so the browsing screen the | 1097 | ;; Pictures. The chooser is the platform's, so the browsing screen the |
| @@ -953,31 +1238,7 @@ | |||
| 953 | (reset! cells/broker-token nil) | 1238 | (reset! cells/broker-token nil) |
| 954 | (reset! cells/session nil)) | 1239 | (reset! cells/session nil)) |
| 955 | :jump-to-present! (fn [] (reset! cells/at-present? true)) | 1240 | :jump-to-present! (fn [] (reset! cells/at-present? true)) |
| 956 | - :open-channel! (fn [name] | 1241 | + :open-channel! open-room!}) |
| 957 | - (reset! cells/current name) | ||
| 958 | - ;; An edit belongs to a line in the buffer being left: | ||
| 959 | - ;; carried across, the next Send would rewrite a | ||
| 960 | - ;; message nobody in this room can see. The box empties | ||
| 961 | - ;; with it, because what is in it is a copy of that | ||
| 962 | - ;; line — `frq.state/open-channel!` does this too. | ||
| 963 | - (when (and @cells/editing | ||
| 964 | - (not= name (:channel @cells/editing))) | ||
| 965 | - (reset! cells/editing nil) | ||
| 966 | - (reset! cells/draft "")) | ||
| 967 | - ;; `frq.state/open-channel!` does this and more: it | ||
| 968 | - ;; marks the buffer read, remembers the room list and | ||
| 969 | - ;; asks for NAMES. What it also does, and what made | ||
| 970 | - ;; Open look like a no-op here, is go to the | ||
| 971 | - ;; conversation — and join it on the way, because a row | ||
| 972 | - ;; can outlive the membership behind it. | ||
| 973 | - (swap! cells/channels update name | ||
| 974 | - #(merge {:name name :messages [] :unread 0} %)) | ||
| 975 | - (reset! cells/screen :chat) | ||
| 976 | - (let [buffer (get @cells/channels name)] | ||
| 977 | - (when (and @conn | ||
| 978 | - (.startsWith (str name) "#") | ||
| 979 | - (not (:joined? buffer))) | ||
| 980 | - (net/send-line! @conn (str "JOIN " name)))))}) | ||
| 981 | (f/run | 1242 | (f/run |
| 982 | (m/MaterialApp .title "frq" .theme (t/app-theme)) | 1243 | (m/MaterialApp .title "frq" .theme (t/app-theme)) |
| 983 | .home | 1244 | .home |