nandi/frqpublic Fork 0
75e557e
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.

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>
nandi committed 2026-08-30T10:46:51-07:00 Browse files
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.
103103 * Join channels, channel buffers with unread counts, send and receive `PRIVMSG`
104104 * Join/part notices, DMs bucketed under the sender's nick
105105 * Discover list, search over buffers, disconnect
106+* Conversations listed most recently opened first
106107
107108 ## Limits
108109
@@ -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 nick104 * Join/part notices, DMs bucketed under the sender's nick
105 * Discover list, search over buffers, disconnect105 * Discover list, search over buffers, disconnect
106+* Conversations listed most recently opened first
106 107
107 ## Limits108 ## Limits
108 109
modified src/frq/state.jolt +15 -4
@@ -56,6 +56,9 @@
5656 (defonce current (atom nil))
5757 (defonce draft (atom ""))
5858 (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))
5962 (defonce search (atom ""))
6063
6164 (defn connected? [] (some? @conn))
@@ -69,7 +72,8 @@
6972 (defn- ensure-channel [m name]
7073 (if (contains? m name)
7174 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})))
7377
7478 (defn push-message!
7579 "Append a line to a buffer, creating it if needed, and bump the unread count
@@ -91,7 +95,9 @@
9195 [name]
9296 (reset! current name)
9397 (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))))
95101 ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a
96102 ;; second JOIN sent in the meantime is what makes the server replay nothing.
97103 (let [buffer (get @channels name)]
@@ -355,13 +361,18 @@
355361 (reset! draft ""))))
356362
357363 (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."
359370 []
360371 (let [q (str/lower-case (str/trim @search))]
361372 (->> (vals @channels)
362373 (filter #(or (str/blank? q)
363374 (str/includes? (str/lower-case (:name %)) q)))
364- (sort-by :name)
375+ (sort-by (juxt #(- (:accessed % 0)) :name))
365376 vec)))
366377
367378 (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 m74 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 count79 "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 a101 ;; `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-list363 (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]