nandi/frqpublic Fork 0
1d062fd
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.

Remember which rooms this client has been in

The chats list already put the most recently opened channel on top, by a tick
that only counted within a run — so every launch started from nothing. The
order is written to $XDG_CONFIG_HOME/frq/channels.edn now, and read back
before the window opens.

Names only, and only channels actually opened. The messages come from the
server, membership is the connection's business, and a DM that arrived once is
not a place this client has been. Restored buffers are empty and unjoined:
opening one is still what joins it.

The tick is seeded from the restored order rather than reset, so the first
channel opened in a new run sorts above everything carried over instead of
tying with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T11:16:15-07:00 Browse files
1d062fd parent: 9237cf4
modified README.md +2 -0
@@ -108,6 +108,8 @@ surface — that surface does not work on Android either, while the syscalls do.
108108 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
109109 * Join/part notices, DMs bucketed under the sender's nick
110110 * Discover list, search over buffers, disconnect
111+* The rooms you have opened, remembered across runs and listed in the order
112+ you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
111113 * Conversations listed most recently opened first
112114
113115 ## Limits
@@ -108,6 +108,8 @@ surface — that surface does not work on Android either, while the syscalls do.
108 `$XDG_CACHE_HOME/frq/media`; click one to see it full size108 `$XDG_CACHE_HOME/frq/media`; click one to see it full size
109 * Join/part notices, DMs bucketed under the sender's nick109 * Join/part notices, DMs bucketed under the sender's nick
110 * Discover list, search over buffers, disconnect110 * Discover list, search over buffers, disconnect
111+* The rooms you have opened, remembered across runs and listed in the order
112+ you last used them (`$XDG_CONFIG_HOME/frq/channels.edn`)
111 * Conversations listed most recently opened first113 * Conversations listed most recently opened first
112 114
113 ## Limits115 ## Limits
modified src/frq/app.jolt +3 -2
@@ -365,8 +365,9 @@
365365 [chats-screen])))
366366
367367 (defn -main [& _]
368- ;; Before the window: a saved sign-in decides which mode the connect screen
369- ;; opens in, and what it says.
368+ ;; Before the window: the rooms this client has been in, and a saved sign-in
369+ ;; deciding which mode the connect screen opens in and what it says.
370+ (s/restore-channels!)
370371 (when (s/restore-session!)
371372 ;; And then it connects on its own. A remembered account has already said
372373 ;; what it wants; making it say so again at every launch is a click that
@@ -365,8 +365,9 @@
365 [chats-screen])))365 [chats-screen])))
366 366
367 (defn -main [& _]367 (defn -main [& _]
368- ;; Before the window: a saved sign-in decides which mode the connect screen368+ ;; Before the window: the rooms this client has been in, and a saved sign-in
369- ;; opens in, and what it says.369+ ;; deciding which mode the connect screen opens in and what it says.
370+ (s/restore-channels!)
370 (when (s/restore-session!)371 (when (s/restore-session!)
371 ;; And then it connects on its own. A remembered account has already said372 ;; And then it connects on its own. A remembered account has already said
372 ;; what it wants; making it say so again at every launch is a click that373 ;; what it wants; making it say so again at every launch is a click that
modified src/frq/state.jolt +41 -0
@@ -71,6 +71,15 @@
7171
7272 (defn connected? [] (some? @conn))
7373
74+(declare channel-order)
75+
76+(defn- remember-channels!
77+ "Write the order out. Off the caller's thread: opening a channel should not
78+ wait on a file, and the order it records is read from the same atom either
79+ way."
80+ []
81+ (future (store/save-channels! (channel-order))))
82+
7483 (defn normalize-channel [s]
7584 (let [s (str/trim (or s ""))]
7685 (cond (str/blank? s) ""
@@ -114,6 +123,7 @@
114123 (swap! channels #(-> (ensure-channel % name)
115124 (assoc-in [name :unread] 0)
116125 (assoc-in [name :accessed] (swap! access-tick inc))))
126+ (remember-channels!)
117127 ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a
118128 ;; second JOIN sent in the meantime is what makes the server replay nothing.
119129 (let [buffer (get @channels name)]
@@ -401,6 +411,37 @@
401411 (sort-by (juxt #(- (:accessed % 0)) :name))
402412 vec)))
403413
414+(defn channel-order
415+ "Just the channel names, most recently opened first — what gets written to
416+ disk. Buffers never opened are left out: a DM that arrived once is not a
417+ place this client has been."
418+ []
419+ (->> (vals @channels)
420+ (filter #(and (str/starts-with? (:name %) "#") (pos? (:accessed % 0))))
421+ (sort-by #(- (:accessed % 0)))
422+ (mapv :name)))
423+
424+(defn restore-channels!
425+ "Bring back the channels of earlier runs, in the order they were last used.
426+
427+ Empty buffers, not memberships: opening one is what joins it, and a list of
428+ rooms is the part worth keeping the messages in them come from the server.
429+ The tick is seeded so this run's first open still sorts above all of them."
430+ []
431+ (when-let [saved (seq (store/load-channels))]
432+ (let [ordered (reverse saved)] ; oldest first, so ticks ascend
433+ (swap! channels
434+ (fn [m]
435+ (reduce (fn [acc name]
436+ (if (contains? acc name)
437+ acc
438+ (assoc acc name {:name name :messages [] :unread 0
439+ :joined? false :joining? false
440+ :accessed (swap! access-tick inc)})))
441+ m
442+ ordered))))
443+ (count saved)))
444+
404445 (defn last-preview [buffer]
405446 (if-let [m (last (:messages buffer))]
406447 (str (:from m) ": " (:text m))
@@ -71,6 +71,15 @@
71 71
72 (defn connected? [] (some? @conn))72 (defn connected? [] (some? @conn))
73 73
74+(declare channel-order)
75+
76+(defn- remember-channels!
77+ "Write the order out. Off the caller's thread: opening a channel should not
78+ wait on a file, and the order it records is read from the same atom either
79+ way."
80+ []
81+ (future (store/save-channels! (channel-order))))
82+
74 (defn normalize-channel [s]83 (defn normalize-channel [s]
75 (let [s (str/trim (or s ""))]84 (let [s (str/trim (or s ""))]
76 (cond (str/blank? s) ""85 (cond (str/blank? s) ""
@@ -114,6 +123,7 @@
114 (swap! channels #(-> (ensure-channel % name)123 (swap! channels #(-> (ensure-channel % name)
115 (assoc-in [name :unread] 0)124 (assoc-in [name :unread] 0)
116 (assoc-in [name :accessed] (swap! access-tick inc))))125 (assoc-in [name :accessed] (swap! access-tick inc))))
126+ (remember-channels!)
117 ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a127 ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a
118 ;; second JOIN sent in the meantime is what makes the server replay nothing.128 ;; second JOIN sent in the meantime is what makes the server replay nothing.
119 (let [buffer (get @channels name)]129 (let [buffer (get @channels name)]
@@ -401,6 +411,37 @@
401 (sort-by (juxt #(- (:accessed % 0)) :name))411 (sort-by (juxt #(- (:accessed % 0)) :name))
402 vec)))412 vec)))
403 413
414+(defn channel-order
415+ "Just the channel names, most recently opened first — what gets written to
416+ disk. Buffers never opened are left out: a DM that arrived once is not a
417+ place this client has been."
418+ []
419+ (->> (vals @channels)
420+ (filter #(and (str/starts-with? (:name %) "#") (pos? (:accessed % 0))))
421+ (sort-by #(- (:accessed % 0)))
422+ (mapv :name)))
423+
424+(defn restore-channels!
425+ "Bring back the channels of earlier runs, in the order they were last used.
426+
427+ Empty buffers, not memberships: opening one is what joins it, and a list of
428+ rooms is the part worth keeping the messages in them come from the server.
429+ The tick is seeded so this run's first open still sorts above all of them."
430+ []
431+ (when-let [saved (seq (store/load-channels))]
432+ (let [ordered (reverse saved)] ; oldest first, so ticks ascend
433+ (swap! channels
434+ (fn [m]
435+ (reduce (fn [acc name]
436+ (if (contains? acc name)
437+ acc
438+ (assoc acc name {:name name :messages [] :unread 0
439+ :joined? false :joining? false
440+ :accessed (swap! access-tick inc)})))
441+ m
442+ ordered))))
443+ (count saved)))
444+
404 (defn last-preview [buffer]445 (defn last-preview [buffer]
405 (if-let [m (last (:messages buffer))]446 (if-let [m (last (:messages buffer))]
406 (str (:from m) ": " (:text m))447 (str (:from m) ": " (:text m))
modified src/frq/store.jolt +23 -0
@@ -42,6 +42,29 @@
4242 true
4343 (catch Exception _ false))))
4444
45+(defn channels-file [] (str (config-dir) "/channels.edn"))
46+
47+(defn load-channels
48+ "The channels this client has opened, in the order it last used them. Unlike
49+ the session beside it this is not a credential just names so it is an
50+ ordinary file."
51+ []
52+ (let [path (channels-file)]
53+ (when (host/file-exists? path)
54+ (try
55+ (let [v (edn/read-string (slurp path))]
56+ (when (vector? v) (filterv string? v)))
57+ (catch Exception _ nil)))))
58+
59+(defn save-channels!
60+ "Write the channel names, most recently used first."
61+ [names]
62+ (try
63+ (host/mkdirs! (config-dir))
64+ (spit (channels-file) (pr-str (vec names)))
65+ true
66+ (catch Exception _ false)))
67+
4568 (defn clear-session! []
4669 (try
4770 (when (host/file-exists? (session-file))
@@ -42,6 +42,29 @@
42 true42 true
43 (catch Exception _ false))))43 (catch Exception _ false))))
44 44
45+(defn channels-file [] (str (config-dir) "/channels.edn"))
46+
47+(defn load-channels
48+ "The channels this client has opened, in the order it last used them. Unlike
49+ the session beside it this is not a credential just names so it is an
50+ ordinary file."
51+ []
52+ (let [path (channels-file)]
53+ (when (host/file-exists? path)
54+ (try
55+ (let [v (edn/read-string (slurp path))]
56+ (when (vector? v) (filterv string? v)))
57+ (catch Exception _ nil)))))
58+
59+(defn save-channels!
60+ "Write the channel names, most recently used first."
61+ [names]
62+ (try
63+ (host/mkdirs! (config-dir))
64+ (spit (channels-file) (pr-str (vec names)))
65+ true
66+ (catch Exception _ false)))
67+
45 (defn clear-session! []68 (defn clear-session! []
46 (try69 (try
47 (when (host/file-exists? (session-file))70 (when (host/file-exists? (session-file))