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>
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. | ||
| 108 | 108 | `$XDG_CACHE_HOME/frq/media`; click one to see it full size |
| 109 | 109 | * Join/part notices, DMs bucketed under the sender's nick |
| 110 | 110 | * 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 | 113 | * Conversations listed most recently opened first |
| 112 | 114 | |
| 113 | 115 | ## 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 size | 108 | `$XDG_CACHE_HOME/frq/media`; click one to see it full size |
| 109 | * Join/part notices, DMs bucketed under the sender's nick | 109 | * Join/part notices, DMs bucketed under the sender's nick |
| 110 | * Discover list, search over buffers, disconnect | 110 | * 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 first | 113 | * Conversations listed most recently opened first |
| 112 | 114 | ||
| 113 | ## Limits | 115 | ## Limits |
modified
src/frq/app.jolt +3 -2 | @@ -365,8 +365,9 @@ | ||
| 365 | 365 | [chats-screen]))) |
| 366 | 366 | |
| 367 | 367 | (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!) | |
| 370 | 371 | (when (s/restore-session!) |
| 371 | 372 | ;; And then it connects on its own. A remembered account has already said |
| 372 | 373 | ;; 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 screen | 368 | + ;; 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 said | 372 | ;; 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 that | 373 | ;; 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 @@ | ||
| 71 | 71 | |
| 72 | 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 | 83 | (defn normalize-channel [s] |
| 75 | 84 | (let [s (str/trim (or s ""))] |
| 76 | 85 | (cond (str/blank? s) "" |
| @@ -114,6 +123,7 @@ | ||
| 114 | 123 | (swap! channels #(-> (ensure-channel % name) |
| 115 | 124 | (assoc-in [name :unread] 0) |
| 116 | 125 | (assoc-in [name :accessed] (swap! access-tick inc)))) |
| 126 | + (remember-channels!) | |
| 117 | 127 | ;; `joining?` as well as `joined?`: the JOIN echo takes a round trip, and a |
| 118 | 128 | ;; second JOIN sent in the meantime is what makes the server replay nothing. |
| 119 | 129 | (let [buffer (get @channels name)] |
| @@ -401,6 +411,37 @@ | ||
| 401 | 411 | (sort-by (juxt #(- (:accessed % 0)) :name)) |
| 402 | 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 | 445 | (defn last-preview [buffer] |
| 405 | 446 | (if-let [m (last (:messages buffer))] |
| 406 | 447 | (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 a | 127 | ;; `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 @@ | ||
| 42 | 42 | true |
| 43 | 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 | 68 | (defn clear-session! [] |
| 46 | 69 | (try |
| 47 | 70 | (when (host/file-exists? (session-file)) |
| @@ -42,6 +42,29 @@ | |||
| 42 | true | 42 | 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 | (try | 69 | (try |
| 47 | (when (host/file-exists? (session-file)) | 70 | (when (host/file-exists? (session-file)) |