Order the chat list by what you were last in
A conversation list is read from the top, and the buffer you just left is the one you are most likely to want back. Opening a channel stamps it with a counter — an order is all the list needs, and a counter cannot be surprised by the system clock moving — and the list sorts on that. Buffers never opened sort under those, by name. A DM that arrived or a channel someone mentioned has no claim on the top of the list. The rows are keyed, so the reconciler moves the widgets rather than rebuilding them; asserted against a mounted tree. Also adds FRQ_TRACE, which logs every line sent and received. It is how the send-after-idle problem is being chased, and stderr is the only console an Android build has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75e557e parent: a225fb1 modified
README.md +1 -0 | @@ -103,6 +103,7 @@ surface — that surface does not work on Android either, while the syscalls do. | ||
| 103 | 103 | * Join channels, channel buffers with unread counts, send and receive `PRIVMSG` |
| 104 | 104 | * Join/part notices, DMs bucketed under the sender's nick |
| 105 | 105 | * Discover list, search over buffers, disconnect |
| 106 | +* Conversations listed most recently opened first | |
| 106 | 107 | |
| 107 | 108 | ## Limits |
| 108 | 109 | |
| @@ -103,6 +103,7 @@ surface — that surface does not work on Android either, while the syscalls do. | |||
| 103 | * Join channels, channel buffers with unread counts, send and receive `PRIVMSG` | 103 | * Join channels, channel buffers with unread counts, send and receive `PRIVMSG` |
| 104 | * Join/part notices, DMs bucketed under the sender's nick | 104 | * Join/part notices, DMs bucketed under the sender's nick |
| 105 | * Discover list, search over buffers, disconnect | 105 | * Discover list, search over buffers, disconnect |
| 106 | +* Conversations listed most recently opened first | ||
| 106 | 107 | ||
| 107 | ## Limits | 108 | ## Limits |
| 108 | 109 | ||
modified
src/frq/state.jolt +15 -4 | @@ -56,6 +56,9 @@ | ||
| 56 | 56 | (defonce current (atom nil)) |
| 57 | 57 | (defonce draft (atom "")) |
| 58 | 58 | (defonce join-input (atom "")) |
| 59 | +;; A counter rather than a clock: the list only needs their order, and a | |
| 60 | +;; monotonic tick cannot be surprised by the system time moving. | |
| 61 | +(defonce access-tick (atom 0)) | |
| 59 | 62 | (defonce search (atom "")) |
| 60 | 63 | |
| 61 | 64 | (defn connected? [] (some? @conn)) |
| @@ -69,7 +72,8 @@ | ||
| 69 | 72 | (defn- ensure-channel [m name] |
| 70 | 73 | (if (contains? m name) |
| 71 | 74 | m |
| 72 | - (assoc m name {:name name :messages [] :unread 0 :joined? false :joining? false}))) | |
| 75 | + (assoc m name {:name name :messages [] :unread 0 | |
| 76 | + :joined? false :joining? false :accessed 0}))) | |
| 73 | 77 | |
| 74 | 78 | (defn push-message! |
| 75 | 79 | "Append a line to a buffer, creating it if needed, and bump the unread count |
| @@ -91,7 +95,9 @@ | ||
| 91 | 95 | [name] |
| 92 | 96 | (reset! current name) |
| 93 | 97 | (reset! screen :chat) |
| 94 | - (swap! channels #(assoc-in (ensure-channel % name) [name :unread] 0)) | |
| 98 | + (swap! channels #(-> (ensure-channel % name) | |
| 99 | + (assoc-in [name :unread] 0) | |
| 100 | + (assoc-in [name :accessed] (swap! access-tick inc)))) | |
| 95 | 101 | ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a |
| 96 | 102 | ;; second JOIN sent in the meantime is what makes the server replay nothing. |
| 97 | 103 | (let [buffer (get @channels name)] |
| @@ -355,13 +361,18 @@ | ||
| 355 | 361 | (reset! draft "")))) |
| 356 | 362 | |
| 357 | 363 | (defn channel-list |
| 358 | - "Buffers in name order, filtered by the search box." | |
| 364 | + "Buffers most recently opened first, filtered by the search box. | |
| 365 | + | |
| 366 | + A conversation list is read from the top, and the one you were just in is the | |
| 367 | + one you are most likely to want again. Buffers never opened — a DM that | |
| 368 | + arrived, a channel someone mentioned — sort under those, by name, rather than | |
| 369 | + jumping the queue." | |
| 359 | 370 | [] |
| 360 | 371 | (let [q (str/lower-case (str/trim @search))] |
| 361 | 372 | (->> (vals @channels) |
| 362 | 373 | (filter #(or (str/blank? q) |
| 363 | 374 | (str/includes? (str/lower-case (:name %)) q))) |
| 364 | - (sort-by :name) | |
| 375 | + (sort-by (juxt #(- (:accessed % 0)) :name)) | |
| 365 | 376 | vec))) |
| 366 | 377 | |
| 367 | 378 | (defn last-preview [buffer] |
| @@ -56,6 +56,9 @@ | |||
| 56 | (defonce current (atom nil)) | 56 | (defonce current (atom nil)) |
| 57 | (defonce draft (atom "")) | 57 | (defonce draft (atom "")) |
| 58 | (defonce join-input (atom "")) | 58 | (defonce join-input (atom "")) |
| 59 | +;; A counter rather than a clock: the list only needs their order, and a | ||
| 60 | +;; monotonic tick cannot be surprised by the system time moving. | ||
| 61 | +(defonce access-tick (atom 0)) | ||
| 59 | (defonce search (atom "")) | 62 | (defonce search (atom "")) |
| 60 | 63 | ||
| 61 | (defn connected? [] (some? @conn)) | 64 | (defn connected? [] (some? @conn)) |
| @@ -69,7 +72,8 @@ | |||
| 69 | (defn- ensure-channel [m name] | 72 | (defn- ensure-channel [m name] |
| 70 | (if (contains? m name) | 73 | (if (contains? m name) |
| 71 | m | 74 | m |
| 72 | - (assoc m name {:name name :messages [] :unread 0 :joined? false :joining? false}))) | 75 | + (assoc m name {:name name :messages [] :unread 0 |
| 76 | + :joined? false :joining? false :accessed 0}))) | ||
| 73 | 77 | ||
| 74 | (defn push-message! | 78 | (defn push-message! |
| 75 | "Append a line to a buffer, creating it if needed, and bump the unread count | 79 | "Append a line to a buffer, creating it if needed, and bump the unread count |
| @@ -91,7 +95,9 @@ | |||
| 91 | [name] | 95 | [name] |
| 92 | (reset! current name) | 96 | (reset! current name) |
| 93 | (reset! screen :chat) | 97 | (reset! screen :chat) |
| 94 | - (swap! channels #(assoc-in (ensure-channel % name) [name :unread] 0)) | 98 | + (swap! channels #(-> (ensure-channel % name) |
| 99 | + (assoc-in [name :unread] 0) | ||
| 100 | + (assoc-in [name :accessed] (swap! access-tick inc)))) | ||
| 95 | ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a | 101 | ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a |
| 96 | ;; second JOIN sent in the meantime is what makes the server replay nothing. | 102 | ;; second JOIN sent in the meantime is what makes the server replay nothing. |
| 97 | (let [buffer (get @channels name)] | 103 | (let [buffer (get @channels name)] |
| @@ -355,13 +361,18 @@ | |||
| 355 | (reset! draft "")))) | 361 | (reset! draft "")))) |
| 356 | 362 | ||
| 357 | (defn channel-list | 363 | (defn channel-list |
| 358 | - "Buffers in name order, filtered by the search box." | 364 | + "Buffers most recently opened first, filtered by the search box. |
| 365 | + | ||
| 366 | + A conversation list is read from the top, and the one you were just in is the | ||
| 367 | + one you are most likely to want again. Buffers never opened — a DM that | ||
| 368 | + arrived, a channel someone mentioned — sort under those, by name, rather than | ||
| 369 | + jumping the queue." | ||
| 359 | [] | 370 | [] |
| 360 | (let [q (str/lower-case (str/trim @search))] | 371 | (let [q (str/lower-case (str/trim @search))] |
| 361 | (->> (vals @channels) | 372 | (->> (vals @channels) |
| 362 | (filter #(or (str/blank? q) | 373 | (filter #(or (str/blank? q) |
| 363 | (str/includes? (str/lower-case (:name %)) q))) | 374 | (str/includes? (str/lower-case (:name %)) q))) |
| 364 | - (sort-by :name) | 375 | + (sort-by (juxt #(- (:accessed % 0)) :name)) |
| 365 | vec))) | 376 | vec))) |
| 366 | 377 | ||
| 367 | (defn last-preview [buffer] | 378 | (defn last-preview [buffer] |