Share the room list and the read markers with the phone
`rooms.edn` was ported to the Flutter side a commit ago by writing a second copy of the desktop's room-list code beside it — a private `room-records`, `remember-rooms!` and `restore-rooms!` in `frq.main`, each the same shape as the one in `frq.state`. Two copies of a thing both backends need is what `common/` is for, so they move there: `frq.rooms` now holds the list and both halves call it. Under that was the reason the copy could not simply be the same function. The marker machinery — `after-marker`, `recount`, `mark-read`, `ensure-channel` — was jolt-only, so the phone had no `:unread` to write and no way to read one back. Its `room-records` compensated with a `room-extras` map: the fields it did not understand, read off the file and merged back on every write, so a desktop run's marker survived a phone run rather than being silently dropped and handing the reader their whole history as new. With the marker shared there is nothing left to preserve blindly. The phone counts its own unread now: messages carry `:at` off the `time` tag, a line arriving in the room on screen moves the marker past itself and one arriving anywhere else is counted against the marker there, and opening a room marks it read. `:at` also lights up the clock times and day separators in `frq.screens.chat`, which had been rendering nothing for want of it. What stays on this side is what is genuinely this side's: `room-list-owned?` and the adoption rule at 001, because the strict "part what the file does not hold" answer to freeq announcing memberships that are not real is newer than anything the desktop does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f751b7d parent: 23b7999 modified
common/frq/rooms.cljc +201 -1 | @@ -7,7 +7,9 @@ | ||
| 7 | 7 | the overview strip's list of what is happening in every other room. `frq.state` |
| 8 | 8 | re-defs them all, so the thousand lines below them did not move." |
| 9 | 9 | (:require [clojure.string :as str] |
| 10 | - [frq.cells :as cells])) | |
| 10 | + [frq.cells :as cells] | |
| 11 | + [frq.clock :as clock] | |
| 12 | + [frq.store :as store])) | |
| 11 | 13 | |
| 12 | 14 | (defn dm? |
| 13 | 15 | "Whether a buffer is a conversation with a person rather than a room. Every |
| @@ -126,3 +128,201 @@ | ||
| 126 | 128 | ;; last. The turn-taking above is about which lines are in it, not |
| 127 | 129 | ;; about where they sit. |
| 128 | 130 | (sort-by #(or (:at %) 0) >))) |
| 131 | + | |
| 132 | + | |
| 133 | +;; ------------------------------------------------------------- the marker | |
| 134 | +;; What has been seen, and what the unread count is derived from. Shared for | |
| 135 | +;; the reason the list is: a marker that only one half keeps is a phone that | |
| 136 | +;; comes back to a hundred lines it has already read. | |
| 137 | + | |
| 138 | +(defn after-marker | |
| 139 | + "The messages in `buffer` the reader has not seen: everything after its read | |
| 140 | + marker. | |
| 141 | + | |
| 142 | + By id where the marked message is still held, and by time otherwise. The id | |
| 143 | + is the exact answer — a msgid survives every revision, so it names the same | |
| 144 | + line however often the server replays it — and the timestamp is what answers | |
| 145 | + when the marked line has fallen off the end of the buffer or was never in | |
| 146 | + this run's copy of it. | |
| 147 | + | |
| 148 | + Derived rather than counted, because a count cannot survive what the server | |
| 149 | + does: a JOIN replays the backlog and CHATHISTORY replays it again, and every | |
| 150 | + line of it would tick a counter a second time. Against a marker a replayed | |
| 151 | + line is simply older than it and counts for nothing." | |
| 152 | + [buffer] | |
| 153 | + (let [id (:last-read-id buffer) | |
| 154 | + at (:last-read-at buffer 0) | |
| 155 | + msgs (vec (:messages buffer))] | |
| 156 | + (if (and id (some #(= id (:id %)) msgs)) | |
| 157 | + (vec (rest (drop-while #(not= id (:id %)) msgs))) | |
| 158 | + (filterv #(> (:at % 0) at) msgs)))) | |
| 159 | + | |
| 160 | +(defn mentions-me? | |
| 161 | + "Whether a line is addressed at the reader by name. Our own lines do not | |
| 162 | + count — saying your own nick is not being called." | |
| 163 | + [m] | |
| 164 | + (let [me (str/trim (or @cells/form-nick ""))] | |
| 165 | + (and (seq me) | |
| 166 | + (not= (:from m) me) | |
| 167 | + (str/includes? (str/lower-case (or (:text m) "")) | |
| 168 | + (str/lower-case me))))) | |
| 169 | + | |
| 170 | +(defn recount | |
| 171 | + "Answer what the marker says: how many lines are unseen, and whether any of | |
| 172 | + them names the reader." | |
| 173 | + [buffer] | |
| 174 | + ;; Joins, parts, quits and "Joined #room" are the room talking about itself, | |
| 175 | + ;; not somebody talking in it. They arrive stamped now — the join notice is | |
| 176 | + ;; written the moment we are in — so counted, every room you are a member of | |
| 177 | + ;; sits at one unread from the moment it opens, saying only that you joined | |
| 178 | + ;; it. The marker still moves past them: they are read, they are just never | |
| 179 | + ;; what made a room worth looking at. | |
| 180 | + (let [fresh (remove :system? (after-marker buffer))] | |
| 181 | + (assoc buffer | |
| 182 | + :unread (count fresh) | |
| 183 | + :mention? (boolean (some mentions-me? fresh))))) | |
| 184 | + | |
| 185 | +(defn mark-read | |
| 186 | + "Move the marker to the newest line this buffer holds. Both halves: the id | |
| 187 | + for as long as that line is here, and its time for after it is gone. | |
| 188 | + | |
| 189 | + The time only ever goes forward. A backlog can arrive after the reader has | |
| 190 | + already read past it, and taking the last line's time unconditionally would | |
| 191 | + walk the marker backwards and re-unread what was read." | |
| 192 | + [buffer] | |
| 193 | + (let [newest (last (:messages buffer))] | |
| 194 | + (assoc buffer | |
| 195 | + :unread 0 | |
| 196 | + :mention? false | |
| 197 | + :last-read-id (:id newest) | |
| 198 | + :last-read-at (max (:last-read-at buffer 0) (:at newest 0))))) | |
| 199 | + | |
| 200 | +;; How far back a room nobody has seen before counts as already read. | |
| 201 | +;; | |
| 202 | +;; A room being joined for the first time replays its whole history, and none | |
| 203 | +;; of that is news — the reader was not away for it, they were not here. So a | |
| 204 | +;; new buffer starts caught up rather than at the beginning, or joining a busy | |
| 205 | +;; channel announces a hundred unread posts from before you arrived. | |
| 206 | +;; | |
| 207 | +;; Caught up to a minute ago rather than to this instant, because a live line | |
| 208 | +;; is timestamped by the server and this by our clock: the two disagree by | |
| 209 | +;; whatever the skew is, and a live message stamped a few seconds behind us | |
| 210 | +;; would land under the marker and never be counted. A minute is more skew than | |
| 211 | +;; there will be and far less than the age of any backlog, and what it costs is | |
| 212 | +;; that a message sent in the minute before you joined counts as unread — which | |
| 213 | +;; is the harmless direction. | |
| 214 | +(def fresh-room-grace-ms 60000) | |
| 215 | + | |
| 216 | +(defn ensure-channel [m name] | |
| 217 | + (if (contains? m name) | |
| 218 | + m | |
| 219 | + (assoc m name {:name name :messages [] :unread 0 | |
| 220 | + :joined? false :joining? false :accessed 0 | |
| 221 | + ;; What has been seen, and what the count is derived from. | |
| 222 | + ;; `:unread` and `:mention?` are answers, not records — see | |
| 223 | + ;; `recount`. | |
| 224 | + :last-read-id nil :mention? false | |
| 225 | + :last-read-at (max 0 (- (clock/now-ms) fresh-room-grace-ms)) | |
| 226 | + :kind (if (dm? name) :dm :channel) | |
| 227 | + :peer-did nil :last-activity 0 | |
| 228 | + ;; nick -> mode prefix, for the people panel | |
| 229 | + :users {}}))) | |
| 230 | + | |
| 231 | +;; ---------------------------------------------------------------- the list | |
| 232 | +;; A counter rather than a clock: the list only needs their order, and a | |
| 233 | +;; monotonic tick cannot be surprised by the system time moving. | |
| 234 | +(defonce access-tick (atom 0)) | |
| 235 | + | |
| 236 | +(defn room-records | |
| 237 | + "The rooms as they go to disk: what each one is, when it last said anything, | |
| 238 | + and how far into it the reader has got. | |
| 239 | + | |
| 240 | + Every room, not only the ones that have been opened. Being in a channel is | |
| 241 | + what makes it yours; opening it only says which you looked at last, and that | |
| 242 | + is what `:accessed` orders them by. Writing down the opened ones alone is how | |
| 243 | + a client in a dozen channels came back knowing one — the rest were left for | |
| 244 | + the server to remember, which is the thing it does not do. | |
| 245 | + | |
| 246 | + `:unread` and `:mention?` are not written. They are what the marker adds up | |
| 247 | + to against the messages in hand, and a count written down is a count that can | |
| 248 | + be wrong — the marker cannot be. `:mention?` rides along all the same, as the | |
| 249 | + one thing that cannot be recomputed before the history it was derived from | |
| 250 | + comes back: a room that had your name in it says so on the next run's first | |
| 251 | + frame rather than a round trip later, and is corrected by `recount` the | |
| 252 | + moment the backlog lands." | |
| 253 | + [] | |
| 254 | + (->> (vals @cells/channels) | |
| 255 | + (sort-by #(- (:accessed % 0))) | |
| 256 | + (mapv #(select-keys % [:name :kind :peer-did :last-activity | |
| 257 | + :last-read-id :last-read-at :mention?])))) | |
| 258 | + | |
| 259 | +(defonce ^:private rooms-saved-at (atom 0)) | |
| 260 | + | |
| 261 | +(defn remember-rooms! | |
| 262 | + "Write the room records out, most recently used first: what rooms there are, | |
| 263 | + in the order they were last used, and how much of each has been read. | |
| 264 | + | |
| 265 | + Throttled, because the marker moves on every line that arrives while a room | |
| 266 | + is on screen and a busy channel would otherwise write the file per message. | |
| 267 | + `force?` is for the moments worth paying for, which is a room being opened, | |
| 268 | + joined or closed. | |
| 269 | + | |
| 270 | + A late write costs at most the handful of lines that arrived since the last | |
| 271 | + one, shown unread again on the next run. That is the right way round: the | |
| 272 | + marker never claims to have read more than it has. | |
| 273 | + | |
| 274 | + Shared because both halves have the same thing worth keeping and the same | |
| 275 | + moments worth keeping it at. The desktop puts this on a thread of its own as | |
| 276 | + well — a jolt answer to a jolt cost, and why `frq.state` keeps its own | |
| 277 | + wrapper around `save-rooms!` rather than calling this." | |
| 278 | + ([] (remember-rooms! false)) | |
| 279 | + ([force?] | |
| 280 | + (let [now (clock/now-ms)] | |
| 281 | + (when (or force? (> (- now @rooms-saved-at) 5000)) | |
| 282 | + (reset! rooms-saved-at now) | |
| 283 | + (store/save-rooms! (room-records)))))) | |
| 284 | + | |
| 285 | +(defn restore-channels! | |
| 286 | + "Bring back the rooms of earlier runs, in the order they were last used, each | |
| 287 | + with the marker saying how much of it had been read. | |
| 288 | + | |
| 289 | + Empty buffers, not memberships: opening one is what joins it, and a list of | |
| 290 | + rooms is the part worth keeping — the messages in them come from the server. | |
| 291 | + The tick is seeded so this run's first open still sorts above all of them. | |
| 292 | + | |
| 293 | + The marker is what makes the returning backlog readable. Without one every | |
| 294 | + replayed line is new and every room comes back with its whole history | |
| 295 | + unread; with one, the reader is put back where they were and only what | |
| 296 | + arrived while they were away is counted. A room migrated from an older frq | |
| 297 | + has no marker and is caught up as if read, which is the kinder of the two | |
| 298 | + wrong answers — the alternative announces a hundred unread lines the reader | |
| 299 | + has already seen." | |
| 300 | + [] | |
| 301 | + (when-let [saved (seq (store/load-rooms))] | |
| 302 | + (let [ordered (reverse saved)] ; oldest first, so ticks ascend | |
| 303 | + (swap! cells/channels | |
| 304 | + (fn [m] | |
| 305 | + (reduce (fn [acc room] | |
| 306 | + (let [name (:name room)] | |
| 307 | + (if (contains? acc name) | |
| 308 | + acc | |
| 309 | + (assoc acc name | |
| 310 | + {:name name :messages [] :unread 0 | |
| 311 | + :joined? false :joining? false | |
| 312 | + :users {} | |
| 313 | + :kind (or (:kind room) | |
| 314 | + (if (dm? name) :dm :channel)) | |
| 315 | + :peer-did (:peer-did room) | |
| 316 | + :last-activity (:last-activity room 0) | |
| 317 | + :last-read-id (:last-read-id room) | |
| 318 | + ;; No marker at all — an older frq's list, | |
| 319 | + ;; or a record that lost it. Read up to | |
| 320 | + ;; now rather than back to the beginning. | |
| 321 | + :last-read-at (:last-read-at room | |
| 322 | + (clock/now-ms)) | |
| 323 | + :mention? (boolean (:mention? room)) | |
| 324 | + :accessed (swap! access-tick inc)}))) | |
| 325 | + ) | |
| 326 | + m | |
| 327 | + ordered)))) | |
| 328 | + (count saved))) | |
| @@ -7,7 +7,9 @@ | |||
| 7 | the overview strip's list of what is happening in every other room. `frq.state` | 7 | the overview strip's list of what is happening in every other room. `frq.state` |
| 8 | re-defs them all, so the thousand lines below them did not move." | 8 | re-defs them all, so the thousand lines below them did not move." |
| 9 | (:require [clojure.string :as str] | 9 | (:require [clojure.string :as str] |
| 10 | - [frq.cells :as cells])) | 10 | + [frq.cells :as cells] |
| 11 | + [frq.clock :as clock] | ||
| 12 | + [frq.store :as store])) | ||
| 11 | 13 | ||
| 12 | (defn dm? | 14 | (defn dm? |
| 13 | "Whether a buffer is a conversation with a person rather than a room. Every | 15 | "Whether a buffer is a conversation with a person rather than a room. Every |
| @@ -126,3 +128,201 @@ | |||
| 126 | ;; last. The turn-taking above is about which lines are in it, not | 128 | ;; last. The turn-taking above is about which lines are in it, not |
| 127 | ;; about where they sit. | 129 | ;; about where they sit. |
| 128 | (sort-by #(or (:at %) 0) >))) | 130 | (sort-by #(or (:at %) 0) >))) |
| 131 | + | ||
| 132 | + | ||
| 133 | +;; ------------------------------------------------------------- the marker | ||
| 134 | +;; What has been seen, and what the unread count is derived from. Shared for | ||
| 135 | +;; the reason the list is: a marker that only one half keeps is a phone that | ||
| 136 | +;; comes back to a hundred lines it has already read. | ||
| 137 | + | ||
| 138 | +(defn after-marker | ||
| 139 | + "The messages in `buffer` the reader has not seen: everything after its read | ||
| 140 | + marker. | ||
| 141 | + | ||
| 142 | + By id where the marked message is still held, and by time otherwise. The id | ||
| 143 | + is the exact answer — a msgid survives every revision, so it names the same | ||
| 144 | + line however often the server replays it — and the timestamp is what answers | ||
| 145 | + when the marked line has fallen off the end of the buffer or was never in | ||
| 146 | + this run's copy of it. | ||
| 147 | + | ||
| 148 | + Derived rather than counted, because a count cannot survive what the server | ||
| 149 | + does: a JOIN replays the backlog and CHATHISTORY replays it again, and every | ||
| 150 | + line of it would tick a counter a second time. Against a marker a replayed | ||
| 151 | + line is simply older than it and counts for nothing." | ||
| 152 | + [buffer] | ||
| 153 | + (let [id (:last-read-id buffer) | ||
| 154 | + at (:last-read-at buffer 0) | ||
| 155 | + msgs (vec (:messages buffer))] | ||
| 156 | + (if (and id (some #(= id (:id %)) msgs)) | ||
| 157 | + (vec (rest (drop-while #(not= id (:id %)) msgs))) | ||
| 158 | + (filterv #(> (:at % 0) at) msgs)))) | ||
| 159 | + | ||
| 160 | +(defn mentions-me? | ||
| 161 | + "Whether a line is addressed at the reader by name. Our own lines do not | ||
| 162 | + count — saying your own nick is not being called." | ||
| 163 | + [m] | ||
| 164 | + (let [me (str/trim (or @cells/form-nick ""))] | ||
| 165 | + (and (seq me) | ||
| 166 | + (not= (:from m) me) | ||
| 167 | + (str/includes? (str/lower-case (or (:text m) "")) | ||
| 168 | + (str/lower-case me))))) | ||
| 169 | + | ||
| 170 | +(defn recount | ||
| 171 | + "Answer what the marker says: how many lines are unseen, and whether any of | ||
| 172 | + them names the reader." | ||
| 173 | + [buffer] | ||
| 174 | + ;; Joins, parts, quits and "Joined #room" are the room talking about itself, | ||
| 175 | + ;; not somebody talking in it. They arrive stamped now — the join notice is | ||
| 176 | + ;; written the moment we are in — so counted, every room you are a member of | ||
| 177 | + ;; sits at one unread from the moment it opens, saying only that you joined | ||
| 178 | + ;; it. The marker still moves past them: they are read, they are just never | ||
| 179 | + ;; what made a room worth looking at. | ||
| 180 | + (let [fresh (remove :system? (after-marker buffer))] | ||
| 181 | + (assoc buffer | ||
| 182 | + :unread (count fresh) | ||
| 183 | + :mention? (boolean (some mentions-me? fresh))))) | ||
| 184 | + | ||
| 185 | +(defn mark-read | ||
| 186 | + "Move the marker to the newest line this buffer holds. Both halves: the id | ||
| 187 | + for as long as that line is here, and its time for after it is gone. | ||
| 188 | + | ||
| 189 | + The time only ever goes forward. A backlog can arrive after the reader has | ||
| 190 | + already read past it, and taking the last line's time unconditionally would | ||
| 191 | + walk the marker backwards and re-unread what was read." | ||
| 192 | + [buffer] | ||
| 193 | + (let [newest (last (:messages buffer))] | ||
| 194 | + (assoc buffer | ||
| 195 | + :unread 0 | ||
| 196 | + :mention? false | ||
| 197 | + :last-read-id (:id newest) | ||
| 198 | + :last-read-at (max (:last-read-at buffer 0) (:at newest 0))))) | ||
| 199 | + | ||
| 200 | +;; How far back a room nobody has seen before counts as already read. | ||
| 201 | +;; | ||
| 202 | +;; A room being joined for the first time replays its whole history, and none | ||
| 203 | +;; of that is news — the reader was not away for it, they were not here. So a | ||
| 204 | +;; new buffer starts caught up rather than at the beginning, or joining a busy | ||
| 205 | +;; channel announces a hundred unread posts from before you arrived. | ||
| 206 | +;; | ||
| 207 | +;; Caught up to a minute ago rather than to this instant, because a live line | ||
| 208 | +;; is timestamped by the server and this by our clock: the two disagree by | ||
| 209 | +;; whatever the skew is, and a live message stamped a few seconds behind us | ||
| 210 | +;; would land under the marker and never be counted. A minute is more skew than | ||
| 211 | +;; there will be and far less than the age of any backlog, and what it costs is | ||
| 212 | +;; that a message sent in the minute before you joined counts as unread — which | ||
| 213 | +;; is the harmless direction. | ||
| 214 | +(def fresh-room-grace-ms 60000) | ||
| 215 | + | ||
| 216 | +(defn ensure-channel [m name] | ||
| 217 | + (if (contains? m name) | ||
| 218 | + m | ||
| 219 | + (assoc m name {:name name :messages [] :unread 0 | ||
| 220 | + :joined? false :joining? false :accessed 0 | ||
| 221 | + ;; What has been seen, and what the count is derived from. | ||
| 222 | + ;; `:unread` and `:mention?` are answers, not records — see | ||
| 223 | + ;; `recount`. | ||
| 224 | + :last-read-id nil :mention? false | ||
| 225 | + :last-read-at (max 0 (- (clock/now-ms) fresh-room-grace-ms)) | ||
| 226 | + :kind (if (dm? name) :dm :channel) | ||
| 227 | + :peer-did nil :last-activity 0 | ||
| 228 | + ;; nick -> mode prefix, for the people panel | ||
| 229 | + :users {}}))) | ||
| 230 | + | ||
| 231 | +;; ---------------------------------------------------------------- the list | ||
| 232 | +;; A counter rather than a clock: the list only needs their order, and a | ||
| 233 | +;; monotonic tick cannot be surprised by the system time moving. | ||
| 234 | +(defonce access-tick (atom 0)) | ||
| 235 | + | ||
| 236 | +(defn room-records | ||
| 237 | + "The rooms as they go to disk: what each one is, when it last said anything, | ||
| 238 | + and how far into it the reader has got. | ||
| 239 | + | ||
| 240 | + Every room, not only the ones that have been opened. Being in a channel is | ||
| 241 | + what makes it yours; opening it only says which you looked at last, and that | ||
| 242 | + is what `:accessed` orders them by. Writing down the opened ones alone is how | ||
| 243 | + a client in a dozen channels came back knowing one — the rest were left for | ||
| 244 | + the server to remember, which is the thing it does not do. | ||
| 245 | + | ||
| 246 | + `:unread` and `:mention?` are not written. They are what the marker adds up | ||
| 247 | + to against the messages in hand, and a count written down is a count that can | ||
| 248 | + be wrong — the marker cannot be. `:mention?` rides along all the same, as the | ||
| 249 | + one thing that cannot be recomputed before the history it was derived from | ||
| 250 | + comes back: a room that had your name in it says so on the next run's first | ||
| 251 | + frame rather than a round trip later, and is corrected by `recount` the | ||
| 252 | + moment the backlog lands." | ||
| 253 | + [] | ||
| 254 | + (->> (vals @cells/channels) | ||
| 255 | + (sort-by #(- (:accessed % 0))) | ||
| 256 | + (mapv #(select-keys % [:name :kind :peer-did :last-activity | ||
| 257 | + :last-read-id :last-read-at :mention?])))) | ||
| 258 | + | ||
| 259 | +(defonce ^:private rooms-saved-at (atom 0)) | ||
| 260 | + | ||
| 261 | +(defn remember-rooms! | ||
| 262 | + "Write the room records out, most recently used first: what rooms there are, | ||
| 263 | + in the order they were last used, and how much of each has been read. | ||
| 264 | + | ||
| 265 | + Throttled, because the marker moves on every line that arrives while a room | ||
| 266 | + is on screen and a busy channel would otherwise write the file per message. | ||
| 267 | + `force?` is for the moments worth paying for, which is a room being opened, | ||
| 268 | + joined or closed. | ||
| 269 | + | ||
| 270 | + A late write costs at most the handful of lines that arrived since the last | ||
| 271 | + one, shown unread again on the next run. That is the right way round: the | ||
| 272 | + marker never claims to have read more than it has. | ||
| 273 | + | ||
| 274 | + Shared because both halves have the same thing worth keeping and the same | ||
| 275 | + moments worth keeping it at. The desktop puts this on a thread of its own as | ||
| 276 | + well — a jolt answer to a jolt cost, and why `frq.state` keeps its own | ||
| 277 | + wrapper around `save-rooms!` rather than calling this." | ||
| 278 | + ([] (remember-rooms! false)) | ||
| 279 | + ([force?] | ||
| 280 | + (let [now (clock/now-ms)] | ||
| 281 | + (when (or force? (> (- now @rooms-saved-at) 5000)) | ||
| 282 | + (reset! rooms-saved-at now) | ||
| 283 | + (store/save-rooms! (room-records)))))) | ||
| 284 | + | ||
| 285 | +(defn restore-channels! | ||
| 286 | + "Bring back the rooms of earlier runs, in the order they were last used, each | ||
| 287 | + with the marker saying how much of it had been read. | ||
| 288 | + | ||
| 289 | + Empty buffers, not memberships: opening one is what joins it, and a list of | ||
| 290 | + rooms is the part worth keeping — the messages in them come from the server. | ||
| 291 | + The tick is seeded so this run's first open still sorts above all of them. | ||
| 292 | + | ||
| 293 | + The marker is what makes the returning backlog readable. Without one every | ||
| 294 | + replayed line is new and every room comes back with its whole history | ||
| 295 | + unread; with one, the reader is put back where they were and only what | ||
| 296 | + arrived while they were away is counted. A room migrated from an older frq | ||
| 297 | + has no marker and is caught up as if read, which is the kinder of the two | ||
| 298 | + wrong answers — the alternative announces a hundred unread lines the reader | ||
| 299 | + has already seen." | ||
| 300 | + [] | ||
| 301 | + (when-let [saved (seq (store/load-rooms))] | ||
| 302 | + (let [ordered (reverse saved)] ; oldest first, so ticks ascend | ||
| 303 | + (swap! cells/channels | ||
| 304 | + (fn [m] | ||
| 305 | + (reduce (fn [acc room] | ||
| 306 | + (let [name (:name room)] | ||
| 307 | + (if (contains? acc name) | ||
| 308 | + acc | ||
| 309 | + (assoc acc name | ||
| 310 | + {:name name :messages [] :unread 0 | ||
| 311 | + :joined? false :joining? false | ||
| 312 | + :users {} | ||
| 313 | + :kind (or (:kind room) | ||
| 314 | + (if (dm? name) :dm :channel)) | ||
| 315 | + :peer-did (:peer-did room) | ||
| 316 | + :last-activity (:last-activity room 0) | ||
| 317 | + :last-read-id (:last-read-id room) | ||
| 318 | + ;; No marker at all — an older frq's list, | ||
| 319 | + ;; or a record that lost it. Read up to | ||
| 320 | + ;; now rather than back to the beginning. | ||
| 321 | + :last-read-at (:last-read-at room | ||
| 322 | + (clock/now-ms)) | ||
| 323 | + :mention? (boolean (:mention? room)) | ||
| 324 | + :accessed (swap! access-tick inc)}))) | ||
| 325 | + ) | ||
| 326 | + m | ||
| 327 | + ordered)))) | ||
| 328 | + (count saved))) | ||
modified
flutter/src/frq/main.cljd +89 -124 | @@ -157,15 +157,6 @@ | ||
| 157 | 157 | ;; And whether this session is the adopting one, decided at 001. |
| 158 | 158 | (defonce ^:private adopting-rooms? (atom false)) |
| 159 | 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 | 160 | (defn- save-prefs! |
| 170 | 161 | "Keep the one flag that has to outlive the run: whether this client has |
| 171 | 162 | taken the room list over from the server. Merged into whatever else |
| @@ -180,68 +171,17 @@ | ||
| 180 | 171 | (reset! room-list-owned? (boolean (:room-list-owned? (store/load-prefs)))) |
| 181 | 172 | nil) |
| 182 | 173 | |
| 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))) | |
| 174 | +(def ^:private remember-rooms! | |
| 175 | + "Write the room list out, most recently used first. `frq.rooms` — the same | |
| 176 | + records the desktop writes, throttled the same way, so a marker one half | |
| 177 | + moved is one the other reads back." | |
| 178 | + rooms/remember-rooms!) | |
| 179 | + | |
| 180 | +(def ^:private restore-rooms! | |
| 181 | + "Bring back the rooms of earlier runs, in the order they were last used, | |
| 182 | + each with the marker saying how much of it had been read. `frq.rooms` again: | |
| 183 | + the buffers this makes are the ones `recount` counts against." | |
| 184 | + rooms/restore-channels!) | |
| 245 | 185 | |
| 246 | 186 | (defn- ask-to-join! |
| 247 | 187 | "Send the JOIN, once. `:joining?` as well as `:joined?`, because the echo |
| @@ -274,13 +214,14 @@ | ||
| 274 | 214 | (when (and @cells/editing (not= name (:channel @cells/editing))) |
| 275 | 215 | (reset! cells/editing nil) |
| 276 | 216 | (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)) | |
| 217 | + ;; Opening a room is reading it: the marker goes to the newest line this | |
| 218 | + ;; buffer holds, which is what stops the backlog that follows from arriving | |
| 219 | + ;; unread all over again. The tick is `frq.rooms`', so a room opened this | |
| 220 | + ;; run sorts above every one `restore-rooms!` counted in. | |
| 221 | + (swap! cells/channels | |
| 222 | + #(-> (rooms/ensure-channel % name) | |
| 223 | + (update name rooms/mark-read) | |
| 224 | + (assoc-in [name :accessed] (swap! rooms/access-tick inc)))) | |
| 284 | 225 | (reset! cells/screen :chat) |
| 285 | 226 | ;; Worth a write of its own: the order the list is read in is the order |
| 286 | 227 | ;; rooms were last opened, and this is the moment it changes. |
| @@ -338,7 +279,10 @@ | ||
| 338 | 279 | params (vec (:params m)) |
| 339 | 280 | who (irc/nick-of (:prefix m)) |
| 340 | 281 | me? (= who (str @cells/form-nick)) |
| 341 | - ensure (fn [m name] (update m name #(merge {:name name :messages [] :unread 0} %)))] | |
| 282 | + ;; The shared one, so a buffer born here carries the same read marker | |
| 283 | + ;; the desktop's does — `frq.rooms/recount` has nothing to count | |
| 284 | + ;; against without it. | |
| 285 | + ensure rooms/ensure-channel] | |
| 342 | 286 | (cond |
| 343 | 287 | (= "JOIN" cmd) |
| 344 | 288 | (let [name (first params)] |
| @@ -417,7 +361,20 @@ | ||
| 417 | 361 | text (last params) |
| 418 | 362 | name (if (rooms/dm? target) who target) |
| 419 | 363 | edit-of (or (irc/tag-value tags "+draft/edit") |
| 420 | - (irc/tag-value tags "+edit"))] | |
| 364 | + (irc/tag-value tags "+edit")) | |
| 365 | + ;; When the server says the line was written, or now when it says | |
| 366 | + ;; nothing. The marker is a time as well as an id — a replayed | |
| 367 | + ;; backlog is older than what has been read and has to be able to | |
| 368 | + ;; say so — and the day separators in `frq.screens.chat` have been | |
| 369 | + ;; waiting on this too. | |
| 370 | + at (or (clock/parse-time-tag tags) (clock/now-ms)) | |
| 371 | + ;; Reading a room is marking it read: a line arriving in the room | |
| 372 | + ;; on screen moves the marker past itself, and one arriving | |
| 373 | + ;; anywhere else is counted against the marker there. | |
| 374 | + viewing? (and (= :chat @cells/screen) (= name @cells/current)) | |
| 375 | + settle (fn [m] (update m name (if viewing? | |
| 376 | + rooms/mark-read | |
| 377 | + rooms/recount)))] | |
| 421 | 378 | ;; The face, asked for as the line arrives rather than when the row is |
| 422 | 379 | ;; built: a row is built during a Flutter frame, and a fetch started |
| 423 | 380 | ;; there would be state changed mid-build. `frq.avatars.dart` asks the |
| @@ -430,49 +387,55 @@ | ||
| 430 | 387 | ;; nothing else refers to. One older than the backlog we hold has |
| 431 | 388 | ;; nothing here to replace, and is shown as itself rather than lost. |
| 432 | 389 | (when (= :absent (edit-message! name edit-of who text)) |
| 433 | - (swap! cells/channels update name | |
| 434 | - #(-> (merge {:name name :messages [] :unread 0} %) | |
| 435 | - (update :messages conj {:from who | |
| 436 | - :text text | |
| 437 | - :images (images-in text) | |
| 438 | - :did (:account m) | |
| 439 | - :id edit-of | |
| 440 | - :edited? true})))) | |
| 441 | - (swap! cells/channels update name | |
| 442 | - #(-> (merge {:name name :messages [] :unread 0} %) | |
| 443 | - (update :messages conj | |
| 444 | - ;; The msgid is what everything after a message | |
| 445 | - ;; names it by — a reply points at one, a reaction | |
| 446 | - ;; lands on one. Without it a line is on screen and | |
| 447 | - ;; nothing can be said about it. | |
| 448 | - {:from who | |
| 449 | - :text text | |
| 450 | - ;; Any .png link on the line, and the fetch that | |
| 451 | - ;; makes one a picture rather than a link. | |
| 452 | - :images (images-in text) | |
| 453 | - :did (:account m) | |
| 454 | - ;; Who to look a profile up by: the DID the | |
| 455 | - ;; server put on the line, or the nick when that | |
| 456 | - ;; is handle-shaped. A guest has neither and gets | |
| 457 | - ;; no lookup, which is the honest answer. | |
| 458 | - :actor (profile/actor (:account m) who) | |
| 459 | - :id (irc/tag-value tags "msgid") | |
| 460 | - ;; The server canonicalises +draft/reply to | |
| 461 | - ;; +reply; a client that sent the draft name may | |
| 462 | - ;; still reach us before it does. | |
| 463 | - :reply-to (or (irc/tag-value tags "+reply") | |
| 464 | - (irc/tag-value tags "+draft/reply")) | |
| 465 | - ;; What is already on it, so a reconnect does not | |
| 466 | - ;; start every message empty. | |
| 467 | - ;; What the server says about a line it has | |
| 468 | - ;; already collapsed: replay sends the current | |
| 469 | - ;; text and no `+draft/edit` to hint that it is | |
| 470 | - ;; not the original. This tag is the only trace. | |
| 471 | - :edited? (= "1" (irc/tag-value tags | |
| 472 | - "+freeq.at/edited")) | |
| 473 | - :reactions (reactions/parse-tally | |
| 474 | - (irc/tag-value tags | |
| 475 | - "+freeq.at/reactions"))}))))) | |
| 390 | + (swap! cells/channels | |
| 391 | + #(-> (rooms/ensure-channel % name) | |
| 392 | + (update-in [name :messages] conj | |
| 393 | + {:from who | |
| 394 | + :text text | |
| 395 | + :images (images-in text) | |
| 396 | + :did (:account m) | |
| 397 | + :id edit-of | |
| 398 | + :at at | |
| 399 | + :edited? true}) | |
| 400 | + settle)))) | |
| 401 | + (swap! cells/channels | |
| 402 | + #(-> (rooms/ensure-channel % name) | |
| 403 | + (update-in [name :messages] conj | |
| 404 | + ;; The msgid is what everything after a message | |
| 405 | + ;; names it by — a reply points at one, a reaction | |
| 406 | + ;; lands on one. Without it a line is on screen and | |
| 407 | + ;; nothing can be said about it. | |
| 408 | + {:from who | |
| 409 | + :text text | |
| 410 | + ;; Any .png link on the line, and the fetch that | |
| 411 | + ;; makes one a picture rather than a link. | |
| 412 | + :images (images-in text) | |
| 413 | + :did (:account m) | |
| 414 | + :at at | |
| 415 | + ;; Who to look a profile up by: the DID the | |
| 416 | + ;; server put on the line, or the nick when that | |
| 417 | + ;; is handle-shaped. A guest has neither and gets | |
| 418 | + ;; no lookup, which is the honest answer. | |
| 419 | + :actor (profile/actor (:account m) who) | |
| 420 | + :id (irc/tag-value tags "msgid") | |
| 421 | + ;; The server canonicalises +draft/reply to | |
| 422 | + ;; +reply; a client that sent the draft name may | |
| 423 | + ;; still reach us before it does. | |
| 424 | + :reply-to (or (irc/tag-value tags "+reply") | |
| 425 | + (irc/tag-value tags "+draft/reply")) | |
| 426 | + ;; What is already on it, so a reconnect does not | |
| 427 | + ;; start every message empty. | |
| 428 | + ;; What the server says about a line it has | |
| 429 | + ;; already collapsed: replay sends the current | |
| 430 | + ;; text and no `+draft/edit` to hint that it is | |
| 431 | + ;; not the original. This tag is the only trace. | |
| 432 | + :edited? (= "1" (irc/tag-value tags | |
| 433 | + "+freeq.at/edited")) | |
| 434 | + :reactions (reactions/parse-tally | |
| 435 | + (irc/tag-value tags | |
| 436 | + "+freeq.at/reactions"))}) | |
| 437 | + (assoc-in [name :last-activity] at) | |
| 438 | + settle))) | |
| 476 | 439 | |
| 477 | 440 | ;; A message that is only tags. A reaction is the one this reads: |
| 478 | 441 | ;; `+react` puts an emoji on the message `+reply` names, and the |
| @@ -494,7 +457,9 @@ | ||
| 494 | 457 | ;; Whatever that message did to the list, write it down. Throttled, so a |
| 495 | 458 | ;; busy channel does not cost a file write per line — but a DM arriving |
| 496 | 459 | ;; from someone new is a room that exists only here until this runs, and |
| 497 | - ;; the old client lost exactly those between runs. | |
| 460 | + ;; the old client lost exactly those between runs. The read marker moves | |
| 461 | + ;; on the same path, for the same reason: a line arriving in the room on | |
| 462 | + ;; screen is a line read. | |
| 498 | 463 | (remember-rooms!))) |
| 499 | 464 | |
| 500 | 465 | (defn- note! [m] |
| @@ -157,15 +157,6 @@ | |||
| 157 | ;; And whether this session is the adopting one, decided at 001. | 157 | ;; And whether this session is the adopting one, decided at 001. |
| 158 | (defonce ^:private adopting-rooms? (atom false)) | 158 | (defonce ^:private adopting-rooms? (atom false)) |
| 159 | 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! | 160 | (defn- save-prefs! |
| 170 | "Keep the one flag that has to outlive the run: whether this client has | 161 | "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 | 162 | taken the room list over from the server. Merged into whatever else |
| @@ -180,68 +171,17 @@ | |||
| 180 | (reset! room-list-owned? (boolean (:room-list-owned? (store/load-prefs)))) | 171 | (reset! room-list-owned? (boolean (:room-list-owned? (store/load-prefs)))) |
| 181 | nil) | 172 | nil) |
| 182 | 173 | ||
| 183 | -(defn- room-records | 174 | +(def ^:private remember-rooms! |
| 184 | - "The rooms as they go on disk: most recently used first, each carrying | 175 | + "Write the room list out, most recently used first. `frq.rooms` — the same |
| 185 | - whatever the file already said about it that this client does not hold." | 176 | + records the desktop writes, throttled the same way, so a marker one half |
| 186 | - [] | 177 | + moved is one the other reads back." |
| 187 | - (->> (vals @cells/channels) | 178 | + rooms/remember-rooms!) |
| 188 | - (sort-by #(- (:accessed % 0))) | 179 | + |
| 189 | - (mapv (fn [b] | 180 | +(def ^:private restore-rooms! |
| 190 | - (let [name (:name b)] | 181 | + "Bring back the rooms of earlier runs, in the order they were last used, |
| 191 | - (merge (get @room-extras name) | 182 | + each with the marker saying how much of it had been read. `frq.rooms` again: |
| 192 | - {:name name | 183 | + the buffers this makes are the ones `recount` counts against." |
| 193 | - :kind (or (:kind b) | 184 | + rooms/restore-channels!) |
| 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 | 185 | ||
| 246 | (defn- ask-to-join! | 186 | (defn- ask-to-join! |
| 247 | "Send the JOIN, once. `:joining?` as well as `:joined?`, because the echo | 187 | "Send the JOIN, once. `:joining?` as well as `:joined?`, because the echo |
| @@ -274,13 +214,14 @@ | |||
| 274 | (when (and @cells/editing (not= name (:channel @cells/editing))) | 214 | (when (and @cells/editing (not= name (:channel @cells/editing))) |
| 275 | (reset! cells/editing nil) | 215 | (reset! cells/editing nil) |
| 276 | (reset! cells/draft "")) | 216 | (reset! cells/draft "")) |
| 277 | - (swap! cells/channels update name | 217 | + ;; Opening a room is reading it: the marker goes to the newest line this |
| 278 | - #(merge {:name name :messages [] :unread 0 | 218 | + ;; buffer holds, which is what stops the backlog that follows from arriving |
| 279 | - :kind (if (rooms/dm? name) :dm :channel)} | 219 | + ;; unread all over again. The tick is `frq.rooms`', so a room opened this |
| 280 | - %)) | 220 | + ;; run sorts above every one `restore-rooms!` counted in. |
| 281 | - ;; The wall clock, so a room opened this run sorts above every one | 221 | + (swap! cells/channels |
| 282 | - ;; `restore-rooms!` counted in. | 222 | + #(-> (rooms/ensure-channel % name) |
| 283 | - (swap! cells/channels assoc-in [name :accessed] (clock/now-ms)) | 223 | + (update name rooms/mark-read) |
| 224 | + (assoc-in [name :accessed] (swap! rooms/access-tick inc)))) | ||
| 284 | (reset! cells/screen :chat) | 225 | (reset! cells/screen :chat) |
| 285 | ;; Worth a write of its own: the order the list is read in is the order | 226 | ;; 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. | 227 | ;; rooms were last opened, and this is the moment it changes. |
| @@ -338,7 +279,10 @@ | |||
| 338 | params (vec (:params m)) | 279 | params (vec (:params m)) |
| 339 | who (irc/nick-of (:prefix m)) | 280 | who (irc/nick-of (:prefix m)) |
| 340 | me? (= who (str @cells/form-nick)) | 281 | me? (= who (str @cells/form-nick)) |
| 341 | - ensure (fn [m name] (update m name #(merge {:name name :messages [] :unread 0} %)))] | 282 | + ;; The shared one, so a buffer born here carries the same read marker |
| 283 | + ;; the desktop's does — `frq.rooms/recount` has nothing to count | ||
| 284 | + ;; against without it. | ||
| 285 | + ensure rooms/ensure-channel] | ||
| 342 | (cond | 286 | (cond |
| 343 | (= "JOIN" cmd) | 287 | (= "JOIN" cmd) |
| 344 | (let [name (first params)] | 288 | (let [name (first params)] |
| @@ -417,7 +361,20 @@ | |||
| 417 | text (last params) | 361 | text (last params) |
| 418 | name (if (rooms/dm? target) who target) | 362 | name (if (rooms/dm? target) who target) |
| 419 | edit-of (or (irc/tag-value tags "+draft/edit") | 363 | edit-of (or (irc/tag-value tags "+draft/edit") |
| 420 | - (irc/tag-value tags "+edit"))] | 364 | + (irc/tag-value tags "+edit")) |
| 365 | + ;; When the server says the line was written, or now when it says | ||
| 366 | + ;; nothing. The marker is a time as well as an id — a replayed | ||
| 367 | + ;; backlog is older than what has been read and has to be able to | ||
| 368 | + ;; say so — and the day separators in `frq.screens.chat` have been | ||
| 369 | + ;; waiting on this too. | ||
| 370 | + at (or (clock/parse-time-tag tags) (clock/now-ms)) | ||
| 371 | + ;; Reading a room is marking it read: a line arriving in the room | ||
| 372 | + ;; on screen moves the marker past itself, and one arriving | ||
| 373 | + ;; anywhere else is counted against the marker there. | ||
| 374 | + viewing? (and (= :chat @cells/screen) (= name @cells/current)) | ||
| 375 | + settle (fn [m] (update m name (if viewing? | ||
| 376 | + rooms/mark-read | ||
| 377 | + rooms/recount)))] | ||
| 421 | ;; The face, asked for as the line arrives rather than when the row is | 378 | ;; The face, asked for as the line arrives rather than when the row is |
| 422 | ;; built: a row is built during a Flutter frame, and a fetch started | 379 | ;; built: a row is built during a Flutter frame, and a fetch started |
| 423 | ;; there would be state changed mid-build. `frq.avatars.dart` asks the | 380 | ;; there would be state changed mid-build. `frq.avatars.dart` asks the |
| @@ -430,49 +387,55 @@ | |||
| 430 | ;; nothing else refers to. One older than the backlog we hold has | 387 | ;; nothing else refers to. One older than the backlog we hold has |
| 431 | ;; nothing here to replace, and is shown as itself rather than lost. | 388 | ;; nothing here to replace, and is shown as itself rather than lost. |
| 432 | (when (= :absent (edit-message! name edit-of who text)) | 389 | (when (= :absent (edit-message! name edit-of who text)) |
| 433 | - (swap! cells/channels update name | 390 | + (swap! cells/channels |
| 434 | - #(-> (merge {:name name :messages [] :unread 0} %) | 391 | + #(-> (rooms/ensure-channel % name) |
| 435 | - (update :messages conj {:from who | 392 | + (update-in [name :messages] conj |
| 436 | - :text text | 393 | + {:from who |
| 437 | - :images (images-in text) | 394 | + :text text |
| 438 | - :did (:account m) | 395 | + :images (images-in text) |
| 439 | - :id edit-of | 396 | + :did (:account m) |
| 440 | - :edited? true})))) | 397 | + :id edit-of |
| 441 | - (swap! cells/channels update name | 398 | + :at at |
| 442 | - #(-> (merge {:name name :messages [] :unread 0} %) | 399 | + :edited? true}) |
| 443 | - (update :messages conj | 400 | + settle)))) |
| 444 | - ;; The msgid is what everything after a message | 401 | + (swap! cells/channels |
| 445 | - ;; names it by — a reply points at one, a reaction | 402 | + #(-> (rooms/ensure-channel % name) |
| 446 | - ;; lands on one. Without it a line is on screen and | 403 | + (update-in [name :messages] conj |
| 447 | - ;; nothing can be said about it. | 404 | + ;; The msgid is what everything after a message |
| 448 | - {:from who | 405 | + ;; names it by — a reply points at one, a reaction |
| 449 | - :text text | 406 | + ;; lands on one. Without it a line is on screen and |
| 450 | - ;; Any .png link on the line, and the fetch that | 407 | + ;; nothing can be said about it. |
| 451 | - ;; makes one a picture rather than a link. | 408 | + {:from who |
| 452 | - :images (images-in text) | 409 | + :text text |
| 453 | - :did (:account m) | 410 | + ;; Any .png link on the line, and the fetch that |
| 454 | - ;; Who to look a profile up by: the DID the | 411 | + ;; makes one a picture rather than a link. |
| 455 | - ;; server put on the line, or the nick when that | 412 | + :images (images-in text) |
| 456 | - ;; is handle-shaped. A guest has neither and gets | 413 | + :did (:account m) |
| 457 | - ;; no lookup, which is the honest answer. | 414 | + :at at |
| 458 | - :actor (profile/actor (:account m) who) | 415 | + ;; Who to look a profile up by: the DID the |
| 459 | - :id (irc/tag-value tags "msgid") | 416 | + ;; server put on the line, or the nick when that |
| 460 | - ;; The server canonicalises +draft/reply to | 417 | + ;; is handle-shaped. A guest has neither and gets |
| 461 | - ;; +reply; a client that sent the draft name may | 418 | + ;; no lookup, which is the honest answer. |
| 462 | - ;; still reach us before it does. | 419 | + :actor (profile/actor (:account m) who) |
| 463 | - :reply-to (or (irc/tag-value tags "+reply") | 420 | + :id (irc/tag-value tags "msgid") |
| 464 | - (irc/tag-value tags "+draft/reply")) | 421 | + ;; The server canonicalises +draft/reply to |
| 465 | - ;; What is already on it, so a reconnect does not | 422 | + ;; +reply; a client that sent the draft name may |
| 466 | - ;; start every message empty. | 423 | + ;; still reach us before it does. |
| 467 | - ;; What the server says about a line it has | 424 | + :reply-to (or (irc/tag-value tags "+reply") |
| 468 | - ;; already collapsed: replay sends the current | 425 | + (irc/tag-value tags "+draft/reply")) |
| 469 | - ;; text and no `+draft/edit` to hint that it is | 426 | + ;; What is already on it, so a reconnect does not |
| 470 | - ;; not the original. This tag is the only trace. | 427 | + ;; start every message empty. |
| 471 | - :edited? (= "1" (irc/tag-value tags | 428 | + ;; What the server says about a line it has |
| 472 | - "+freeq.at/edited")) | 429 | + ;; already collapsed: replay sends the current |
| 473 | - :reactions (reactions/parse-tally | 430 | + ;; text and no `+draft/edit` to hint that it is |
| 474 | - (irc/tag-value tags | 431 | + ;; not the original. This tag is the only trace. |
| 475 | - "+freeq.at/reactions"))}))))) | 432 | + :edited? (= "1" (irc/tag-value tags |
| 433 | + "+freeq.at/edited")) | ||
| 434 | + :reactions (reactions/parse-tally | ||
| 435 | + (irc/tag-value tags | ||
| 436 | + "+freeq.at/reactions"))}) | ||
| 437 | + (assoc-in [name :last-activity] at) | ||
| 438 | + settle))) | ||
| 476 | 439 | ||
| 477 | ;; A message that is only tags. A reaction is the one this reads: | 440 | ;; A message that is only tags. A reaction is the one this reads: |
| 478 | ;; `+react` puts an emoji on the message `+reply` names, and the | 441 | ;; `+react` puts an emoji on the message `+reply` names, and the |
| @@ -494,7 +457,9 @@ | |||
| 494 | ;; Whatever that message did to the list, write it down. Throttled, so a | 457 | ;; 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 | 458 | ;; 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 | 459 | ;; from someone new is a room that exists only here until this runs, and |
| 497 | - ;; the old client lost exactly those between runs. | 460 | + ;; the old client lost exactly those between runs. The read marker moves |
| 461 | + ;; on the same path, for the same reason: a line arriving in the room on | ||
| 462 | + ;; screen is a line read. | ||
| 498 | (remember-rooms!))) | 463 | (remember-rooms!))) |
| 499 | 464 | ||
| 500 | (defn- note! [m] | 465 | (defn- note! [m] |
modified
src/frq/state.clj +11 -160 | @@ -182,7 +182,7 @@ | ||
| 182 | 182 | (swap! jump-tick inc)) |
| 183 | 183 | ;; A counter rather than a clock: the list only needs their order, and a |
| 184 | 184 | ;; monotonic tick cannot be surprised by the system time moving. |
| 185 | -(defonce access-tick (atom 0)) | |
| 185 | +(def access-tick rooms/access-tick) | |
| 186 | 186 | |
| 187 | 187 | (def hide-join-part? cells/hide-join-part?) |
| 188 | 188 | |
| @@ -253,98 +253,13 @@ | ||
| 253 | 253 | (str/starts-with? s "#") s |
| 254 | 254 | :else (str "#" s)))) |
| 255 | 255 | |
| 256 | -(defn- after-marker | |
| 257 | - "The messages in `buffer` the reader has not seen: everything after its read | |
| 258 | - marker. | |
| 259 | - | |
| 260 | - By id where the marked message is still held, and by time otherwise. The id | |
| 261 | - is the exact answer — a msgid survives every revision, so it names the same | |
| 262 | - line however often the server replays it — and the timestamp is what answers | |
| 263 | - when the marked line has fallen off the end of the buffer or was never in | |
| 264 | - this run's copy of it. | |
| 265 | - | |
| 266 | - Derived rather than counted, because a count cannot survive what the server | |
| 267 | - does: a JOIN replays the backlog and CHATHISTORY replays it again, and every | |
| 268 | - line of it would tick a counter a second time. Against a marker a replayed | |
| 269 | - line is simply older than it and counts for nothing." | |
| 270 | - [buffer] | |
| 271 | - (let [id (:last-read-id buffer) | |
| 272 | - at (:last-read-at buffer 0) | |
| 273 | - msgs (vec (:messages buffer))] | |
| 274 | - (if (and id (some #(= id (:id %)) msgs)) | |
| 275 | - (vec (rest (drop-while #(not= id (:id %)) msgs))) | |
| 276 | - (filterv #(> (:at % 0) at) msgs)))) | |
| 277 | - | |
| 278 | -(defn- mentions-me? | |
| 279 | - "Whether a line is addressed at the reader by name. Our own lines do not | |
| 280 | - count — saying your own nick is not being called." | |
| 281 | - [m] | |
| 282 | - (let [me (str/trim (or @form-nick ""))] | |
| 283 | - (and (seq me) | |
| 284 | - (not= (:from m) me) | |
| 285 | - (str/includes? (str/lower-case (or (:text m) "")) | |
| 286 | - (str/lower-case me))))) | |
| 287 | - | |
| 288 | -(defn- recount | |
| 289 | - "Answer what the marker says: how many lines are unseen, and whether any of | |
| 290 | - them names the reader." | |
| 291 | - [buffer] | |
| 292 | - ;; Joins, parts, quits and "Joined #room" are the room talking about itself, | |
| 293 | - ;; not somebody talking in it. They arrive stamped now — the join notice is | |
| 294 | - ;; written the moment we are in — so counted, every room you are a member of | |
| 295 | - ;; sits at one unread from the moment it opens, saying only that you joined | |
| 296 | - ;; it. The marker still moves past them: they are read, they are just never | |
| 297 | - ;; what made a room worth looking at. | |
| 298 | - (let [fresh (remove :system? (after-marker buffer))] | |
| 299 | - (assoc buffer | |
| 300 | - :unread (count fresh) | |
| 301 | - :mention? (boolean (some mentions-me? fresh))))) | |
| 302 | - | |
| 303 | -(defn- mark-read | |
| 304 | - "Move the marker to the newest line this buffer holds. Both halves: the id | |
| 305 | - for as long as that line is here, and its time for after it is gone. | |
| 306 | - | |
| 307 | - The time only ever goes forward. A backlog can arrive after the reader has | |
| 308 | - already read past it, and taking the last line's time unconditionally would | |
| 309 | - walk the marker backwards and re-unread what was read." | |
| 310 | - [buffer] | |
| 311 | - (let [newest (last (:messages buffer))] | |
| 312 | - (assoc buffer | |
| 313 | - :unread 0 | |
| 314 | - :mention? false | |
| 315 | - :last-read-id (:id newest) | |
| 316 | - :last-read-at (max (:last-read-at buffer 0) (:at newest 0))))) | |
| 317 | - | |
| 318 | -;; How far back a room nobody has seen before counts as already read. | |
| 319 | -;; | |
| 320 | -;; A room being joined for the first time replays its whole history, and none | |
| 321 | -;; of that is news — the reader was not away for it, they were not here. So a | |
| 322 | -;; new buffer starts caught up rather than at the beginning, or joining a busy | |
| 323 | -;; channel announces a hundred unread posts from before you arrived. | |
| 324 | -;; | |
| 325 | -;; Caught up to a minute ago rather than to this instant, because a live line | |
| 326 | -;; is timestamped by the server and this by our clock: the two disagree by | |
| 327 | -;; whatever the skew is, and a live message stamped a few seconds behind us | |
| 328 | -;; would land under the marker and never be counted. A minute is more skew than | |
| 329 | -;; there will be and far less than the age of any backlog, and what it costs is | |
| 330 | -;; that a message sent in the minute before you joined counts as unread — which | |
| 331 | -;; is the harmless direction. | |
| 332 | -(def ^:private fresh-room-grace-ms 60000) | |
| 333 | - | |
| 334 | -(defn- ensure-channel [m name] | |
| 335 | - (if (contains? m name) | |
| 336 | - m | |
| 337 | - (assoc m name {:name name :messages [] :unread 0 | |
| 338 | - :joined? false :joining? false :accessed 0 | |
| 339 | - ;; What has been seen, and what the count is derived from. | |
| 340 | - ;; `:unread` and `:mention?` are answers, not records — see | |
| 341 | - ;; `recount`. | |
| 342 | - :last-read-id nil :mention? false | |
| 343 | - :last-read-at (max 0 (- (clock/now-ms) fresh-room-grace-ms)) | |
| 344 | - :kind (if (dm? name) :dm :channel) | |
| 345 | - :peer-did nil :last-activity 0 | |
| 346 | - ;; nick -> mode prefix, for the people panel | |
| 347 | - :users {}}))) | |
| 256 | +(def ^:private after-marker rooms/after-marker) | |
| 257 | + | |
| 258 | +(def ^:private recount rooms/recount) | |
| 259 | + | |
| 260 | +(def ^:private mark-read rooms/mark-read) | |
| 261 | + | |
| 262 | +(def ^:private ensure-channel rooms/ensure-channel) | |
| 348 | 263 | |
| 349 | 264 | (defonce ^{:doc "Bumped whenever a fetched image becomes available, so the |
| 350 | 265 | chat view re-renders without every message row watching the media cache."} |
| @@ -1632,73 +1547,9 @@ | ||
| 1632 | 1547 | (sort-by #(- (:accessed % 0))) |
| 1633 | 1548 | (mapv :name))) |
| 1634 | 1549 | |
| 1635 | -(defn room-records | |
| 1636 | - "The rooms as they go to disk: what each one is, when it last said anything, | |
| 1637 | - and how far into it the reader has got. | |
| 1638 | - | |
| 1639 | - Every room, not only the ones that have been opened. Being in a channel is | |
| 1640 | - what makes it yours; opening it only says which you looked at last, and that | |
| 1641 | - is what `:accessed` orders them by. Writing down the opened ones alone is how | |
| 1642 | - a client in a dozen channels came back knowing one — the rest were left for | |
| 1643 | - the server to remember, which is the thing it does not do. | |
| 1644 | - | |
| 1645 | - `:unread` and `:mention?` are not written. They are what the marker adds up | |
| 1646 | - to against the messages in hand, and a count written down is a count that can | |
| 1647 | - be wrong — the marker cannot be. `:mention?` rides along all the same, as the | |
| 1648 | - one thing that cannot be recomputed before the history it was derived from | |
| 1649 | - comes back: a room that had your name in it says so on the next run's first | |
| 1650 | - frame rather than a round trip later, and is corrected by `recount` the | |
| 1651 | - moment the backlog lands." | |
| 1652 | - [] | |
| 1653 | - (->> (vals @channels) | |
| 1654 | - (sort-by #(- (:accessed % 0))) | |
| 1655 | - (mapv #(select-keys % [:name :kind :peer-did :last-activity | |
| 1656 | - :last-read-id :last-read-at :mention?])))) | |
| 1657 | - | |
| 1658 | -(defn restore-channels! | |
| 1659 | - "Bring back the rooms of earlier runs, in the order they were last used, each | |
| 1660 | - with the marker saying how much of it had been read. | |
| 1661 | - | |
| 1662 | - Empty buffers, not memberships: opening one is what joins it, and a list of | |
| 1663 | - rooms is the part worth keeping — the messages in them come from the server. | |
| 1664 | - The tick is seeded so this run's first open still sorts above all of them. | |
| 1665 | - | |
| 1666 | - The marker is what makes the returning backlog readable. Without one every | |
| 1667 | - replayed line is new and every room comes back with its whole history | |
| 1668 | - unread; with one, the reader is put back where they were and only what | |
| 1669 | - arrived while they were away is counted. A room migrated from an older frq | |
| 1670 | - has no marker and is caught up as if read, which is the kinder of the two | |
| 1671 | - wrong answers — the alternative announces a hundred unread lines the reader | |
| 1672 | - has already seen." | |
| 1673 | - [] | |
| 1674 | - (when-let [saved (seq (store/load-rooms))] | |
| 1675 | - (let [ordered (reverse saved)] ; oldest first, so ticks ascend | |
| 1676 | - (swap! channels | |
| 1677 | - (fn [m] | |
| 1678 | - (reduce (fn [acc room] | |
| 1679 | - (let [name (:name room)] | |
| 1680 | - (if (contains? acc name) | |
| 1681 | - acc | |
| 1682 | - (assoc acc name | |
| 1683 | - {:name name :messages [] :unread 0 | |
| 1684 | - :joined? false :joining? false | |
| 1685 | - :users {} | |
| 1686 | - :kind (or (:kind room) | |
| 1687 | - (if (dm? name) :dm :channel)) | |
| 1688 | - :peer-did (:peer-did room) | |
| 1689 | - :last-activity (:last-activity room 0) | |
| 1690 | - :last-read-id (:last-read-id room) | |
| 1691 | - ;; No marker at all — an older frq's list, | |
| 1692 | - ;; or a record that lost it. Read up to | |
| 1693 | - ;; now rather than back to the beginning. | |
| 1694 | - :last-read-at (:last-read-at room | |
| 1695 | - (clock/now-ms)) | |
| 1696 | - :mention? (boolean (:mention? room)) | |
| 1697 | - :accessed (swap! access-tick inc)}))) | |
| 1698 | - ) | |
| 1699 | - m | |
| 1700 | - ordered)))) | |
| 1701 | - (count saved))) | |
| 1550 | +(def room-records rooms/room-records) | |
| 1551 | + | |
| 1552 | +(def restore-channels! rooms/restore-channels!) | |
| 1702 | 1553 | |
| 1703 | 1554 | (defn message-by-id |
| 1704 | 1555 | "The message a reply points at, if this buffer still holds it. |
| @@ -182,7 +182,7 @@ | |||
| 182 | (swap! jump-tick inc)) | 182 | (swap! jump-tick inc)) |
| 183 | ;; A counter rather than a clock: the list only needs their order, and a | 183 | ;; A counter rather than a clock: the list only needs their order, and a |
| 184 | ;; monotonic tick cannot be surprised by the system time moving. | 184 | ;; monotonic tick cannot be surprised by the system time moving. |
| 185 | -(defonce access-tick (atom 0)) | 185 | +(def access-tick rooms/access-tick) |
| 186 | 186 | ||
| 187 | (def hide-join-part? cells/hide-join-part?) | 187 | (def hide-join-part? cells/hide-join-part?) |
| 188 | 188 | ||
| @@ -253,98 +253,13 @@ | |||
| 253 | (str/starts-with? s "#") s | 253 | (str/starts-with? s "#") s |
| 254 | :else (str "#" s)))) | 254 | :else (str "#" s)))) |
| 255 | 255 | ||
| 256 | -(defn- after-marker | 256 | +(def ^:private after-marker rooms/after-marker) |
| 257 | - "The messages in `buffer` the reader has not seen: everything after its read | 257 | + |
| 258 | - marker. | 258 | +(def ^:private recount rooms/recount) |
| 259 | - | 259 | + |
| 260 | - By id where the marked message is still held, and by time otherwise. The id | 260 | +(def ^:private mark-read rooms/mark-read) |
| 261 | - is the exact answer — a msgid survives every revision, so it names the same | 261 | + |
| 262 | - line however often the server replays it — and the timestamp is what answers | 262 | +(def ^:private ensure-channel rooms/ensure-channel) |
| 263 | - when the marked line has fallen off the end of the buffer or was never in | ||
| 264 | - this run's copy of it. | ||
| 265 | - | ||
| 266 | - Derived rather than counted, because a count cannot survive what the server | ||
| 267 | - does: a JOIN replays the backlog and CHATHISTORY replays it again, and every | ||
| 268 | - line of it would tick a counter a second time. Against a marker a replayed | ||
| 269 | - line is simply older than it and counts for nothing." | ||
| 270 | - [buffer] | ||
| 271 | - (let [id (:last-read-id buffer) | ||
| 272 | - at (:last-read-at buffer 0) | ||
| 273 | - msgs (vec (:messages buffer))] | ||
| 274 | - (if (and id (some #(= id (:id %)) msgs)) | ||
| 275 | - (vec (rest (drop-while #(not= id (:id %)) msgs))) | ||
| 276 | - (filterv #(> (:at % 0) at) msgs)))) | ||
| 277 | - | ||
| 278 | -(defn- mentions-me? | ||
| 279 | - "Whether a line is addressed at the reader by name. Our own lines do not | ||
| 280 | - count — saying your own nick is not being called." | ||
| 281 | - [m] | ||
| 282 | - (let [me (str/trim (or @form-nick ""))] | ||
| 283 | - (and (seq me) | ||
| 284 | - (not= (:from m) me) | ||
| 285 | - (str/includes? (str/lower-case (or (:text m) "")) | ||
| 286 | - (str/lower-case me))))) | ||
| 287 | - | ||
| 288 | -(defn- recount | ||
| 289 | - "Answer what the marker says: how many lines are unseen, and whether any of | ||
| 290 | - them names the reader." | ||
| 291 | - [buffer] | ||
| 292 | - ;; Joins, parts, quits and "Joined #room" are the room talking about itself, | ||
| 293 | - ;; not somebody talking in it. They arrive stamped now — the join notice is | ||
| 294 | - ;; written the moment we are in — so counted, every room you are a member of | ||
| 295 | - ;; sits at one unread from the moment it opens, saying only that you joined | ||
| 296 | - ;; it. The marker still moves past them: they are read, they are just never | ||
| 297 | - ;; what made a room worth looking at. | ||
| 298 | - (let [fresh (remove :system? (after-marker buffer))] | ||
| 299 | - (assoc buffer | ||
| 300 | - :unread (count fresh) | ||
| 301 | - :mention? (boolean (some mentions-me? fresh))))) | ||
| 302 | - | ||
| 303 | -(defn- mark-read | ||
| 304 | - "Move the marker to the newest line this buffer holds. Both halves: the id | ||
| 305 | - for as long as that line is here, and its time for after it is gone. | ||
| 306 | - | ||
| 307 | - The time only ever goes forward. A backlog can arrive after the reader has | ||
| 308 | - already read past it, and taking the last line's time unconditionally would | ||
| 309 | - walk the marker backwards and re-unread what was read." | ||
| 310 | - [buffer] | ||
| 311 | - (let [newest (last (:messages buffer))] | ||
| 312 | - (assoc buffer | ||
| 313 | - :unread 0 | ||
| 314 | - :mention? false | ||
| 315 | - :last-read-id (:id newest) | ||
| 316 | - :last-read-at (max (:last-read-at buffer 0) (:at newest 0))))) | ||
| 317 | - | ||
| 318 | -;; How far back a room nobody has seen before counts as already read. | ||
| 319 | -;; | ||
| 320 | -;; A room being joined for the first time replays its whole history, and none | ||
| 321 | -;; of that is news — the reader was not away for it, they were not here. So a | ||
| 322 | -;; new buffer starts caught up rather than at the beginning, or joining a busy | ||
| 323 | -;; channel announces a hundred unread posts from before you arrived. | ||
| 324 | -;; | ||
| 325 | -;; Caught up to a minute ago rather than to this instant, because a live line | ||
| 326 | -;; is timestamped by the server and this by our clock: the two disagree by | ||
| 327 | -;; whatever the skew is, and a live message stamped a few seconds behind us | ||
| 328 | -;; would land under the marker and never be counted. A minute is more skew than | ||
| 329 | -;; there will be and far less than the age of any backlog, and what it costs is | ||
| 330 | -;; that a message sent in the minute before you joined counts as unread — which | ||
| 331 | -;; is the harmless direction. | ||
| 332 | -(def ^:private fresh-room-grace-ms 60000) | ||
| 333 | - | ||
| 334 | -(defn- ensure-channel [m name] | ||
| 335 | - (if (contains? m name) | ||
| 336 | - m | ||
| 337 | - (assoc m name {:name name :messages [] :unread 0 | ||
| 338 | - :joined? false :joining? false :accessed 0 | ||
| 339 | - ;; What has been seen, and what the count is derived from. | ||
| 340 | - ;; `:unread` and `:mention?` are answers, not records — see | ||
| 341 | - ;; `recount`. | ||
| 342 | - :last-read-id nil :mention? false | ||
| 343 | - :last-read-at (max 0 (- (clock/now-ms) fresh-room-grace-ms)) | ||
| 344 | - :kind (if (dm? name) :dm :channel) | ||
| 345 | - :peer-did nil :last-activity 0 | ||
| 346 | - ;; nick -> mode prefix, for the people panel | ||
| 347 | - :users {}}))) | ||
| 348 | 263 | ||
| 349 | (defonce ^{:doc "Bumped whenever a fetched image becomes available, so the | 264 | (defonce ^{:doc "Bumped whenever a fetched image becomes available, so the |
| 350 | chat view re-renders without every message row watching the media cache."} | 265 | chat view re-renders without every message row watching the media cache."} |
| @@ -1632,73 +1547,9 @@ | |||
| 1632 | (sort-by #(- (:accessed % 0))) | 1547 | (sort-by #(- (:accessed % 0))) |
| 1633 | (mapv :name))) | 1548 | (mapv :name))) |
| 1634 | 1549 | ||
| 1635 | -(defn room-records | 1550 | +(def room-records rooms/room-records) |
| 1636 | - "The rooms as they go to disk: what each one is, when it last said anything, | 1551 | + |
| 1637 | - and how far into it the reader has got. | 1552 | +(def restore-channels! rooms/restore-channels!) |
| 1638 | - | ||
| 1639 | - Every room, not only the ones that have been opened. Being in a channel is | ||
| 1640 | - what makes it yours; opening it only says which you looked at last, and that | ||
| 1641 | - is what `:accessed` orders them by. Writing down the opened ones alone is how | ||
| 1642 | - a client in a dozen channels came back knowing one — the rest were left for | ||
| 1643 | - the server to remember, which is the thing it does not do. | ||
| 1644 | - | ||
| 1645 | - `:unread` and `:mention?` are not written. They are what the marker adds up | ||
| 1646 | - to against the messages in hand, and a count written down is a count that can | ||
| 1647 | - be wrong — the marker cannot be. `:mention?` rides along all the same, as the | ||
| 1648 | - one thing that cannot be recomputed before the history it was derived from | ||
| 1649 | - comes back: a room that had your name in it says so on the next run's first | ||
| 1650 | - frame rather than a round trip later, and is corrected by `recount` the | ||
| 1651 | - moment the backlog lands." | ||
| 1652 | - [] | ||
| 1653 | - (->> (vals @channels) | ||
| 1654 | - (sort-by #(- (:accessed % 0))) | ||
| 1655 | - (mapv #(select-keys % [:name :kind :peer-did :last-activity | ||
| 1656 | - :last-read-id :last-read-at :mention?])))) | ||
| 1657 | - | ||
| 1658 | -(defn restore-channels! | ||
| 1659 | - "Bring back the rooms of earlier runs, in the order they were last used, each | ||
| 1660 | - with the marker saying how much of it had been read. | ||
| 1661 | - | ||
| 1662 | - Empty buffers, not memberships: opening one is what joins it, and a list of | ||
| 1663 | - rooms is the part worth keeping — the messages in them come from the server. | ||
| 1664 | - The tick is seeded so this run's first open still sorts above all of them. | ||
| 1665 | - | ||
| 1666 | - The marker is what makes the returning backlog readable. Without one every | ||
| 1667 | - replayed line is new and every room comes back with its whole history | ||
| 1668 | - unread; with one, the reader is put back where they were and only what | ||
| 1669 | - arrived while they were away is counted. A room migrated from an older frq | ||
| 1670 | - has no marker and is caught up as if read, which is the kinder of the two | ||
| 1671 | - wrong answers — the alternative announces a hundred unread lines the reader | ||
| 1672 | - has already seen." | ||
| 1673 | - [] | ||
| 1674 | - (when-let [saved (seq (store/load-rooms))] | ||
| 1675 | - (let [ordered (reverse saved)] ; oldest first, so ticks ascend | ||
| 1676 | - (swap! channels | ||
| 1677 | - (fn [m] | ||
| 1678 | - (reduce (fn [acc room] | ||
| 1679 | - (let [name (:name room)] | ||
| 1680 | - (if (contains? acc name) | ||
| 1681 | - acc | ||
| 1682 | - (assoc acc name | ||
| 1683 | - {:name name :messages [] :unread 0 | ||
| 1684 | - :joined? false :joining? false | ||
| 1685 | - :users {} | ||
| 1686 | - :kind (or (:kind room) | ||
| 1687 | - (if (dm? name) :dm :channel)) | ||
| 1688 | - :peer-did (:peer-did room) | ||
| 1689 | - :last-activity (:last-activity room 0) | ||
| 1690 | - :last-read-id (:last-read-id room) | ||
| 1691 | - ;; No marker at all — an older frq's list, | ||
| 1692 | - ;; or a record that lost it. Read up to | ||
| 1693 | - ;; now rather than back to the beginning. | ||
| 1694 | - :last-read-at (:last-read-at room | ||
| 1695 | - (clock/now-ms)) | ||
| 1696 | - :mention? (boolean (:mention? room)) | ||
| 1697 | - :accessed (swap! access-tick inc)}))) | ||
| 1698 | - ) | ||
| 1699 | - m | ||
| 1700 | - ordered)))) | ||
| 1701 | - (count saved))) | ||
| 1702 | 1553 | ||
| 1703 | (defn message-by-id | 1554 | (defn message-by-id |
| 1704 | "The message a reply points at, if this buffer still holds it. | 1555 | "The message a reply points at, if this buffer still holds it. |