Let rooms.edn say what we are in, and the server answer for it
The file was a record of what the server had said, which is the wrong way round: freeq re-joins an authenticated user's channels at registration and is wrong in both directions — it forgets rooms and announces ones that are not ours — so the unreliable half was deciding and the local half was writing down whatever it decided. Three things put it right. On connect we ask to be in every channel the file names. A room the server has forgotten is otherwise one that quietly stops existing, and asking for a membership we already have costs a line. A JOIN for our own nick in a room the file does not claim is answered with PART. That is the half that keeps a list nobody chose from filling up. The cost, and it is a real one: a channel joined from another client is left again on the next connect. And Close on the room row parts and forgets. Nothing else ever removed a room — the list only grew — so strictness had nothing to be strict about, and rooms.edn could never shrink. The first connect adopts instead of parting. Before this version has run once the file has never been told what we are in, and a client that parted everything missing from it would walk out of every room it was actually in. So session one writes down what the server says and sets :room-list-owned?; every connect after it is the strict one. Two rooms that were never really unread stop counting while this is open: the join notice a room writes about itself, and the joins, parts and quits under it. They are the room talking about itself rather than somebody talking in it — counted, every room you are a member of sits at one unread from the moment it opens, saying only that you joined it. Rooms are all written down now, too, not only the ones that have been opened: being in a channel is what makes it yours, and opening it only says which you looked at last. A dozen channels came back as one because the rest were left for the server to remember. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88741e9 parent: 6b820ee modified
src/frq/app.jolt +13 -2 | @@ -170,6 +170,13 @@ | ||
| 170 | 170 | :kind :primary |
| 171 | 171 | :on-click #(s/open-channel! name)}]) |
| 172 | 172 | |
| 173 | +;; The only way out of a room. Everything else adds one — the server saying we | |
| 174 | +;; are in it, a message arriving in it — so without this the list only grows, | |
| 175 | +;; and what rooms.edn claims we are in could never shrink. | |
| 176 | +(defn- close-button [name] | |
| 177 | + [:button {:label "Close" | |
| 178 | + :on-click #(s/leave-channel! name)}]) | |
| 179 | + | |
| 173 | 180 | (defn conversation-row [buffer] |
| 174 | 181 | (let [name (:name buffer) |
| 175 | 182 | unread (:unread buffer)] |
| @@ -189,7 +196,8 @@ | ||
| 189 | 196 | (if @terminal? |
| 190 | 197 | [:hbox {:spacing 8 :wrap false} |
| 191 | 198 | [:title-2 {:label name}] |
| 192 | - [open-button name]] | |
| 199 | + [open-button name] | |
| 200 | + [close-button name]] | |
| 193 | 201 | [:title-2 {:label name}]) |
| 194 | 202 | ;; `:wrap false` on the badges: they sit beside each other or not at all. |
| 195 | 203 | [:hbox {:spacing 12 :wrap false} |
| @@ -206,7 +214,10 @@ | ||
| 206 | 214 | (when (pos? unread) |
| 207 | 215 | [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])] |
| 208 | 216 | [:dim-label {:label (s/last-preview buffer)}] |
| 209 | - (when-not @terminal? [open-button name])])) | |
| 217 | + (when-not @terminal? | |
| 218 | + [:hbox {:spacing 8 :wrap false} | |
| 219 | + [open-button name] | |
| 220 | + [close-button name]])])) | |
| 210 | 221 | |
| 211 | 222 | (def ^:private sidebar-width 320) |
| 212 | 223 | |
| @@ -170,6 +170,13 @@ | |||
| 170 | :kind :primary | 170 | :kind :primary |
| 171 | :on-click #(s/open-channel! name)}]) | 171 | :on-click #(s/open-channel! name)}]) |
| 172 | 172 | ||
| 173 | +;; The only way out of a room. Everything else adds one — the server saying we | ||
| 174 | +;; are in it, a message arriving in it — so without this the list only grows, | ||
| 175 | +;; and what rooms.edn claims we are in could never shrink. | ||
| 176 | +(defn- close-button [name] | ||
| 177 | + [:button {:label "Close" | ||
| 178 | + :on-click #(s/leave-channel! name)}]) | ||
| 179 | + | ||
| 173 | (defn conversation-row [buffer] | 180 | (defn conversation-row [buffer] |
| 174 | (let [name (:name buffer) | 181 | (let [name (:name buffer) |
| 175 | unread (:unread buffer)] | 182 | unread (:unread buffer)] |
| @@ -189,7 +196,8 @@ | |||
| 189 | (if @terminal? | 196 | (if @terminal? |
| 190 | [:hbox {:spacing 8 :wrap false} | 197 | [:hbox {:spacing 8 :wrap false} |
| 191 | [:title-2 {:label name}] | 198 | [:title-2 {:label name}] |
| 192 | - [open-button name]] | 199 | + [open-button name] |
| 200 | + [close-button name]] | ||
| 193 | [:title-2 {:label name}]) | 201 | [:title-2 {:label name}]) |
| 194 | ;; `:wrap false` on the badges: they sit beside each other or not at all. | 202 | ;; `:wrap false` on the badges: they sit beside each other or not at all. |
| 195 | [:hbox {:spacing 12 :wrap false} | 203 | [:hbox {:spacing 12 :wrap false} |
| @@ -206,7 +214,10 @@ | |||
| 206 | (when (pos? unread) | 214 | (when (pos? unread) |
| 207 | [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])] | 215 | [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])] |
| 208 | [:dim-label {:label (s/last-preview buffer)}] | 216 | [:dim-label {:label (s/last-preview buffer)}] |
| 209 | - (when-not @terminal? [open-button name])])) | 217 | + (when-not @terminal? |
| 218 | + [:hbox {:spacing 8 :wrap false} | ||
| 219 | + [open-button name] | ||
| 220 | + [close-button name]])])) | ||
| 210 | 221 | ||
| 211 | (def ^:private sidebar-width 320) | 222 | (def ^:private sidebar-width 320) |
| 212 | 223 | ||
modified
src/frq/state.jolt +106 -8 | @@ -182,9 +182,23 @@ | ||
| 182 | 182 | ;; the answer to something you just did. |
| 183 | 183 | (defonce hide-join-part? (atom false)) |
| 184 | 184 | |
| 185 | +;; Whether rooms.edn is the authority yet. | |
| 186 | +;; | |
| 187 | +;; It is not, the first time this version runs: freeq re-joins an authenticated | |
| 188 | +;; user's channels at registration, so on connect the server announces every | |
| 189 | +;; room it has you in — and a client that parted everything not already in its | |
| 190 | +;; file would walk out of all of them before the file had ever been told they | |
| 191 | +;; existed. So the first connect adopts what the server says and writes it | |
| 192 | +;; down, and every connect after that is the strict one. | |
| 193 | +(defonce room-list-owned? (atom false)) | |
| 194 | + | |
| 195 | +;; And whether this session is the adopting one, decided at 001. | |
| 196 | +(defonce ^:private adopting-rooms? (atom false)) | |
| 197 | + | |
| 185 | 198 | (defn- save-prefs! [] |
| 186 | 199 | (future (store/save-prefs! (assoc (store/load-prefs) |
| 187 | - :hide-join-part? @hide-join-part?)))) | |
| 200 | + :hide-join-part? @hide-join-part? | |
| 201 | + :room-list-owned? @room-list-owned?)))) | |
| 188 | 202 | |
| 189 | 203 | (defn toggle-hide-join-part! [] |
| 190 | 204 | (swap! hide-join-part? not) |
| @@ -195,6 +209,7 @@ | ||
| 195 | 209 | [] |
| 196 | 210 | (let [prefs (store/load-prefs)] |
| 197 | 211 | (reset! hide-join-part? (boolean (:hide-join-part? prefs))) |
| 212 | + (reset! room-list-owned? (boolean (:room-list-owned? prefs))) | |
| 198 | 213 | prefs)) |
| 199 | 214 | |
| 200 | 215 | (defn connected? [] (some? @conn)) |
| @@ -270,7 +285,13 @@ | ||
| 270 | 285 | "Answer what the marker says: how many lines are unseen, and whether any of |
| 271 | 286 | them names the reader." |
| 272 | 287 | [buffer] |
| 273 | - (let [fresh (after-marker buffer)] | |
| 288 | + ;; Joins, parts, quits and "Joined #room" are the room talking about itself, | |
| 289 | + ;; not somebody talking in it. They arrive stamped now — the join notice is | |
| 290 | + ;; written the moment we are in — so counted, every room you are a member of | |
| 291 | + ;; sits at one unread from the moment it opens, saying only that you joined | |
| 292 | + ;; it. The marker still moves past them: they are read, they are just never | |
| 293 | + ;; what made a room worth looking at. | |
| 294 | + (let [fresh (remove :system? (after-marker buffer))] | |
| 274 | 295 | (assoc buffer |
| 275 | 296 | :unread (count fresh) |
| 276 | 297 | :mention? (boolean (some mentions-me? fresh))))) |
| @@ -339,7 +360,13 @@ | ||
| 339 | 360 | ([channel from text] (push-message! channel from text {})) |
| 340 | 361 | ([channel from text {:keys [at did id reply-to reactions edited?]}] |
| 341 | 362 | (let [at (or at (clock/now-ms)) |
| 342 | - who (avatars/actor did from)] | |
| 363 | + who (avatars/actor did from) | |
| 364 | + ;; A room reaching the store matters more than the throttle does: a | |
| 365 | + ;; connection joins every channel at once, and the writes for all but | |
| 366 | + ;; the first would be five seconds away — long enough that quitting | |
| 367 | + ;; straight after signing in is how a client forgets the rooms it just | |
| 368 | + ;; joined. Read before the swap, so this is the arrival that made it. | |
| 369 | + new-room? (not (contains? @channels channel))] | |
| 343 | 370 | (doseq [url (media/image-urls text)] |
| 344 | 371 | (media/fetch! url #(swap! media-tick inc))) |
| 345 | 372 | ;; The same tick: an avatar arriving is a picture arriving, and the chat |
| @@ -425,8 +452,8 @@ | ||
| 425 | 452 | ;; itself. Everything else re-derives, so a line arriving |
| 426 | 453 | ;; in a room nobody is looking at costs a recount of that |
| 427 | 454 | ;; room and nothing more. |
| 428 | - (update channel (if viewing? mark-read recount)))))))) | |
| 429 | - (remember-rooms!))) | |
| 455 | + (update channel (if viewing? mark-read recount))))))) | |
| 456 | + (remember-rooms! new-room?)))) | |
| 430 | 457 | |
| 431 | 458 | (defn open-channel! |
| 432 | 459 | "Show a buffer. A channel we are not in is joined on the way — a row can |
| @@ -464,6 +491,44 @@ | ||
| 464 | 491 | (when (:joined? (get @channels name)) |
| 465 | 492 | (request-names! name))) |
| 466 | 493 | |
| 494 | +(defn join-saved-rooms! | |
| 495 | + "Ask to be in every channel `rooms.edn` says we are in. | |
| 496 | + | |
| 497 | + The server re-joins an authenticated user's channels itself, and gets it | |
| 498 | + wrong in both directions — it forgets rooms and announces ones that are not | |
| 499 | + ours. This is the half that answers the forgetting: the file says what we are | |
| 500 | + in, so on arrival we say it too. A JOIN for a channel the server has already | |
| 501 | + put us in is answered with the membership we already have, so asking twice | |
| 502 | + costs nothing. | |
| 503 | + | |
| 504 | + DMs are not asked for. There is nothing to join in a conversation with a | |
| 505 | + person; the buffer is the whole of it." | |
| 506 | + [] | |
| 507 | + (when-let [conn @conn] | |
| 508 | + (doseq [[name buffer] @channels | |
| 509 | + :when (and (str/starts-with? name "#") | |
| 510 | + (not (:joined? buffer)) | |
| 511 | + (not (:joining? buffer)))] | |
| 512 | + (swap! channels #(assoc-in % [name :joining?] true)) | |
| 513 | + (irc/join! conn name)))) | |
| 514 | + | |
| 515 | +(defn leave-channel! | |
| 516 | + "Leave a room and forget it: PART on the wire, gone from the list, gone from | |
| 517 | + `rooms.edn`. | |
| 518 | + | |
| 519 | + The only way a room leaves the file. Everything else adds — the server | |
| 520 | + announcing one, a message arriving in one — so without this the list is a | |
| 521 | + thing that only grows, and the strictness above would have nothing to be | |
| 522 | + strict about." | |
| 523 | + [name] | |
| 524 | + (when (and @conn (str/starts-with? name "#")) | |
| 525 | + (irc/part! @conn name)) | |
| 526 | + (swap! channels dissoc name) | |
| 527 | + (when (= name @current) | |
| 528 | + (reset! current nil) | |
| 529 | + (reset! screen :chats)) | |
| 530 | + (remember-rooms! true)) | |
| 531 | + | |
| 467 | 532 | (defn- parse-reactions |
| 468 | 533 | "The server's tally of what is already on a message, as |
| 469 | 534 | `emoji:nick,nick;emoji:nick` — what CHATHISTORY sends so reactions survive a |
| @@ -742,6 +807,18 @@ | ||
| 742 | 807 | (str "Connected as " @form-nick))) |
| 743 | 808 | (reset! connecting? false) |
| 744 | 809 | (reset! screen :chats) |
| 810 | + ;; This session decides once whether it is the one that | |
| 811 | + ;; takes the room list over from the server. Before the flag | |
| 812 | + ;; is set the file has never been told what we are in, so the | |
| 813 | + ;; server's answer is adopted rather than argued with. | |
| 814 | + (reset! adopting-rooms? (not @room-list-owned?)) | |
| 815 | + (when-not @room-list-owned? | |
| 816 | + (reset! room-list-owned? true) | |
| 817 | + (save-prefs!)) | |
| 818 | + ;; What the file says we are in, we ask to be in. The server | |
| 819 | + ;; forgets rooms, and a room it has forgotten is one that | |
| 820 | + ;; would otherwise quietly stop existing. | |
| 821 | + (join-saved-rooms!) | |
| 745 | 822 | ;; Back where the reader left off. The list is still what a |
| 746 | 823 | ;; connect lands on underneath, so Back from the reopened |
| 747 | 824 | ;; channel goes to the chats rather than out of the app. |
| @@ -825,14 +902,30 @@ | ||
| 825 | 902 | :else nil)) |
| 826 | 903 | "JOIN" (let [ch (first params)] |
| 827 | 904 | (if (= from @form-nick) |
| 828 | - (let [fresh? (empty? (get-in @channels [ch :messages]))] | |
| 905 | + ;; Ours if the file says so — restored from rooms.edn, or | |
| 906 | + ;; asked for since. Anything else is the server putting us | |
| 907 | + ;; somewhere we did not ask to be, which it does: it | |
| 908 | + ;; announces memberships that are not real, and adding them | |
| 909 | + ;; is how a list nobody chose fills up with rooms. | |
| 910 | + ;; | |
| 911 | + ;; So we leave again, unless this is the session that is | |
| 912 | + ;; still adopting — on the first connect the file has not | |
| 913 | + ;; been told anything yet, and parting then would be leaving | |
| 914 | + ;; every room we are actually in. | |
| 915 | + (if (and (not (contains? @channels ch)) | |
| 916 | + (not @adopting-rooms?)) | |
| 917 | + (when @conn (irc/part! @conn ch)) | |
| 918 | + (let [fresh? (empty? (get-in @channels [ch :messages]))] | |
| 829 | 919 | (swap! channels #(-> (ensure-channel % ch) |
| 830 | 920 | (assoc-in [ch :joined?] true) |
| 831 | 921 | (assoc-in [ch :joining?] false))) |
| 832 | 922 | ;; Only on the way in to an empty buffer. A reconnect joins |
| 833 | 923 | ;; every channel again, and saying so on top of the backlog |
| 834 | 924 | ;; already there is just a second line of noise. |
| 835 | - (when fresh? (push-message! ch "*" (str "Joined " ch)))) | |
| 925 | + (when fresh? (push-message! ch "*" (str "Joined " ch))) | |
| 926 | + ;; Adopted or asked for, it is ours now and the file should | |
| 927 | + ;; say so before the next connect judges it. | |
| 928 | + (remember-rooms! true))) | |
| 836 | 929 | (do (add-user! ch from) |
| 837 | 930 | (when-not @hide-join-part? |
| 838 | 931 | (push-message! ch "*" (str from " joined")))))) |
| @@ -1630,6 +1723,12 @@ | ||
| 1630 | 1723 | "The rooms as they go to disk: what each one is, when it last said anything, |
| 1631 | 1724 | and how far into it the reader has got. |
| 1632 | 1725 | |
| 1726 | + Every room, not only the ones that have been opened. Being in a channel is | |
| 1727 | + what makes it yours; opening it only says which you looked at last, and that | |
| 1728 | + is what `:accessed` orders them by. Writing down the opened ones alone is how | |
| 1729 | + a client in a dozen channels came back knowing one — the rest were left for | |
| 1730 | + the server to remember, which is the thing it does not do. | |
| 1731 | + | |
| 1633 | 1732 | `:unread` and `:mention?` are not written. They are what the marker adds up |
| 1634 | 1733 | to against the messages in hand, and a count written down is a count that can |
| 1635 | 1734 | be wrong — the marker cannot be. `:mention?` rides along all the same, as the |
| @@ -1639,7 +1738,6 @@ | ||
| 1639 | 1738 | moment the backlog lands." |
| 1640 | 1739 | [] |
| 1641 | 1740 | (->> (vals @channels) |
| 1642 | - (filter #(pos? (:accessed % 0))) | |
| 1643 | 1741 | (sort-by #(- (:accessed % 0))) |
| 1644 | 1742 | (mapv #(select-keys % [:name :kind :peer-did :last-activity |
| 1645 | 1743 | :last-read-id :last-read-at :mention?])))) |
| @@ -182,9 +182,23 @@ | |||
| 182 | ;; the answer to something you just did. | 182 | ;; the answer to something you just did. |
| 183 | (defonce hide-join-part? (atom false)) | 183 | (defonce hide-join-part? (atom false)) |
| 184 | 184 | ||
| 185 | +;; Whether rooms.edn is the authority yet. | ||
| 186 | +;; | ||
| 187 | +;; It is not, the first time this version runs: freeq re-joins an authenticated | ||
| 188 | +;; user's channels at registration, so on connect the server announces every | ||
| 189 | +;; room it has you in — and a client that parted everything not already in its | ||
| 190 | +;; file would walk out of all of them before the file had ever been told they | ||
| 191 | +;; existed. So the first connect adopts what the server says and writes it | ||
| 192 | +;; down, and every connect after that is the strict one. | ||
| 193 | +(defonce room-list-owned? (atom false)) | ||
| 194 | + | ||
| 195 | +;; And whether this session is the adopting one, decided at 001. | ||
| 196 | +(defonce ^:private adopting-rooms? (atom false)) | ||
| 197 | + | ||
| 185 | (defn- save-prefs! [] | 198 | (defn- save-prefs! [] |
| 186 | (future (store/save-prefs! (assoc (store/load-prefs) | 199 | (future (store/save-prefs! (assoc (store/load-prefs) |
| 187 | - :hide-join-part? @hide-join-part?)))) | 200 | + :hide-join-part? @hide-join-part? |
| 201 | + :room-list-owned? @room-list-owned?)))) | ||
| 188 | 202 | ||
| 189 | (defn toggle-hide-join-part! [] | 203 | (defn toggle-hide-join-part! [] |
| 190 | (swap! hide-join-part? not) | 204 | (swap! hide-join-part? not) |
| @@ -195,6 +209,7 @@ | |||
| 195 | [] | 209 | [] |
| 196 | (let [prefs (store/load-prefs)] | 210 | (let [prefs (store/load-prefs)] |
| 197 | (reset! hide-join-part? (boolean (:hide-join-part? prefs))) | 211 | (reset! hide-join-part? (boolean (:hide-join-part? prefs))) |
| 212 | + (reset! room-list-owned? (boolean (:room-list-owned? prefs))) | ||
| 198 | prefs)) | 213 | prefs)) |
| 199 | 214 | ||
| 200 | (defn connected? [] (some? @conn)) | 215 | (defn connected? [] (some? @conn)) |
| @@ -270,7 +285,13 @@ | |||
| 270 | "Answer what the marker says: how many lines are unseen, and whether any of | 285 | "Answer what the marker says: how many lines are unseen, and whether any of |
| 271 | them names the reader." | 286 | them names the reader." |
| 272 | [buffer] | 287 | [buffer] |
| 273 | - (let [fresh (after-marker buffer)] | 288 | + ;; Joins, parts, quits and "Joined #room" are the room talking about itself, |
| 289 | + ;; not somebody talking in it. They arrive stamped now — the join notice is | ||
| 290 | + ;; written the moment we are in — so counted, every room you are a member of | ||
| 291 | + ;; sits at one unread from the moment it opens, saying only that you joined | ||
| 292 | + ;; it. The marker still moves past them: they are read, they are just never | ||
| 293 | + ;; what made a room worth looking at. | ||
| 294 | + (let [fresh (remove :system? (after-marker buffer))] | ||
| 274 | (assoc buffer | 295 | (assoc buffer |
| 275 | :unread (count fresh) | 296 | :unread (count fresh) |
| 276 | :mention? (boolean (some mentions-me? fresh))))) | 297 | :mention? (boolean (some mentions-me? fresh))))) |
| @@ -339,7 +360,13 @@ | |||
| 339 | ([channel from text] (push-message! channel from text {})) | 360 | ([channel from text] (push-message! channel from text {})) |
| 340 | ([channel from text {:keys [at did id reply-to reactions edited?]}] | 361 | ([channel from text {:keys [at did id reply-to reactions edited?]}] |
| 341 | (let [at (or at (clock/now-ms)) | 362 | (let [at (or at (clock/now-ms)) |
| 342 | - who (avatars/actor did from)] | 363 | + who (avatars/actor did from) |
| 364 | + ;; A room reaching the store matters more than the throttle does: a | ||
| 365 | + ;; connection joins every channel at once, and the writes for all but | ||
| 366 | + ;; the first would be five seconds away — long enough that quitting | ||
| 367 | + ;; straight after signing in is how a client forgets the rooms it just | ||
| 368 | + ;; joined. Read before the swap, so this is the arrival that made it. | ||
| 369 | + new-room? (not (contains? @channels channel))] | ||
| 343 | (doseq [url (media/image-urls text)] | 370 | (doseq [url (media/image-urls text)] |
| 344 | (media/fetch! url #(swap! media-tick inc))) | 371 | (media/fetch! url #(swap! media-tick inc))) |
| 345 | ;; The same tick: an avatar arriving is a picture arriving, and the chat | 372 | ;; The same tick: an avatar arriving is a picture arriving, and the chat |
| @@ -425,8 +452,8 @@ | |||
| 425 | ;; itself. Everything else re-derives, so a line arriving | 452 | ;; itself. Everything else re-derives, so a line arriving |
| 426 | ;; in a room nobody is looking at costs a recount of that | 453 | ;; in a room nobody is looking at costs a recount of that |
| 427 | ;; room and nothing more. | 454 | ;; room and nothing more. |
| 428 | - (update channel (if viewing? mark-read recount)))))))) | 455 | + (update channel (if viewing? mark-read recount))))))) |
| 429 | - (remember-rooms!))) | 456 | + (remember-rooms! new-room?)))) |
| 430 | 457 | ||
| 431 | (defn open-channel! | 458 | (defn open-channel! |
| 432 | "Show a buffer. A channel we are not in is joined on the way — a row can | 459 | "Show a buffer. A channel we are not in is joined on the way — a row can |
| @@ -464,6 +491,44 @@ | |||
| 464 | (when (:joined? (get @channels name)) | 491 | (when (:joined? (get @channels name)) |
| 465 | (request-names! name))) | 492 | (request-names! name))) |
| 466 | 493 | ||
| 494 | +(defn join-saved-rooms! | ||
| 495 | + "Ask to be in every channel `rooms.edn` says we are in. | ||
| 496 | + | ||
| 497 | + The server re-joins an authenticated user's channels itself, and gets it | ||
| 498 | + wrong in both directions — it forgets rooms and announces ones that are not | ||
| 499 | + ours. This is the half that answers the forgetting: the file says what we are | ||
| 500 | + in, so on arrival we say it too. A JOIN for a channel the server has already | ||
| 501 | + put us in is answered with the membership we already have, so asking twice | ||
| 502 | + costs nothing. | ||
| 503 | + | ||
| 504 | + DMs are not asked for. There is nothing to join in a conversation with a | ||
| 505 | + person; the buffer is the whole of it." | ||
| 506 | + [] | ||
| 507 | + (when-let [conn @conn] | ||
| 508 | + (doseq [[name buffer] @channels | ||
| 509 | + :when (and (str/starts-with? name "#") | ||
| 510 | + (not (:joined? buffer)) | ||
| 511 | + (not (:joining? buffer)))] | ||
| 512 | + (swap! channels #(assoc-in % [name :joining?] true)) | ||
| 513 | + (irc/join! conn name)))) | ||
| 514 | + | ||
| 515 | +(defn leave-channel! | ||
| 516 | + "Leave a room and forget it: PART on the wire, gone from the list, gone from | ||
| 517 | + `rooms.edn`. | ||
| 518 | + | ||
| 519 | + The only way a room leaves the file. Everything else adds — the server | ||
| 520 | + announcing one, a message arriving in one — so without this the list is a | ||
| 521 | + thing that only grows, and the strictness above would have nothing to be | ||
| 522 | + strict about." | ||
| 523 | + [name] | ||
| 524 | + (when (and @conn (str/starts-with? name "#")) | ||
| 525 | + (irc/part! @conn name)) | ||
| 526 | + (swap! channels dissoc name) | ||
| 527 | + (when (= name @current) | ||
| 528 | + (reset! current nil) | ||
| 529 | + (reset! screen :chats)) | ||
| 530 | + (remember-rooms! true)) | ||
| 531 | + | ||
| 467 | (defn- parse-reactions | 532 | (defn- parse-reactions |
| 468 | "The server's tally of what is already on a message, as | 533 | "The server's tally of what is already on a message, as |
| 469 | `emoji:nick,nick;emoji:nick` — what CHATHISTORY sends so reactions survive a | 534 | `emoji:nick,nick;emoji:nick` — what CHATHISTORY sends so reactions survive a |
| @@ -742,6 +807,18 @@ | |||
| 742 | (str "Connected as " @form-nick))) | 807 | (str "Connected as " @form-nick))) |
| 743 | (reset! connecting? false) | 808 | (reset! connecting? false) |
| 744 | (reset! screen :chats) | 809 | (reset! screen :chats) |
| 810 | + ;; This session decides once whether it is the one that | ||
| 811 | + ;; takes the room list over from the server. Before the flag | ||
| 812 | + ;; is set the file has never been told what we are in, so the | ||
| 813 | + ;; server's answer is adopted rather than argued with. | ||
| 814 | + (reset! adopting-rooms? (not @room-list-owned?)) | ||
| 815 | + (when-not @room-list-owned? | ||
| 816 | + (reset! room-list-owned? true) | ||
| 817 | + (save-prefs!)) | ||
| 818 | + ;; What the file says we are in, we ask to be in. The server | ||
| 819 | + ;; forgets rooms, and a room it has forgotten is one that | ||
| 820 | + ;; would otherwise quietly stop existing. | ||
| 821 | + (join-saved-rooms!) | ||
| 745 | ;; Back where the reader left off. The list is still what a | 822 | ;; Back where the reader left off. The list is still what a |
| 746 | ;; connect lands on underneath, so Back from the reopened | 823 | ;; connect lands on underneath, so Back from the reopened |
| 747 | ;; channel goes to the chats rather than out of the app. | 824 | ;; channel goes to the chats rather than out of the app. |
| @@ -825,14 +902,30 @@ | |||
| 825 | :else nil)) | 902 | :else nil)) |
| 826 | "JOIN" (let [ch (first params)] | 903 | "JOIN" (let [ch (first params)] |
| 827 | (if (= from @form-nick) | 904 | (if (= from @form-nick) |
| 828 | - (let [fresh? (empty? (get-in @channels [ch :messages]))] | 905 | + ;; Ours if the file says so — restored from rooms.edn, or |
| 906 | + ;; asked for since. Anything else is the server putting us | ||
| 907 | + ;; somewhere we did not ask to be, which it does: it | ||
| 908 | + ;; announces memberships that are not real, and adding them | ||
| 909 | + ;; is how a list nobody chose fills up with rooms. | ||
| 910 | + ;; | ||
| 911 | + ;; So we leave again, unless this is the session that is | ||
| 912 | + ;; still adopting — on the first connect the file has not | ||
| 913 | + ;; been told anything yet, and parting then would be leaving | ||
| 914 | + ;; every room we are actually in. | ||
| 915 | + (if (and (not (contains? @channels ch)) | ||
| 916 | + (not @adopting-rooms?)) | ||
| 917 | + (when @conn (irc/part! @conn ch)) | ||
| 918 | + (let [fresh? (empty? (get-in @channels [ch :messages]))] | ||
| 829 | (swap! channels #(-> (ensure-channel % ch) | 919 | (swap! channels #(-> (ensure-channel % ch) |
| 830 | (assoc-in [ch :joined?] true) | 920 | (assoc-in [ch :joined?] true) |
| 831 | (assoc-in [ch :joining?] false))) | 921 | (assoc-in [ch :joining?] false))) |
| 832 | ;; Only on the way in to an empty buffer. A reconnect joins | 922 | ;; Only on the way in to an empty buffer. A reconnect joins |
| 833 | ;; every channel again, and saying so on top of the backlog | 923 | ;; every channel again, and saying so on top of the backlog |
| 834 | ;; already there is just a second line of noise. | 924 | ;; already there is just a second line of noise. |
| 835 | - (when fresh? (push-message! ch "*" (str "Joined " ch)))) | 925 | + (when fresh? (push-message! ch "*" (str "Joined " ch))) |
| 926 | + ;; Adopted or asked for, it is ours now and the file should | ||
| 927 | + ;; say so before the next connect judges it. | ||
| 928 | + (remember-rooms! true))) | ||
| 836 | (do (add-user! ch from) | 929 | (do (add-user! ch from) |
| 837 | (when-not @hide-join-part? | 930 | (when-not @hide-join-part? |
| 838 | (push-message! ch "*" (str from " joined")))))) | 931 | (push-message! ch "*" (str from " joined")))))) |
| @@ -1630,6 +1723,12 @@ | |||
| 1630 | "The rooms as they go to disk: what each one is, when it last said anything, | 1723 | "The rooms as they go to disk: what each one is, when it last said anything, |
| 1631 | and how far into it the reader has got. | 1724 | and how far into it the reader has got. |
| 1632 | 1725 | ||
| 1726 | + Every room, not only the ones that have been opened. Being in a channel is | ||
| 1727 | + what makes it yours; opening it only says which you looked at last, and that | ||
| 1728 | + is what `:accessed` orders them by. Writing down the opened ones alone is how | ||
| 1729 | + a client in a dozen channels came back knowing one — the rest were left for | ||
| 1730 | + the server to remember, which is the thing it does not do. | ||
| 1731 | + | ||
| 1633 | `:unread` and `:mention?` are not written. They are what the marker adds up | 1732 | `:unread` and `:mention?` are not written. They are what the marker adds up |
| 1634 | to against the messages in hand, and a count written down is a count that can | 1733 | to against the messages in hand, and a count written down is a count that can |
| 1635 | be wrong — the marker cannot be. `:mention?` rides along all the same, as the | 1734 | be wrong — the marker cannot be. `:mention?` rides along all the same, as the |
| @@ -1639,7 +1738,6 @@ | |||
| 1639 | moment the backlog lands." | 1738 | moment the backlog lands." |
| 1640 | [] | 1739 | [] |
| 1641 | (->> (vals @channels) | 1740 | (->> (vals @channels) |
| 1642 | - (filter #(pos? (:accessed % 0))) | ||
| 1643 | (sort-by #(- (:accessed % 0))) | 1741 | (sort-by #(- (:accessed % 0))) |
| 1644 | (mapv #(select-keys % [:name :kind :peer-did :last-activity | 1742 | (mapv #(select-keys % [:name :kind :peer-did :last-activity |
| 1645 | :last-read-id :last-read-at :mention?])))) | 1743 | :last-read-id :last-read-at :mention?])))) |