nandi/frqpublic Fork 0
88741e9
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandi committed 2026-09-06T02:49:51-07:00 Browse files
88741e9 parent: 6b820ee
modified src/frq/app.jolt +13 -2
@@ -170,6 +170,13 @@
170170 :kind :primary
171171 :on-click #(s/open-channel! name)}])
172172
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+
173180 (defn conversation-row [buffer]
174181 (let [name (:name buffer)
175182 unread (:unread buffer)]
@@ -189,7 +196,8 @@
189196 (if @terminal?
190197 [:hbox {:spacing 8 :wrap false}
191198 [:title-2 {:label name}]
192- [open-button name]]
199+ [open-button name]
200+ [close-button name]]
193201 [:title-2 {:label name}])
194202 ;; `:wrap false` on the badges: they sit beside each other or not at all.
195203 [:hbox {:spacing 12 :wrap false}
@@ -206,7 +214,10 @@
206214 (when (pos? unread)
207215 [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]
208216 [: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]])]))
210221
211222 (def ^:private sidebar-width 320)
212223
@@ -170,6 +170,13 @@
170 :kind :primary170 :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 @@
182182 ;; the answer to something you just did.
183183 (defonce hide-join-part? (atom false))
184184
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+
185198 (defn- save-prefs! []
186199 (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?))))
188202
189203 (defn toggle-hide-join-part! []
190204 (swap! hide-join-part? not)
@@ -195,6 +209,7 @@
195209 []
196210 (let [prefs (store/load-prefs)]
197211 (reset! hide-join-part? (boolean (:hide-join-part? prefs)))
212+ (reset! room-list-owned? (boolean (:room-list-owned? prefs)))
198213 prefs))
199214
200215 (defn connected? [] (some? @conn))
@@ -270,7 +285,13 @@
270285 "Answer what the marker says: how many lines are unseen, and whether any of
271286 them names the reader."
272287 [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))]
274295 (assoc buffer
275296 :unread (count fresh)
276297 :mention? (boolean (some mentions-me? fresh)))))
@@ -339,7 +360,13 @@
339360 ([channel from text] (push-message! channel from text {}))
340361 ([channel from text {:keys [at did id reply-to reactions edited?]}]
341362 (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))]
343370 (doseq [url (media/image-urls text)]
344371 (media/fetch! url #(swap! media-tick inc)))
345372 ;; The same tick: an avatar arriving is a picture arriving, and the chat
@@ -425,8 +452,8 @@
425452 ;; itself. Everything else re-derives, so a line arriving
426453 ;; in a room nobody is looking at costs a recount of that
427454 ;; 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?))))
430457
431458 (defn open-channel!
432459 "Show a buffer. A channel we are not in is joined on the way — a row can
@@ -464,6 +491,44 @@
464491 (when (:joined? (get @channels name))
465492 (request-names! name)))
466493
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+
467532 (defn- parse-reactions
468533 "The server's tally of what is already on a message, as
469534 `emoji:nick,nick;emoji:nick` what CHATHISTORY sends so reactions survive a
@@ -742,6 +807,18 @@
742807 (str "Connected as " @form-nick)))
743808 (reset! connecting? false)
744809 (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!)
745822 ;; Back where the reader left off. The list is still what a
746823 ;; connect lands on underneath, so Back from the reopened
747824 ;; channel goes to the chats rather than out of the app.
@@ -825,14 +902,30 @@
825902 :else nil))
826903 "JOIN" (let [ch (first params)]
827904 (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]))]
829919 (swap! channels #(-> (ensure-channel % ch)
830920 (assoc-in [ch :joined?] true)
831921 (assoc-in [ch :joining?] false)))
832922 ;; Only on the way in to an empty buffer. A reconnect joins
833923 ;; every channel again, and saying so on top of the backlog
834924 ;; 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)))
836929 (do (add-user! ch from)
837930 (when-not @hide-join-part?
838931 (push-message! ch "*" (str from " joined"))))))
@@ -1630,6 +1723,12 @@
16301723 "The rooms as they go to disk: what each one is, when it last said anything,
16311724 and how far into it the reader has got.
16321725
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+
16331732 `:unread` and `:mention?` are not written. They are what the marker adds up
16341733 to against the messages in hand, and a count written down is a count that can
16351734 be wrong the marker cannot be. `:mention?` rides along all the same, as the
@@ -1639,7 +1738,6 @@
16391738 moment the backlog lands."
16401739 []
16411740 (->> (vals @channels)
1642- (filter #(pos? (:accessed % 0)))
16431741 (sort-by #(- (:accessed % 0)))
16441742 (mapv #(select-keys % [:name :kind :peer-did :last-activity
16451743 :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 of285 "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 buffer295 (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 chat372 ;; 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 arriving452 ;; itself. Everything else re-derives, so a line arriving
426 ;; in a room nobody is looking at costs a recount of that453 ;; 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 can459 "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-reactions532 (defn- parse-reactions
468 "The server's tally of what is already on a message, as533 "The server's tally of what is already on a message, as
469 `emoji:nick,nick;emoji:nick` what CHATHISTORY sends so reactions survive a534 `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 a822 ;; Back where the reader left off. The list is still what a
746 ;; connect lands on underneath, so Back from the reopened823 ;; 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 joins922 ;; Only on the way in to an empty buffer. A reconnect joins
833 ;; every channel again, and saying so on top of the backlog923 ;; 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 up1732 `: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 can1733 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 the1734 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-activity1742 (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?]))))