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

Share the chats screen, and the four things under it

The second screen out of frq.app, and again a rename rather than a rewrite:
the same hiccup, reading frq.cells and calling frq.actions. The desktop draws
this file now — the TUI shows the list with its rooms, previews and Open
buttons exactly as before.

Four pieces came with it, each because the screen needed it and each worth
having apart:

frq.rooms — channel-list, dm?, last-preview. Pure over the cells, so shared;
frq.state re-defs all three.

frq.metrics — window-row, chrome-row, chrome-scale and the two terminal
flags. These are what the backend is like, and screens count reserves against
them. frq.tui still says `app/chrome-row` and still works, because frq.app
re-defs them.

frq.cells grows channels, current, join-input and search; frq.actions grows
connected?, join!, open-channel! and leave-channel!. connected? is a query
rather than a cell because what counts as connected is a socket, and each
platform holds its own.

The phone does not draw it yet, and that is the renderer rather than the
screen. `:fill-height` is iced's Length::Fill — the space left over, which is
Flutter's Expanded and not a taller mainAxisSize. Getting that onto the right
children without breaking the bands around them blanked the screen, so it is
reverted rather than shipped half-working, and flutter/README.md says what it
needs.

One real renderer bug did come out of the attempt and is worth keeping in
mind when it resumes: an :entry with no :width-request fills its row, which
in a Row is Expanded. The connect screen's server boxes carry widths and were
fine; the chats screen's join box carries none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-11T21:34:38-07:00 Browse files
1f55e68 parent: 19a2ec6
modified common/frq/actions.cljc +12 -0
@@ -21,7 +21,19 @@
2121 (defn- call [k args]
2222 (when-let [f (get @impl k)] (apply f args)))
2323
24+(defn connected?
25+ "Whether there is a live connection. A query rather than a cell: what counts
26+ as connected is a socket, and each platform holds its own."
27+ []
28+ (boolean (call :connected? [])))
29+
2430 (defn connect! [] (call :connect! []))
2531 (defn disconnect! [] (call :disconnect! []))
2632 (defn forget-session! [] (call :forget-session! []))
2733 (defn open-url! [url] (call :open-url! [url]))
34+
35+;; ------------------------------------------------------------------ rooms
36+
37+(defn join! [name] (call :join! [name]))
38+(defn open-channel! [name] (call :open-channel! [name]))
39+(defn leave-channel! [name] (call :leave-channel! [name]))
@@ -21,7 +21,19 @@
21 (defn- call [k args]21 (defn- call [k args]
22 (when-let [f (get @impl k)] (apply f args)))22 (when-let [f (get @impl k)] (apply f args)))
23 23
24+(defn connected?
25+ "Whether there is a live connection. A query rather than a cell: what counts
26+ as connected is a socket, and each platform holds its own."
27+ []
28+ (boolean (call :connected? [])))
29+
24 (defn connect! [] (call :connect! []))30 (defn connect! [] (call :connect! []))
25 (defn disconnect! [] (call :disconnect! []))31 (defn disconnect! [] (call :disconnect! []))
26 (defn forget-session! [] (call :forget-session! []))32 (defn forget-session! [] (call :forget-session! []))
27 (defn open-url! [url] (call :open-url! [url]))33 (defn open-url! [url] (call :open-url! [url]))
34+
35+;; ------------------------------------------------------------------ rooms
36+
37+(defn join! [name] (call :join! [name]))
38+(defn open-channel! [name] (call :open-channel! [name]))
39+(defn leave-channel! [name] (call :leave-channel! [name]))
modified common/frq/cells.cljc +7 -0
@@ -43,5 +43,12 @@
4343
4444 ;; The durable half of an OAuth sign-in. The web-token beside it is single-use,
4545 ;; so a reconnect mints a fresh one from this rather than replaying the old.
46+; ------------------------------------------------------------------ rooms
47+;; name -> {:name :messages [{:from :text}] :unread n :joined? bool}
48+(defonce channels (atom {}))
49+(defonce current (atom nil))
50+(defonce join-input (atom ""))
51+(defonce search (atom ""))
52+
4653 (defonce broker-token (atom nil))
4754 (defonce login-url (atom nil)) ; shown while the browser is open
@@ -43,5 +43,12 @@
43 43
44 ;; The durable half of an OAuth sign-in. The web-token beside it is single-use,44 ;; The durable half of an OAuth sign-in. The web-token beside it is single-use,
45 ;; so a reconnect mints a fresh one from this rather than replaying the old.45 ;; so a reconnect mints a fresh one from this rather than replaying the old.
46+; ------------------------------------------------------------------ rooms
47+;; name -> {:name :messages [{:from :text}] :unread n :joined? bool}
48+(defonce channels (atom {}))
49+(defonce current (atom nil))
50+(defonce join-input (atom ""))
51+(defonce search (atom ""))
52+
46 (defonce broker-token (atom nil))53 (defonce broker-token (atom nil))
47 (defonce login-url (atom nil)) ; shown while the browser is open54 (defonce login-url (atom nil)) ; shown while the browser is open
added common/frq/metrics.cljc +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+(ns frq.metrics
2+ "How tall a row of chrome is — a button, the compose bar, a line of tabs.
3+
4+ The reserves the screens count are in points against this: a window's row is
5+ 34 of them, and every gap around it was chosen at that size. A terminal's row
6+ is one cell, and the same count then reserves two or three times the room the
7+ strip under the list actually needs — which costs a message a row, and a
8+ conversation is measured in how many of those fit.
9+
10+ So the counts stay where the reasoning is and a backend whose rows are a
11+ different height says so here. `frq.tui` sets `chrome-row` before its first
12+ paint; a window and a phone leave it alone.
13+
14+ Shared because the screens that count against it are shared: `below-list` in
15+ `frq.screens.chats` is the first, and the chat screen's reserves will be next.
16+ `frq.app` re-defs all three, so `frq.tui` still writes `app/chrome-row`."
17+ (:require #?@(:cljd []
18+ :jolt [[glimmer.ratom :refer [atom]]])))
19+
20+(def window-row 34)
21+(defonce chrome-row (atom window-row))
22+
23+(defn chrome-scale [] (/ (double @chrome-row) window-row))
24+
25+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
26+;; the first paint and never again. Two things in a message hang on it, and
27+;; both are about a cell grid rather than a canvas: there is no picture to draw
28+;; a face with, and a name with nothing to its left is the heading the text
29+;; hangs off — the shape a terminal has read messages in for forty years.
30+(defonce terminal? (atom false))
31+
32+;; And whether that terminal draws pictures over its cells — Kitty's graphics
33+;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not.
34+(defonce terminal-graphics? (atom false))
new file mode 100644
@@ -0,0 +1,34 @@
1+(ns frq.metrics
2+ "How tall a row of chrome is — a button, the compose bar, a line of tabs.
3+
4+ The reserves the screens count are in points against this: a window's row is
5+ 34 of them, and every gap around it was chosen at that size. A terminal's row
6+ is one cell, and the same count then reserves two or three times the room the
7+ strip under the list actually needs — which costs a message a row, and a
8+ conversation is measured in how many of those fit.
9+
10+ So the counts stay where the reasoning is and a backend whose rows are a
11+ different height says so here. `frq.tui` sets `chrome-row` before its first
12+ paint; a window and a phone leave it alone.
13+
14+ Shared because the screens that count against it are shared: `below-list` in
15+ `frq.screens.chats` is the first, and the chat screen's reserves will be next.
16+ `frq.app` re-defs all three, so `frq.tui` still writes `app/chrome-row`."
17+ (:require #?@(:cljd []
18+ :jolt [[glimmer.ratom :refer [atom]]])))
19+
20+(def window-row 34)
21+(defonce chrome-row (atom window-row))
22+
23+(defn chrome-scale [] (/ (double @chrome-row) window-row))
24+
25+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
26+;; the first paint and never again. Two things in a message hang on it, and
27+;; both are about a cell grid rather than a canvas: there is no picture to draw
28+;; a face with, and a name with nothing to its left is the heading the text
29+;; hangs off — the shape a terminal has read messages in for forty years.
30+(defonce terminal? (atom false))
31+
32+;; And whether that terminal draws pictures over its cells — Kitty's graphics
33+;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not.
34+(defonce terminal-graphics? (atom false))
added common/frq/rooms.cljc +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+(ns frq.rooms
2+ "What the conversation list is, derived from the cells it is held in.
3+
4+ Pure, and so shared: `channel-list` is a filter and a sort over
5+ `frq.cells/channels` and the search box, `dm?` is a question about a name,
6+ `last-preview` reads the last message of a buffer. `frq.state` re-defs all
7+ three, so the thousand lines below them did not move."
8+ (:require [clojure.string :as str]
9+ [frq.cells :as cells]))
10+
11+(defn dm?
12+ "Whether a buffer is a conversation with a person rather than a room. Every
13+ channel name starts with `#`; what does not is somebody's nick."
14+ [name]
15+ (and (seq name) (not (str/starts-with? name "#"))))
16+
17+(defn last-preview [buffer]
18+ (if-let [m (last (:messages buffer))]
19+ (str (:from m) ": " (:text m))
20+ "No messages yet"))
21+
22+(defn channel-list
23+ "Buffers most recently opened first, filtered by the search box.
24+
25+ A conversation list is read from the top, and the one you were just in is the
26+ one you are most likely to want again. Buffers never opened — a DM that
27+ arrived, a channel someone mentioned — sort under those, by name, rather than
28+ jumping the queue."
29+ []
30+ (let [q (str/lower-case (str/trim @cells/search))]
31+ (->> (vals @cells/channels)
32+ (filter #(or (str/blank? q)
33+ (str/includes? (str/lower-case (:name %)) q)))
34+ (sort-by (juxt #(- (:accessed % 0)) :name))
35+ vec)))
new file mode 100644
@@ -0,0 +1,35 @@
1+(ns frq.rooms
2+ "What the conversation list is, derived from the cells it is held in.
3+
4+ Pure, and so shared: `channel-list` is a filter and a sort over
5+ `frq.cells/channels` and the search box, `dm?` is a question about a name,
6+ `last-preview` reads the last message of a buffer. `frq.state` re-defs all
7+ three, so the thousand lines below them did not move."
8+ (:require [clojure.string :as str]
9+ [frq.cells :as cells]))
10+
11+(defn dm?
12+ "Whether a buffer is a conversation with a person rather than a room. Every
13+ channel name starts with `#`; what does not is somebody's nick."
14+ [name]
15+ (and (seq name) (not (str/starts-with? name "#"))))
16+
17+(defn last-preview [buffer]
18+ (if-let [m (last (:messages buffer))]
19+ (str (:from m) ": " (:text m))
20+ "No messages yet"))
21+
22+(defn channel-list
23+ "Buffers most recently opened first, filtered by the search box.
24+
25+ A conversation list is read from the top, and the one you were just in is the
26+ one you are most likely to want again. Buffers never opened — a DM that
27+ arrived, a channel someone mentioned — sort under those, by name, rather than
28+ jumping the queue."
29+ []
30+ (let [q (str/lower-case (str/trim @cells/search))]
31+ (->> (vals @cells/channels)
32+ (filter #(or (str/blank? q)
33+ (str/includes? (str/lower-case (:name %)) q)))
34+ (sort-by (juxt #(- (:accessed % 0)) :name))
35+ vec)))
added common/frq/screens/chats.cljc +222 -0
new file mode 100644
@@ -0,0 +1,222 @@
1+(ns frq.screens.chats
2+ "The conversation list, shared.
3+
4+ The second screen out of `frq.app`, and like the first it moved rather than
5+ changed: the same hiccup, reading `frq.cells` and `frq.rooms` instead of
6+ `frq.state`, and calling `frq.actions` instead of the reducers.
7+
8+ `error-note` and `tab-bar` come with it because the list is not the only
9+ screen that shows them — the chat screen and settings do too, and they will
10+ want this namespace rather than a second copy when their turn comes."
11+ (:require [clojure.string :as str]
12+ [frq.actions :as actions]
13+ [frq.cells :as cells]
14+ [frq.metrics :refer [chrome-scale terminal?]]
15+ [frq.rooms :as rooms]
16+ [frq.screens.connect :refer [error-note]]))
17+
18+(defn tab-bar []
19+ [:hbox {:spacing 8}
20+ (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]]
21+ [:button {:key k
22+ :label label
23+ :kind (if (= k @cells/screen) :primary :default)
24+ :on-click #(reset! cells/screen k)}])])
25+
26+
27+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
28+;; the first paint and never again. Two things in a message hang on it, and
29+;; both are about a cell grid rather than a canvas: there is no picture to
30+;; draw a face with, and a name with nothing to its left is the heading the
31+;; text hangs off — the shape a terminal has read messages in for forty years.
32+
33+(def ^:private list-gutter 16)
34+
35+(defn- below-list []
36+ ;; What the strip under the list needs, counted: the gap after the list, the
37+ ;; separator, the gap under it, the tab bar's row of buttons and the margin
38+ ;; below them. Short by any of it and the tabs go off the bottom edge — which
39+ ;; is the thing this layout exists to stop.
40+ (* (chrome-scale) (+ 8 8 8 34 12)))
41+
42+(defn- preview-line
43+ "The last line of a conversation, as one line: a pasted shell script or a
44+ long link is a card's worth of text otherwise, and the cards stop reading as
45+ a list of rooms."
46+ [text]
47+ (let [line (str/replace (str text) #"\s+" " ")]
48+ ;; 60 rather than a card's worth: at `sidebar-width` a 90-character line is
49+ ;; three wrapped rows, and three rows of somebody else's last sentence is
50+ ;; the row shouting over the name above it. One row of it says which
51+ ;; conversation this is, which is all the list is for.
52+ (if (> (count line) 60)
53+ (str (subs line 0 59) "…")
54+ line)))
55+
56+;; What everything in the chats pane keeps clear of its right edge.
57+;;
58+;; It is one number because it is one edge: the scrollbar rides it, and the
59+;; cards in the list have to stop short of the bar rather than be drawn under
60+;; it — but the Join box above the list is outside the scroll and has no bar
61+;; beside it, so left to itself it ran on past the cards and the pane had two
62+;; right edges half a finger apart. Whatever the gap is, both of them take it.
63+
64+(defn- close-button [name]
65+ [:button {:label "Close"
66+ :on-click #(actions/leave-channel! name)}])
67+
68+(defn- open-button [name]
69+ [:button {:label "Open"
70+ :on-click #(actions/open-channel! name)}])
71+
72+;; The only way out of a room. Everything else adds one — the server saying we
73+;; are in it, a message arriving in it — so without this the list only grows,
74+;; and what rooms.edn claims we are in could never shrink.
75+
76+(defn conversation-row [buffer]
77+ (let [name (:name buffer)
78+ ;; Defaulted rather than assumed. Everything that builds a buffer
79+ ;; goes through `ensure-channel`, but the list draws whatever is in
80+ ;; the atom, and a half-made room reaching here should be a row that
81+ ;; reads as quiet rather than the frame that killed the client.
82+ unread (:unread buffer 0)]
83+ ;; The card in a wrapper that holds it off the right edge, where the
84+ ;; list's scrollbar rides — `message-row` keeps its actions clear of the
85+ ;; same edge for the same reason, and without it the cards are drawn
86+ ;; under the bar rather than beside it. The wrapper and not the card
87+ ;; itself: the window backend pads a card by a fixed 12 and never reads a
88+ ;; margin off one, so the room has to be taken outside it.
89+ [:vbox {:key name :margin-right list-gutter}
90+ [:card {}
91+ ;; The name gets the line, and membership and unread count the one under
92+ ;; it. All three on one line is what the list pane has no room for: the
93+ ;; name takes what it likes, the badges are left the remainder, and a
94+ ;; long name squeezes them into a status reading "joine/d" and a count
95+ ;; split down two lines — while the card, sized to a row that no longer
96+ ;; fits, sticks out past the cards above and below it.
97+ ;;
98+ ;; In a terminal the Open button rides up onto the name's line. A row of
99+ ;; chrome there is one cell, so a button on a line of its own costs the
100+ ;; card a whole row — four of them and the list has spent a screenful on
101+ ;; buttons. A window's row is 34 points and its button is a lozenge with
102+ ;; air around it, which is why it keeps its own line there.
103+ (if @terminal?
104+ [:hbox {:spacing 8 :wrap false}
105+ [:title-2 {:label name}]
106+ [open-button name]
107+ [close-button name]]
108+ [:title-2 {:label name}])
109+ ;; The badge line, and only when there is a badge: every row carrying a
110+ ;; green "joined" was a row spending a third of its height saying the
111+ ;; ordinary thing, and a list where every card says the same word is a
112+ ;; list you read by skipping. So membership is reported when it is news —
113+ ;; a room in the list we are not in — and a DM says nothing at all, since
114+ ;; there is no membership to have and the name already says what it is.
115+ ;;
116+ ;; In a wrapper of its own, because the line comes and goes with the
117+ ;; unread count and the reconciler numbers a card's children by position.
118+ ;; `:wrap false` on the badges: they sit beside each other or not at all.
119+ [:vbox {:key :badges}
120+ (let [away? (and (not (rooms/dm? name)) (not (:joined? buffer)))]
121+ (when (or away? (pos? unread))
122+ [:hbox {:spacing 12 :wrap false}
123+ (when away?
124+ [:status {:label "not joined" :live false}])
125+ ;; A mention is not more unread, it is different unread: the dot
126+ ;; says how much there is and the name says it was aimed at you.
127+ ;; Both or neither — a room with your name in it always has a line
128+ ;; to count.
129+ (when (pos? unread)
130+ [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]))]
131+ [:dim-label {:label (preview-line (rooms/last-preview buffer))}]
132+ (when-not @terminal?
133+ [:hbox {:spacing 8 :wrap false}
134+ [open-button name]
135+ [close-button name]])]]))
136+
137+(defn chats-screen []
138+ (let [buffers (rooms/channel-list)]
139+ ;; Three bands rather than a page: the title and the two boxes at the top,
140+ ;; the tabs at the bottom, and the conversations scrolling between them.
141+ ;; Everything you act with stays where you last saw it — the same bargain
142+ ;; the chat screen makes, where the compose bar holds still under a backlog
143+ ;; that moves.
144+ ;;
145+ ;; What goes with the page is its centring at 620, on a window between that
146+ ;; and the split view's 900: the list is full-bleed there now, which is
147+ ;; what the conversation beside it has always been.
148+ ;; `:fill-height` on the outermost box, not only on the band inside it.
149+ ;; A backend that gives a column the height of what is in it hands the
150+ ;; band below a window's worth of nothing to divide: the list asks for
151+ ;; the rest of the screen, is told the rest is forty points, and the
152+ ;; whole screen paints into a strip along the top with the window empty
153+ ;; under it. Every screen that pins something to the bottom says this on
154+ ;; its own root — the split view already said it, one wrapper further out.
155+ [:vbox {:spacing 8 :margin 12 :fill-height true}
156+ [:vbox {:key :head :spacing 8 :margin-right list-gutter}
157+ ;; Your nick in the title, where "Chats" alone said nothing you did not
158+ ;; already know — and said plainly, as a sign-in rather than as a name
159+ ;; the list is somehow held in. It is what every channel calls you and
160+ ;; what your own lines are signed with, and the only other place it
161+ ;; showed was Settings.
162+ [:title {:label (if (actions/connected?)
163+ (str "Logged in as " @cells/form-nick)
164+ "Chats")}]
165+ [error-note]
166+ ;; Join and search are the same shape — a channel box with a button
167+ ;; beside it — so they read as one control block rather than a titled
168+ ;; card and a stray row: same card, same widths, same gap. The screen
169+ ;; title already says what the box is for, which is what the "Join
170+ ;; channel" header was doing.
171+ [:card {}
172+ [:vbox {:spacing 8}
173+ ;; Button first and `:align :end`, so the row is laid out from its
174+ ;; right edge: the button is placed against that edge and the box takes
175+ ;; what is left. An entry asks for whatever remains where it stands, so
176+ ;; a box placed first takes the row and pushes the button onto a line of
177+ ;; its own — Join under the box rather than beside it — and a box given
178+ ;; a fixed width only holds that off until the window is narrower than
179+ ;; the number. Laid out from the right the box is the part that gives,
180+ ;; at every width, in the frame the drag happens rather than the one
181+ ;; after the window is next measured.
182+ [:hbox {:spacing 8 :align :end}
183+ [:button {:label (if (str/starts-with? @cells/join-input "@") "Message" "Join")
184+ :kind :primary
185+ :on-click #(do (actions/join! @cells/join-input) (reset! cells/join-input ""))}]
186+ ;; Both kinds of conversation through one box: `#room` joins a
187+ ;; channel, `@nick` opens a message to a person.
188+ [:entry {:text @cells/join-input
189+ :placeholder "#channel or @nick"
190+ :on-change #(reset! cells/join-input %)
191+ :on-activate #(do (actions/join! @cells/join-input) (reset! cells/join-input ""))}]]
192+ ;; The clear button only shows while there is something to clear: an
193+ ;; empty box has nothing to undo, and a dead button beside it reads as
194+ ;; one that stopped working. It leads the row for the same reason the
195+ ;; Join button does, and the box gives up the width it takes.
196+ [:hbox {:spacing 8 :align :end}
197+ (when (seq @cells/search)
198+ [:button {:label "✕" :on-click #(reset! cells/search "")}])
199+ [:entry {:text @cells/search
200+ :placeholder "Search channels"
201+ :on-change #(reset! cells/search %)}]]]]]
202+ ;; `:fill-height` so the scroll inside is handed the rest of the window
203+ ;; rather than the one card it starts out holding, and `:reserve` to keep
204+ ;; the tabs' strip out of what it may take.
205+ ;; Named, so it is the same list — and the same scroll position — when the
206+ ;; reader comes back from a conversation.
207+ [:vbox {:key :list :fill-height true}
208+ [:scroll {:scroll-key "chats-list" :orientation :vertical
209+ :reserve (below-list)}
210+ (if (seq buffers)
211+ ;; Keyed on the name, because this list reorders: opening a
212+ ;; conversation bumps it to the top, and unkeyed children reconcile by
213+ ;; position — the widgets stay put and their props are rewritten under
214+ ;; them. What is focused is a widget, so the highlight would stay in
215+ ;; the slot the reader clicked while the row that moved into it is
216+ ;; someone else's conversation.
217+ (for [b buffers] ^{:key (:name b)} [conversation-row b])
218+ [:vbox {:margin-right list-gutter}
219+ [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]])]]
220+ [:vbox {:key :foot :spacing 8}
221+ [:separator {}]
222+ [tab-bar]]]))\n
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,222 @@
1+(ns frq.screens.chats
2+ "The conversation list, shared.
3+
4+ The second screen out of `frq.app`, and like the first it moved rather than
5+ changed: the same hiccup, reading `frq.cells` and `frq.rooms` instead of
6+ `frq.state`, and calling `frq.actions` instead of the reducers.
7+
8+ `error-note` and `tab-bar` come with it because the list is not the only
9+ screen that shows them — the chat screen and settings do too, and they will
10+ want this namespace rather than a second copy when their turn comes."
11+ (:require [clojure.string :as str]
12+ [frq.actions :as actions]
13+ [frq.cells :as cells]
14+ [frq.metrics :refer [chrome-scale terminal?]]
15+ [frq.rooms :as rooms]
16+ [frq.screens.connect :refer [error-note]]))
17+
18+(defn tab-bar []
19+ [:hbox {:spacing 8}
20+ (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]]
21+ [:button {:key k
22+ :label label
23+ :kind (if (= k @cells/screen) :primary :default)
24+ :on-click #(reset! cells/screen k)}])])
25+
26+
27+;; Whether the backend under this tree is a terminal, set by `frq.tui` before
28+;; the first paint and never again. Two things in a message hang on it, and
29+;; both are about a cell grid rather than a canvas: there is no picture to
30+;; draw a face with, and a name with nothing to its left is the heading the
31+;; text hangs off — the shape a terminal has read messages in for forty years.
32+
33+(def ^:private list-gutter 16)
34+
35+(defn- below-list []
36+ ;; What the strip under the list needs, counted: the gap after the list, the
37+ ;; separator, the gap under it, the tab bar's row of buttons and the margin
38+ ;; below them. Short by any of it and the tabs go off the bottom edge — which
39+ ;; is the thing this layout exists to stop.
40+ (* (chrome-scale) (+ 8 8 8 34 12)))
41+
42+(defn- preview-line
43+ "The last line of a conversation, as one line: a pasted shell script or a
44+ long link is a card's worth of text otherwise, and the cards stop reading as
45+ a list of rooms."
46+ [text]
47+ (let [line (str/replace (str text) #"\s+" " ")]
48+ ;; 60 rather than a card's worth: at `sidebar-width` a 90-character line is
49+ ;; three wrapped rows, and three rows of somebody else's last sentence is
50+ ;; the row shouting over the name above it. One row of it says which
51+ ;; conversation this is, which is all the list is for.
52+ (if (> (count line) 60)
53+ (str (subs line 0 59) "…")
54+ line)))
55+
56+;; What everything in the chats pane keeps clear of its right edge.
57+;;
58+;; It is one number because it is one edge: the scrollbar rides it, and the
59+;; cards in the list have to stop short of the bar rather than be drawn under
60+;; it — but the Join box above the list is outside the scroll and has no bar
61+;; beside it, so left to itself it ran on past the cards and the pane had two
62+;; right edges half a finger apart. Whatever the gap is, both of them take it.
63+
64+(defn- close-button [name]
65+ [:button {:label "Close"
66+ :on-click #(actions/leave-channel! name)}])
67+
68+(defn- open-button [name]
69+ [:button {:label "Open"
70+ :on-click #(actions/open-channel! name)}])
71+
72+;; The only way out of a room. Everything else adds one — the server saying we
73+;; are in it, a message arriving in it — so without this the list only grows,
74+;; and what rooms.edn claims we are in could never shrink.
75+
76+(defn conversation-row [buffer]
77+ (let [name (:name buffer)
78+ ;; Defaulted rather than assumed. Everything that builds a buffer
79+ ;; goes through `ensure-channel`, but the list draws whatever is in
80+ ;; the atom, and a half-made room reaching here should be a row that
81+ ;; reads as quiet rather than the frame that killed the client.
82+ unread (:unread buffer 0)]
83+ ;; The card in a wrapper that holds it off the right edge, where the
84+ ;; list's scrollbar rides — `message-row` keeps its actions clear of the
85+ ;; same edge for the same reason, and without it the cards are drawn
86+ ;; under the bar rather than beside it. The wrapper and not the card
87+ ;; itself: the window backend pads a card by a fixed 12 and never reads a
88+ ;; margin off one, so the room has to be taken outside it.
89+ [:vbox {:key name :margin-right list-gutter}
90+ [:card {}
91+ ;; The name gets the line, and membership and unread count the one under
92+ ;; it. All three on one line is what the list pane has no room for: the
93+ ;; name takes what it likes, the badges are left the remainder, and a
94+ ;; long name squeezes them into a status reading "joine/d" and a count
95+ ;; split down two lines — while the card, sized to a row that no longer
96+ ;; fits, sticks out past the cards above and below it.
97+ ;;
98+ ;; In a terminal the Open button rides up onto the name's line. A row of
99+ ;; chrome there is one cell, so a button on a line of its own costs the
100+ ;; card a whole row — four of them and the list has spent a screenful on
101+ ;; buttons. A window's row is 34 points and its button is a lozenge with
102+ ;; air around it, which is why it keeps its own line there.
103+ (if @terminal?
104+ [:hbox {:spacing 8 :wrap false}
105+ [:title-2 {:label name}]
106+ [open-button name]
107+ [close-button name]]
108+ [:title-2 {:label name}])
109+ ;; The badge line, and only when there is a badge: every row carrying a
110+ ;; green "joined" was a row spending a third of its height saying the
111+ ;; ordinary thing, and a list where every card says the same word is a
112+ ;; list you read by skipping. So membership is reported when it is news —
113+ ;; a room in the list we are not in — and a DM says nothing at all, since
114+ ;; there is no membership to have and the name already says what it is.
115+ ;;
116+ ;; In a wrapper of its own, because the line comes and goes with the
117+ ;; unread count and the reconciler numbers a card's children by position.
118+ ;; `:wrap false` on the badges: they sit beside each other or not at all.
119+ [:vbox {:key :badges}
120+ (let [away? (and (not (rooms/dm? name)) (not (:joined? buffer)))]
121+ (when (or away? (pos? unread))
122+ [:hbox {:spacing 12 :wrap false}
123+ (when away?
124+ [:status {:label "not joined" :live false}])
125+ ;; A mention is not more unread, it is different unread: the dot
126+ ;; says how much there is and the name says it was aimed at you.
127+ ;; Both or neither — a room with your name in it always has a line
128+ ;; to count.
129+ (when (pos? unread)
130+ [:label {:label (str (if (:mention? buffer) "◆ @ " "● ") unread)}])]))]
131+ [:dim-label {:label (preview-line (rooms/last-preview buffer))}]
132+ (when-not @terminal?
133+ [:hbox {:spacing 8 :wrap false}
134+ [open-button name]
135+ [close-button name]])]]))
136+
137+(defn chats-screen []
138+ (let [buffers (rooms/channel-list)]
139+ ;; Three bands rather than a page: the title and the two boxes at the top,
140+ ;; the tabs at the bottom, and the conversations scrolling between them.
141+ ;; Everything you act with stays where you last saw it — the same bargain
142+ ;; the chat screen makes, where the compose bar holds still under a backlog
143+ ;; that moves.
144+ ;;
145+ ;; What goes with the page is its centring at 620, on a window between that
146+ ;; and the split view's 900: the list is full-bleed there now, which is
147+ ;; what the conversation beside it has always been.
148+ ;; `:fill-height` on the outermost box, not only on the band inside it.
149+ ;; A backend that gives a column the height of what is in it hands the
150+ ;; band below a window's worth of nothing to divide: the list asks for
151+ ;; the rest of the screen, is told the rest is forty points, and the
152+ ;; whole screen paints into a strip along the top with the window empty
153+ ;; under it. Every screen that pins something to the bottom says this on
154+ ;; its own root — the split view already said it, one wrapper further out.
155+ [:vbox {:spacing 8 :margin 12 :fill-height true}
156+ [:vbox {:key :head :spacing 8 :margin-right list-gutter}
157+ ;; Your nick in the title, where "Chats" alone said nothing you did not
158+ ;; already know — and said plainly, as a sign-in rather than as a name
159+ ;; the list is somehow held in. It is what every channel calls you and
160+ ;; what your own lines are signed with, and the only other place it
161+ ;; showed was Settings.
162+ [:title {:label (if (actions/connected?)
163+ (str "Logged in as " @cells/form-nick)
164+ "Chats")}]
165+ [error-note]
166+ ;; Join and search are the same shape — a channel box with a button
167+ ;; beside it — so they read as one control block rather than a titled
168+ ;; card and a stray row: same card, same widths, same gap. The screen
169+ ;; title already says what the box is for, which is what the "Join
170+ ;; channel" header was doing.
171+ [:card {}
172+ [:vbox {:spacing 8}
173+ ;; Button first and `:align :end`, so the row is laid out from its
174+ ;; right edge: the button is placed against that edge and the box takes
175+ ;; what is left. An entry asks for whatever remains where it stands, so
176+ ;; a box placed first takes the row and pushes the button onto a line of
177+ ;; its own — Join under the box rather than beside it — and a box given
178+ ;; a fixed width only holds that off until the window is narrower than
179+ ;; the number. Laid out from the right the box is the part that gives,
180+ ;; at every width, in the frame the drag happens rather than the one
181+ ;; after the window is next measured.
182+ [:hbox {:spacing 8 :align :end}
183+ [:button {:label (if (str/starts-with? @cells/join-input "@") "Message" "Join")
184+ :kind :primary
185+ :on-click #(do (actions/join! @cells/join-input) (reset! cells/join-input ""))}]
186+ ;; Both kinds of conversation through one box: `#room` joins a
187+ ;; channel, `@nick` opens a message to a person.
188+ [:entry {:text @cells/join-input
189+ :placeholder "#channel or @nick"
190+ :on-change #(reset! cells/join-input %)
191+ :on-activate #(do (actions/join! @cells/join-input) (reset! cells/join-input ""))}]]
192+ ;; The clear button only shows while there is something to clear: an
193+ ;; empty box has nothing to undo, and a dead button beside it reads as
194+ ;; one that stopped working. It leads the row for the same reason the
195+ ;; Join button does, and the box gives up the width it takes.
196+ [:hbox {:spacing 8 :align :end}
197+ (when (seq @cells/search)
198+ [:button {:label "✕" :on-click #(reset! cells/search "")}])
199+ [:entry {:text @cells/search
200+ :placeholder "Search channels"
201+ :on-change #(reset! cells/search %)}]]]]]
202+ ;; `:fill-height` so the scroll inside is handed the rest of the window
203+ ;; rather than the one card it starts out holding, and `:reserve` to keep
204+ ;; the tabs' strip out of what it may take.
205+ ;; Named, so it is the same list — and the same scroll position — when the
206+ ;; reader comes back from a conversation.
207+ [:vbox {:key :list :fill-height true}
208+ [:scroll {:scroll-key "chats-list" :orientation :vertical
209+ :reserve (below-list)}
210+ (if (seq buffers)
211+ ;; Keyed on the name, because this list reorders: opening a
212+ ;; conversation bumps it to the top, and unkeyed children reconcile by
213+ ;; position — the widgets stay put and their props are rewritten under
214+ ;; them. What is focused is a widget, so the highlight would stay in
215+ ;; the slot the reader clicked while the row that moved into it is
216+ ;; someone else's conversation.
217+ (for [b buffers] ^{:key (:name b)} [conversation-row b])
218+ [:vbox {:margin-right list-gutter}
219+ [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]])]]
220+ [:vbox {:key :foot :spacing 8}
221+ [:separator {}]
222+ [tab-bar]]]))\n
\ No newline at end of file\ No newline at end of file
modified flutter/README.md +20 -0
@@ -135,6 +135,26 @@ screen rather than the subtree that read it. Fine at this size.
135135 reads is `frq.cells` and what it calls is `frq.actions`, and each platform
136136 fills those in: `frq.state`'s reducers on the desktop, dart:io here.
137137
138+## Two screens shared, and where the renderer stops
139+
140+`frq.screens.connect` and `frq.screens.chats` are in `common/` now, with the
141+cells under them in `frq.cells`, the derivations in `frq.rooms`, the backend
142+metrics in `frq.metrics` and the things a screen cannot do itself behind
143+`frq.actions`. `frq.app` requires both and the desktop draws them — verified in
144+the TUI, including the conversation list with its rooms and previews.
145+
146+The phone draws the connect screen. It does not draw the chats one yet, and
147+the gap is in `frq.hiccup` rather than in the screen: `:fill-height` is iced's
148+`Length::Fill`, the space left over, which is Flutter's `Expanded` — not a
149+taller `mainAxisSize`. Getting that onto the right children without breaking
150+the bands around them is unfinished; the attempt blanked the screen and was
151+reverted rather than shipped half-working.
152+
153+`:entry` is the same lesson twice. An entry with no `:width-request` fills its
154+row, which in a Row is `Expanded`; the connect screen's server boxes carry
155+widths and came out fine, and the chats screen's join box carries none and did
156+not.
157+
138158 ## The order to do the rest in
139159
140160 1. ~~**`frq.irc`**~~ — started. The parser is `common/frq/irc/parse.cljc` now,
@@ -135,6 +135,26 @@ screen rather than the subtree that read it. Fine at this size.
135 reads is `frq.cells` and what it calls is `frq.actions`, and each platform135 reads is `frq.cells` and what it calls is `frq.actions`, and each platform
136 fills those in: `frq.state`'s reducers on the desktop, dart:io here.136 fills those in: `frq.state`'s reducers on the desktop, dart:io here.
137 137
138+## Two screens shared, and where the renderer stops
139+
140+`frq.screens.connect` and `frq.screens.chats` are in `common/` now, with the
141+cells under them in `frq.cells`, the derivations in `frq.rooms`, the backend
142+metrics in `frq.metrics` and the things a screen cannot do itself behind
143+`frq.actions`. `frq.app` requires both and the desktop draws them — verified in
144+the TUI, including the conversation list with its rooms and previews.
145+
146+The phone draws the connect screen. It does not draw the chats one yet, and
147+the gap is in `frq.hiccup` rather than in the screen: `:fill-height` is iced's
148+`Length::Fill`, the space left over, which is Flutter's `Expanded` — not a
149+taller `mainAxisSize`. Getting that onto the right children without breaking
150+the bands around them is unfinished; the attempt blanked the screen and was
151+reverted rather than shipped half-working.
152+
153+`:entry` is the same lesson twice. An entry with no `:width-request` fills its
154+row, which in a Row is `Expanded`; the connect screen's server boxes carry
155+widths and came out fine, and the chats screen's join box carries none and did
156+not.
157+
138 ## The order to do the rest in158 ## The order to do the rest in
139 159
140 1. ~~**`frq.irc`**~~ — started. The parser is `common/frq/irc/parse.cljc` now,160 1. ~~**`frq.irc`**~~ — started. The parser is `common/frq/irc/parse.cljc` now,
modified src/frq/app.clj +13 -208
@@ -29,33 +29,25 @@
2929 ;; The connect screen lives in common/ now — the same file the
3030 ;; phone renders. It reads frq.cells and calls frq.actions, and
3131 ;; this requires it exactly where its own copy used to be.
32+ [frq.metrics :as metrics]
3233 [frq.screens.connect :as connect :refer [connect-screen error-note]]
34+ ;; The conversation list moved out too, with the tab bar and the
35+ ;; row it paints. Same arrangement as the connect screen: the
36+ ;; phone renders this very file.
37+ [frq.screens.chats :refer [below-list chats-screen conversation-row
38+ preview-line tab-bar]]
3339 [frq.state :as s]))
3440
3541 ;; ---------------------------------------------------------------- pieces
3642
37-(defn tab-bar []
38- [:hbox {:spacing 8}
39- (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]]
40- [:button {:key k
41- :label label
42- :kind (if (= k @s/screen) :primary :default)
43- :on-click #(reset! s/screen k)}])])
44-
45-
46-;; Whether the backend under this tree is a terminal, set by `frq.tui` before
47-;; the first paint and never again. Two things in a message hang on it, and
48-;; both are about a cell grid rather than a canvas: there is no picture to
49-;; draw a face with, and a name with nothing to its left is the heading the
50-;; text hangs off — the shape a terminal has read messages in for forty years.
51-(defonce terminal? (atom false))
43+(def terminal? metrics/terminal?)
5244
5345 ;; And whether that terminal draws pictures over its cells — Kitty's graphics
5446 ;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not.
5547 ;; `frq.tui` works it out from the environment and sets this beside the flag
5648 ;; above; a terminal that has not got it is the one described there, a column
5749 ;; of names with the words hanging under them.
58-(defonce terminal-graphics? (atom false))
50+(def terminal-graphics? metrics/terminal-graphics?)
5951
6052 (defn- terminal-face?
6153 "Whether a message carries a picture of its sender in a terminal.
@@ -114,101 +106,6 @@
114106 ;; rhythm down the list the one word that is the same on every row, with the
115107 ;; name that differs set quieter than it. Nothing moves; the button stops
116108 ;; shouting.
117-(defn- open-button [name]
118- [:button {:label "Open"
119- :on-click #(s/open-channel! name)}])
120-
121-;; The only way out of a room. Everything else adds one — the server saying we
122-;; are in it, a message arriving in it — so without this the list only grows,
123-;; and what rooms.edn claims we are in could never shrink.
124-(defn- close-button [name]
125- [:button {:label "Close"
126- :on-click #(s/leave-channel! name)}])
127-
128-(defn- preview-line
129- "The last line of a conversation, as one line: a pasted shell script or a
130- long link is a card's worth of text otherwise, and the cards stop reading as
131- a list of rooms."
132- [text]
133- (let [line (str/replace (str text) #"\s+" " ")]
134- ;; 60 rather than a card's worth: at `sidebar-width` a 90-character line is
135- ;; three wrapped rows, and three rows of somebody else's last sentence is
136- ;; the row shouting over the name above it. One row of it says which
137- ;; conversation this is, which is all the list is for.
138- (if (> (count line) 60)
139- (str (subs line 0 59) "")
140- line)))
141-
142-;; What everything in the chats pane keeps clear of its right edge.
143-;;
144-;; It is one number because it is one edge: the scrollbar rides it, and the
145-;; cards in the list have to stop short of the bar rather than be drawn under
146-;; it — but the Join box above the list is outside the scroll and has no bar
147-;; beside it, so left to itself it ran on past the cards and the pane had two
148-;; right edges half a finger apart. Whatever the gap is, both of them take it.
149-(def ^:private list-gutter 16)
150-
151-(defn conversation-row [buffer]
152- (let [name (:name buffer)
153- ;; Defaulted rather than assumed. Everything that builds a buffer
154- ;; goes through `ensure-channel`, but the list draws whatever is in
155- ;; the atom, and a half-made room reaching here should be a row that
156- ;; reads as quiet rather than the frame that killed the client.
157- unread (:unread buffer 0)]
158- ;; The card in a wrapper that holds it off the right edge, where the
159- ;; list's scrollbar rides — `message-row` keeps its actions clear of the
160- ;; same edge for the same reason, and without it the cards are drawn
161- ;; under the bar rather than beside it. The wrapper and not the card
162- ;; itself: the window backend pads a card by a fixed 12 and never reads a
163- ;; margin off one, so the room has to be taken outside it.
164- [:vbox {:key name :margin-right list-gutter}
165- [:card {}
166- ;; The name gets the line, and membership and unread count the one under
167- ;; it. All three on one line is what the list pane has no room for: the
168- ;; name takes what it likes, the badges are left the remainder, and a
169- ;; long name squeezes them into a status reading "joine/d" and a count
170- ;; split down two lines — while the card, sized to a row that no longer
171- ;; fits, sticks out past the cards above and below it.
172- ;;
173- ;; In a terminal the Open button rides up onto the name's line. A row of
174- ;; chrome there is one cell, so a button on a line of its own costs the
175- ;; card a whole row — four of them and the list has spent a screenful on
176- ;; buttons. A window's row is 34 points and its button is a lozenge with
177- ;; air around it, which is why it keeps its own line there.
178- (if @terminal?
179- [:hbox {:spacing 8 :wrap false}
180- [:title-2 {:label name}]
181- [open-button name]
182- [close-button name]]
183- [:title-2 {:label name}])
184- ;; The badge line, and only when there is a badge: every row carrying a
185- ;; green "joined" was a row spending a third of its height saying the
186- ;; ordinary thing, and a list where every card says the same word is a
187- ;; list you read by skipping. So membership is reported when it is news —
188- ;; a room in the list we are not in — and a DM says nothing at all, since
189- ;; there is no membership to have and the name already says what it is.
190- ;;
191- ;; In a wrapper of its own, because the line comes and goes with the
192- ;; unread count and the reconciler numbers a card's children by position.
193- ;; `:wrap false` on the badges: they sit beside each other or not at all.
194- [:vbox {:key :badges}
195- (let [away? (and (not (s/dm? name)) (not (:joined? buffer)))]
196- (when (or away? (pos? unread))
197- [:hbox {:spacing 12 :wrap false}
198- (when away?
199- [:status {:label "not joined" :live false}])
200- ;; A mention is not more unread, it is different unread: the dot
201- ;; says how much there is and the name says it was aimed at you.
202- ;; Both or neither — a room with your name in it always has a line
203- ;; to count.
204- (when (pos? unread)
205- [:label {:label (str (if (:mention? buffer) " @ " " ") unread)}])]))]
206- [:dim-label {:label (preview-line (s/last-preview buffer))}]
207- (when-not @terminal?
208- [:hbox {:spacing 8 :wrap false}
209- [open-button name]
210- [close-button name]])]]))
211-
212109 (def ^:private sidebar-width 320)
213110
214111 ;; How tall a row of chrome is — a button, the compose bar, a line of tabs.
@@ -223,10 +120,12 @@
223120 ;; backend whose rows are a different height says so here. Nothing else in the
224121 ;; tree needs it: every other number is a length, and a length scales on the
225122 ;; way across.
226-(def ^:private window-row 34)
227-(defonce chrome-row (atom window-row))
123+(def ^:private window-row metrics/window-row)
124+;; Re-defined rather than moved out of reach: `frq.tui` resets this before its
125+;; first paint and says `app/chrome-row` when it does.
126+(def chrome-row metrics/chrome-row)
228127
229-(defn- chrome-scale [] (/ (double @chrome-row) window-row))
128+(def ^:private chrome-scale metrics/chrome-scale)
230129
231130 (defn- chip-gap
232131 "The air between two chips on a row.
@@ -265,100 +164,6 @@
265164 node]
266165 node))
267166
268-(defn- below-list []
269- ;; What the strip under the list needs, counted: the gap after the list, the
270- ;; separator, the gap under it, the tab bar's row of buttons and the margin
271- ;; below them. Short by any of it and the tabs go off the bottom edge — which
272- ;; is the thing this layout exists to stop.
273- (* (chrome-scale) (+ 8 8 8 34 12)))
274-
275-(defn chats-screen []
276- (let [buffers (s/channel-list)]
277- ;; Three bands rather than a page: the title and the two boxes at the top,
278- ;; the tabs at the bottom, and the conversations scrolling between them.
279- ;; Everything you act with stays where you last saw it — the same bargain
280- ;; the chat screen makes, where the compose bar holds still under a backlog
281- ;; that moves.
282- ;;
283- ;; What goes with the page is its centring at 620, on a window between that
284- ;; and the split view's 900: the list is full-bleed there now, which is
285- ;; what the conversation beside it has always been.
286- ;; `:fill-height` on the outermost box, not only on the band inside it.
287- ;; A backend that gives a column the height of what is in it hands the
288- ;; band below a window's worth of nothing to divide: the list asks for
289- ;; the rest of the screen, is told the rest is forty points, and the
290- ;; whole screen paints into a strip along the top with the window empty
291- ;; under it. Every screen that pins something to the bottom says this on
292- ;; its own root — the split view already said it, one wrapper further out.
293- [:vbox {:spacing 8 :margin 12 :fill-height true}
294- [:vbox {:key :head :spacing 8 :margin-right list-gutter}
295- ;; Your nick in the title, where "Chats" alone said nothing you did not
296- ;; already know — and said plainly, as a sign-in rather than as a name
297- ;; the list is somehow held in. It is what every channel calls you and
298- ;; what your own lines are signed with, and the only other place it
299- ;; showed was Settings.
300- [:title {:label (if (s/connected?)
301- (str "Logged in as " @s/form-nick)
302- "Chats")}]
303- [error-note]
304- ;; Join and search are the same shape — a channel box with a button
305- ;; beside it — so they read as one control block rather than a titled
306- ;; card and a stray row: same card, same widths, same gap. The screen
307- ;; title already says what the box is for, which is what the "Join
308- ;; channel" header was doing.
309- [:card {}
310- [:vbox {:spacing 8}
311- ;; Button first and `:align :end`, so the row is laid out from its
312- ;; right edge: the button is placed against that edge and the box takes
313- ;; what is left. An entry asks for whatever remains where it stands, so
314- ;; a box placed first takes the row and pushes the button onto a line of
315- ;; its own — Join under the box rather than beside it — and a box given
316- ;; a fixed width only holds that off until the window is narrower than
317- ;; the number. Laid out from the right the box is the part that gives,
318- ;; at every width, in the frame the drag happens rather than the one
319- ;; after the window is next measured.
320- [:hbox {:spacing 8 :align :end}
321- [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")
322- :kind :primary
323- :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]
324- ;; Both kinds of conversation through one box: `#room` joins a
325- ;; channel, `@nick` opens a message to a person.
326- [:entry {:text @s/join-input
327- :placeholder "#channel or @nick"
328- :on-change #(reset! s/join-input %)
329- :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]
330- ;; The clear button only shows while there is something to clear: an
331- ;; empty box has nothing to undo, and a dead button beside it reads as
332- ;; one that stopped working. It leads the row for the same reason the
333- ;; Join button does, and the box gives up the width it takes.
334- [:hbox {:spacing 8 :align :end}
335- (when (seq @s/search)
336- [:button {:label "✕" :on-click #(reset! s/search "")}])
337- [:entry {:text @s/search
338- :placeholder "Search channels"
339- :on-change #(reset! s/search %)}]]]]]
340- ;; `:fill-height` so the scroll inside is handed the rest of the window
341- ;; rather than the one card it starts out holding, and `:reserve` to keep
342- ;; the tabs' strip out of what it may take.
343- ;; Named, so it is the same list — and the same scroll position — when the
344- ;; reader comes back from a conversation.
345- [:vbox {:key :list :fill-height true}
346- [:scroll {:scroll-key "chats-list" :orientation :vertical
347- :reserve (below-list)}
348- (if (seq buffers)
349- ;; Keyed on the name, because this list reorders: opening a
350- ;; conversation bumps it to the top, and unkeyed children reconcile by
351- ;; position — the widgets stay put and their props are rewritten under
352- ;; them. What is focused is a widget, so the highlight would stay in
353- ;; the slot the reader clicked while the row that moved into it is
354- ;; someone else's conversation.
355- (for [b buffers] ^{:key (:name b)} [conversation-row b])
356- [:vbox {:margin-right list-gutter}
357- [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]])]]
358- [:vbox {:key :foot :spacing 8}
359- [:separator {}]
360- [tab-bar]]]))
361-
362167 ;; ---------------------------------------------------------------- chat
363168
364169 (def ^:private url-pattern #"https?://[^\s<>\"]+")
@@ -29,33 +29,25 @@
29 ;; The connect screen lives in common/ now — the same file the29 ;; The connect screen lives in common/ now — the same file the
30 ;; phone renders. It reads frq.cells and calls frq.actions, and30 ;; phone renders. It reads frq.cells and calls frq.actions, and
31 ;; this requires it exactly where its own copy used to be.31 ;; this requires it exactly where its own copy used to be.
32+ [frq.metrics :as metrics]
32 [frq.screens.connect :as connect :refer [connect-screen error-note]]33 [frq.screens.connect :as connect :refer [connect-screen error-note]]
34+ ;; The conversation list moved out too, with the tab bar and the
35+ ;; row it paints. Same arrangement as the connect screen: the
36+ ;; phone renders this very file.
37+ [frq.screens.chats :refer [below-list chats-screen conversation-row
38+ preview-line tab-bar]]
33 [frq.state :as s]))39 [frq.state :as s]))
34 40
35 ;; ---------------------------------------------------------------- pieces41 ;; ---------------------------------------------------------------- pieces
36 42
37-(defn tab-bar []43+(def terminal? metrics/terminal?)
38- [:hbox {:spacing 8}
39- (for [[k label] [[:chats "Chats"] [:discover "Discover"] [:settings "Settings"]]]
40- [:button {:key k
41- :label label
42- :kind (if (= k @s/screen) :primary :default)
43- :on-click #(reset! s/screen k)}])])
44-
45-
46-;; Whether the backend under this tree is a terminal, set by `frq.tui` before
47-;; the first paint and never again. Two things in a message hang on it, and
48-;; both are about a cell grid rather than a canvas: there is no picture to
49-;; draw a face with, and a name with nothing to its left is the heading the
50-;; text hangs off — the shape a terminal has read messages in for forty years.
51-(defonce terminal? (atom false))
52 44
53 ;; And whether that terminal draws pictures over its cells — Kitty's graphics45 ;; And whether that terminal draws pictures over its cells — Kitty's graphics
54 ;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not.46 ;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not.
55 ;; `frq.tui` works it out from the environment and sets this beside the flag47 ;; `frq.tui` works it out from the environment and sets this beside the flag
56 ;; above; a terminal that has not got it is the one described there, a column48 ;; above; a terminal that has not got it is the one described there, a column
57 ;; of names with the words hanging under them.49 ;; of names with the words hanging under them.
58-(defonce terminal-graphics? (atom false))50+(def terminal-graphics? metrics/terminal-graphics?)
59 51
60 (defn- terminal-face?52 (defn- terminal-face?
61 "Whether a message carries a picture of its sender in a terminal.53 "Whether a message carries a picture of its sender in a terminal.
@@ -114,101 +106,6 @@
114 ;; rhythm down the list the one word that is the same on every row, with the106 ;; rhythm down the list the one word that is the same on every row, with the
115 ;; name that differs set quieter than it. Nothing moves; the button stops107 ;; name that differs set quieter than it. Nothing moves; the button stops
116 ;; shouting.108 ;; shouting.
117-(defn- open-button [name]
118- [:button {:label "Open"
119- :on-click #(s/open-channel! name)}])
120-
121-;; The only way out of a room. Everything else adds one — the server saying we
122-;; are in it, a message arriving in it — so without this the list only grows,
123-;; and what rooms.edn claims we are in could never shrink.
124-(defn- close-button [name]
125- [:button {:label "Close"
126- :on-click #(s/leave-channel! name)}])
127-
128-(defn- preview-line
129- "The last line of a conversation, as one line: a pasted shell script or a
130- long link is a card's worth of text otherwise, and the cards stop reading as
131- a list of rooms."
132- [text]
133- (let [line (str/replace (str text) #"\s+" " ")]
134- ;; 60 rather than a card's worth: at `sidebar-width` a 90-character line is
135- ;; three wrapped rows, and three rows of somebody else's last sentence is
136- ;; the row shouting over the name above it. One row of it says which
137- ;; conversation this is, which is all the list is for.
138- (if (> (count line) 60)
139- (str (subs line 0 59) "")
140- line)))
141-
142-;; What everything in the chats pane keeps clear of its right edge.
143-;;
144-;; It is one number because it is one edge: the scrollbar rides it, and the
145-;; cards in the list have to stop short of the bar rather than be drawn under
146-;; it — but the Join box above the list is outside the scroll and has no bar
147-;; beside it, so left to itself it ran on past the cards and the pane had two
148-;; right edges half a finger apart. Whatever the gap is, both of them take it.
149-(def ^:private list-gutter 16)
150-
151-(defn conversation-row [buffer]
152- (let [name (:name buffer)
153- ;; Defaulted rather than assumed. Everything that builds a buffer
154- ;; goes through `ensure-channel`, but the list draws whatever is in
155- ;; the atom, and a half-made room reaching here should be a row that
156- ;; reads as quiet rather than the frame that killed the client.
157- unread (:unread buffer 0)]
158- ;; The card in a wrapper that holds it off the right edge, where the
159- ;; list's scrollbar rides — `message-row` keeps its actions clear of the
160- ;; same edge for the same reason, and without it the cards are drawn
161- ;; under the bar rather than beside it. The wrapper and not the card
162- ;; itself: the window backend pads a card by a fixed 12 and never reads a
163- ;; margin off one, so the room has to be taken outside it.
164- [:vbox {:key name :margin-right list-gutter}
165- [:card {}
166- ;; The name gets the line, and membership and unread count the one under
167- ;; it. All three on one line is what the list pane has no room for: the
168- ;; name takes what it likes, the badges are left the remainder, and a
169- ;; long name squeezes them into a status reading "joine/d" and a count
170- ;; split down two lines — while the card, sized to a row that no longer
171- ;; fits, sticks out past the cards above and below it.
172- ;;
173- ;; In a terminal the Open button rides up onto the name's line. A row of
174- ;; chrome there is one cell, so a button on a line of its own costs the
175- ;; card a whole row — four of them and the list has spent a screenful on
176- ;; buttons. A window's row is 34 points and its button is a lozenge with
177- ;; air around it, which is why it keeps its own line there.
178- (if @terminal?
179- [:hbox {:spacing 8 :wrap false}
180- [:title-2 {:label name}]
181- [open-button name]
182- [close-button name]]
183- [:title-2 {:label name}])
184- ;; The badge line, and only when there is a badge: every row carrying a
185- ;; green "joined" was a row spending a third of its height saying the
186- ;; ordinary thing, and a list where every card says the same word is a
187- ;; list you read by skipping. So membership is reported when it is news —
188- ;; a room in the list we are not in — and a DM says nothing at all, since
189- ;; there is no membership to have and the name already says what it is.
190- ;;
191- ;; In a wrapper of its own, because the line comes and goes with the
192- ;; unread count and the reconciler numbers a card's children by position.
193- ;; `:wrap false` on the badges: they sit beside each other or not at all.
194- [:vbox {:key :badges}
195- (let [away? (and (not (s/dm? name)) (not (:joined? buffer)))]
196- (when (or away? (pos? unread))
197- [:hbox {:spacing 12 :wrap false}
198- (when away?
199- [:status {:label "not joined" :live false}])
200- ;; A mention is not more unread, it is different unread: the dot
201- ;; says how much there is and the name says it was aimed at you.
202- ;; Both or neither — a room with your name in it always has a line
203- ;; to count.
204- (when (pos? unread)
205- [:label {:label (str (if (:mention? buffer) " @ " " ") unread)}])]))]
206- [:dim-label {:label (preview-line (s/last-preview buffer))}]
207- (when-not @terminal?
208- [:hbox {:spacing 8 :wrap false}
209- [open-button name]
210- [close-button name]])]]))
211-
212 (def ^:private sidebar-width 320)109 (def ^:private sidebar-width 320)
213 110
214 ;; How tall a row of chrome is — a button, the compose bar, a line of tabs.111 ;; How tall a row of chrome is — a button, the compose bar, a line of tabs.
@@ -223,10 +120,12 @@
223 ;; backend whose rows are a different height says so here. Nothing else in the120 ;; backend whose rows are a different height says so here. Nothing else in the
224 ;; tree needs it: every other number is a length, and a length scales on the121 ;; tree needs it: every other number is a length, and a length scales on the
225 ;; way across.122 ;; way across.
226-(def ^:private window-row 34)123+(def ^:private window-row metrics/window-row)
227-(defonce chrome-row (atom window-row))124+;; Re-defined rather than moved out of reach: `frq.tui` resets this before its
125+;; first paint and says `app/chrome-row` when it does.
126+(def chrome-row metrics/chrome-row)
228 127
229-(defn- chrome-scale [] (/ (double @chrome-row) window-row))128+(def ^:private chrome-scale metrics/chrome-scale)
230 129
231 (defn- chip-gap130 (defn- chip-gap
232 "The air between two chips on a row.131 "The air between two chips on a row.
@@ -265,100 +164,6 @@
265 node]164 node]
266 node))165 node))
267 166
268-(defn- below-list []
269- ;; What the strip under the list needs, counted: the gap after the list, the
270- ;; separator, the gap under it, the tab bar's row of buttons and the margin
271- ;; below them. Short by any of it and the tabs go off the bottom edge — which
272- ;; is the thing this layout exists to stop.
273- (* (chrome-scale) (+ 8 8 8 34 12)))
274-
275-(defn chats-screen []
276- (let [buffers (s/channel-list)]
277- ;; Three bands rather than a page: the title and the two boxes at the top,
278- ;; the tabs at the bottom, and the conversations scrolling between them.
279- ;; Everything you act with stays where you last saw it — the same bargain
280- ;; the chat screen makes, where the compose bar holds still under a backlog
281- ;; that moves.
282- ;;
283- ;; What goes with the page is its centring at 620, on a window between that
284- ;; and the split view's 900: the list is full-bleed there now, which is
285- ;; what the conversation beside it has always been.
286- ;; `:fill-height` on the outermost box, not only on the band inside it.
287- ;; A backend that gives a column the height of what is in it hands the
288- ;; band below a window's worth of nothing to divide: the list asks for
289- ;; the rest of the screen, is told the rest is forty points, and the
290- ;; whole screen paints into a strip along the top with the window empty
291- ;; under it. Every screen that pins something to the bottom says this on
292- ;; its own root — the split view already said it, one wrapper further out.
293- [:vbox {:spacing 8 :margin 12 :fill-height true}
294- [:vbox {:key :head :spacing 8 :margin-right list-gutter}
295- ;; Your nick in the title, where "Chats" alone said nothing you did not
296- ;; already know — and said plainly, as a sign-in rather than as a name
297- ;; the list is somehow held in. It is what every channel calls you and
298- ;; what your own lines are signed with, and the only other place it
299- ;; showed was Settings.
300- [:title {:label (if (s/connected?)
301- (str "Logged in as " @s/form-nick)
302- "Chats")}]
303- [error-note]
304- ;; Join and search are the same shape — a channel box with a button
305- ;; beside it — so they read as one control block rather than a titled
306- ;; card and a stray row: same card, same widths, same gap. The screen
307- ;; title already says what the box is for, which is what the "Join
308- ;; channel" header was doing.
309- [:card {}
310- [:vbox {:spacing 8}
311- ;; Button first and `:align :end`, so the row is laid out from its
312- ;; right edge: the button is placed against that edge and the box takes
313- ;; what is left. An entry asks for whatever remains where it stands, so
314- ;; a box placed first takes the row and pushes the button onto a line of
315- ;; its own — Join under the box rather than beside it — and a box given
316- ;; a fixed width only holds that off until the window is narrower than
317- ;; the number. Laid out from the right the box is the part that gives,
318- ;; at every width, in the frame the drag happens rather than the one
319- ;; after the window is next measured.
320- [:hbox {:spacing 8 :align :end}
321- [:button {:label (if (str/starts-with? @s/join-input "@") "Message" "Join")
322- :kind :primary
323- :on-click #(do (s/join! @s/join-input) (reset! s/join-input ""))}]
324- ;; Both kinds of conversation through one box: `#room` joins a
325- ;; channel, `@nick` opens a message to a person.
326- [:entry {:text @s/join-input
327- :placeholder "#channel or @nick"
328- :on-change #(reset! s/join-input %)
329- :on-activate #(do (s/join! @s/join-input) (reset! s/join-input ""))}]]
330- ;; The clear button only shows while there is something to clear: an
331- ;; empty box has nothing to undo, and a dead button beside it reads as
332- ;; one that stopped working. It leads the row for the same reason the
333- ;; Join button does, and the box gives up the width it takes.
334- [:hbox {:spacing 8 :align :end}
335- (when (seq @s/search)
336- [:button {:label "✕" :on-click #(reset! s/search "")}])
337- [:entry {:text @s/search
338- :placeholder "Search channels"
339- :on-change #(reset! s/search %)}]]]]]
340- ;; `:fill-height` so the scroll inside is handed the rest of the window
341- ;; rather than the one card it starts out holding, and `:reserve` to keep
342- ;; the tabs' strip out of what it may take.
343- ;; Named, so it is the same list — and the same scroll position — when the
344- ;; reader comes back from a conversation.
345- [:vbox {:key :list :fill-height true}
346- [:scroll {:scroll-key "chats-list" :orientation :vertical
347- :reserve (below-list)}
348- (if (seq buffers)
349- ;; Keyed on the name, because this list reorders: opening a
350- ;; conversation bumps it to the top, and unkeyed children reconcile by
351- ;; position — the widgets stay put and their props are rewritten under
352- ;; them. What is focused is a widget, so the highlight would stay in
353- ;; the slot the reader clicked while the row that moved into it is
354- ;; someone else's conversation.
355- (for [b buffers] ^{:key (:name b)} [conversation-row b])
356- [:vbox {:margin-right list-gutter}
357- [:card {} [:dim-label {:label "No conversations yet — join a channel."}]]])]]
358- [:vbox {:key :foot :spacing 8}
359- [:separator {}]
360- [tab-bar]]]))
361-
362 ;; ---------------------------------------------------------------- chat167 ;; ---------------------------------------------------------------- chat
363 168
364 (def ^:private url-pattern #"https?://[^\s<>\"]+")169 (def ^:private url-pattern #"https?://[^\s<>\"]+")
modified src/frq/state.clj +16 -32
@@ -5,6 +5,7 @@
55 of `atom`s here; the IRC reader thread pushes into the same ones. `apply-msg!`
66 is the only place a wire message turns into UI state."
77 (:require [clojure.string :as str]
8+ [frq.rooms :as rooms]
89 [glimmer.ratom :as r :refer [atom]]
910 [frq.actions :as actions]
1011 [frq.cells :as cells]
@@ -47,6 +48,10 @@
4748 (def form-app-password cells/form-app-password)
4849 (def session cells/session)
4950 (def broker-token cells/broker-token)
51+(def channels cells/channels)
52+(def current cells/current)
53+(def join-input cells/join-input)
54+(def search cells/search)
5055 (def login-url cells/login-url)
5156
5257 (def popular-channels
@@ -64,9 +69,6 @@
6469 ;; How much backlog to ask for when the server did not volunteer any.
6570 (def history-limit 100)
6671
67-;; name -> {:name :messages [{:from :text}] :unread n :joined? bool}
68-(defonce channels (atom {}))
69-(defonce current (atom nil))
7072 (defonce draft (atom ""))
7173 ;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
7274 ;; rather than as an id alone so the compose bar can say who is being answered
@@ -94,7 +96,6 @@
9496 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,
9597 ;; so this is a screen of its own rather than a layer over the chat.
9698 (defonce lightbox (atom nil)) ; {:path :url}
97-(defonce join-input (atom ""))
9899
99100 ;; Whether the chat screen is showing who is in the channel. Off by default:
100101 ;; the panel costs the conversation a column, and the reader is here for the
@@ -202,7 +203,6 @@
202203 ;; A counter rather than a clock: the list only needs their order, and a
203204 ;; monotonic tick cannot be surprised by the system time moving.
204205 (defonce access-tick (atom 0))
205-(defonce search (atom ""))
206206
207207 ;; Comings and goings, hidden or not. A quiet room reads better with them —
208208 ;; they are how you notice someone arriving — and a busy one drowns in them,
@@ -269,11 +269,7 @@
269269 (reset! rooms-saved-at now)
270270 (future (store/save-rooms! (room-records)))))))
271271
272-(defn dm?
273- "Whether a buffer is a conversation with a person rather than a room. Every
274- channel name starts with `#`; what does not is somebody's nick."
275- [name]
276- (and (seq name) (not (str/starts-with? name "#"))))
272+(def dm? rooms/dm?)
277273
278274 (defn normalize-channel [s]
279275 (let [s (str/trim (or s ""))]
@@ -1833,20 +1829,7 @@
18331829 (when (seq session-id)
18341830 (irc/tagmsg! c channel (av/leave-tags session-id instance))))))
18351831
1836-(defn channel-list
1837- "Buffers most recently opened first, filtered by the search box.
1838-
1839- A conversation list is read from the top, and the one you were just in is the
1840- one you are most likely to want again. Buffers never opened — a DM that
1841- arrived, a channel someone mentioned — sort under those, by name, rather than
1842- jumping the queue."
1843- []
1844- (let [q (str/lower-case (str/trim @search))]
1845- (->> (vals @channels)
1846- (filter #(or (str/blank? q)
1847- (str/includes? (str/lower-case (:name %)) q)))
1848- (sort-by (juxt #(- (:accessed % 0)) :name))
1849- vec)))
1832+(def channel-list rooms/channel-list)
18501833
18511834 (defn channel-order
18521835 "The buffer names, most recently opened first — what gets written to disk.
@@ -1997,15 +1980,16 @@
19971980 (sort-by #(or (:at %) 0))
19981981 (take-last overview-limit)))
19991982
2000-(defn last-preview [buffer]
2001- (if-let [m (last (:messages buffer))]
2002- (str (:from m) ": " (:text m))
2003- "No messages yet"))
1983+(def last-preview rooms/last-preview)
20041984
2005-;; What the shared connect screen calls. Installed here rather than in an
2006-;; entry point because these are this namespace's own reducers, and the screen
2007-;; that calls them is no longer in a position to name them.
1985+;; What the shared screens call. Installed here rather than in an entry point
1986+;; because these are this namespace's own reducers, and the screens that call
1987+;; them are no longer in a position to name them.
20081988 (actions/install!
20091989 {:connect! connect!
20101990 :disconnect! disconnect!
2011- :forget-session! forget-session!})
1991+ :forget-session! forget-session!
1992+ :connected? connected?
1993+ :join! join!
1994+ :open-channel! open-channel!
1995+ :leave-channel! leave-channel!})
@@ -5,6 +5,7 @@
5 of `atom`s here; the IRC reader thread pushes into the same ones. `apply-msg!`5 of `atom`s here; the IRC reader thread pushes into the same ones. `apply-msg!`
6 is the only place a wire message turns into UI state."6 is the only place a wire message turns into UI state."
7 (:require [clojure.string :as str]7 (:require [clojure.string :as str]
8+ [frq.rooms :as rooms]
8 [glimmer.ratom :as r :refer [atom]]9 [glimmer.ratom :as r :refer [atom]]
9 [frq.actions :as actions]10 [frq.actions :as actions]
10 [frq.cells :as cells]11 [frq.cells :as cells]
@@ -47,6 +48,10 @@
47 (def form-app-password cells/form-app-password)48 (def form-app-password cells/form-app-password)
48 (def session cells/session)49 (def session cells/session)
49 (def broker-token cells/broker-token)50 (def broker-token cells/broker-token)
51+(def channels cells/channels)
52+(def current cells/current)
53+(def join-input cells/join-input)
54+(def search cells/search)
50 (def login-url cells/login-url)55 (def login-url cells/login-url)
51 56
52 (def popular-channels57 (def popular-channels
@@ -64,9 +69,6 @@
64 ;; How much backlog to ask for when the server did not volunteer any.69 ;; How much backlog to ask for when the server did not volunteer any.
65 (def history-limit 100)70 (def history-limit 100)
66 71
67-;; name -> {:name :messages [{:from :text}] :unread n :joined? bool}
68-(defonce channels (atom {}))
69-(defonce current (atom nil))
70 (defonce draft (atom ""))72 (defonce draft (atom ""))
71 ;; The message the draft is answering, as {:id :from :text}, or nil. Held whole73 ;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
72 ;; rather than as an id alone so the compose bar can say who is being answered74 ;; rather than as an id alone so the compose bar can say who is being answered
@@ -94,7 +96,6 @@
94 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,96 ;; The picture being looked at full size, or nil. Vidya's tree has no overlay,
95 ;; so this is a screen of its own rather than a layer over the chat.97 ;; so this is a screen of its own rather than a layer over the chat.
96 (defonce lightbox (atom nil)) ; {:path :url}98 (defonce lightbox (atom nil)) ; {:path :url}
97-(defonce join-input (atom ""))
98 99
99 ;; Whether the chat screen is showing who is in the channel. Off by default:100 ;; Whether the chat screen is showing who is in the channel. Off by default:
100 ;; the panel costs the conversation a column, and the reader is here for the101 ;; the panel costs the conversation a column, and the reader is here for the
@@ -202,7 +203,6 @@
202 ;; A counter rather than a clock: the list only needs their order, and a203 ;; A counter rather than a clock: the list only needs their order, and a
203 ;; monotonic tick cannot be surprised by the system time moving.204 ;; monotonic tick cannot be surprised by the system time moving.
204 (defonce access-tick (atom 0))205 (defonce access-tick (atom 0))
205-(defonce search (atom ""))
206 206
207 ;; Comings and goings, hidden or not. A quiet room reads better with them —207 ;; Comings and goings, hidden or not. A quiet room reads better with them —
208 ;; they are how you notice someone arriving — and a busy one drowns in them,208 ;; they are how you notice someone arriving — and a busy one drowns in them,
@@ -269,11 +269,7 @@
269 (reset! rooms-saved-at now)269 (reset! rooms-saved-at now)
270 (future (store/save-rooms! (room-records)))))))270 (future (store/save-rooms! (room-records)))))))
271 271
272-(defn dm?272+(def dm? rooms/dm?)
273- "Whether a buffer is a conversation with a person rather than a room. Every
274- channel name starts with `#`; what does not is somebody's nick."
275- [name]
276- (and (seq name) (not (str/starts-with? name "#"))))
277 273
278 (defn normalize-channel [s]274 (defn normalize-channel [s]
279 (let [s (str/trim (or s ""))]275 (let [s (str/trim (or s ""))]
@@ -1833,20 +1829,7 @@
1833 (when (seq session-id)1829 (when (seq session-id)
1834 (irc/tagmsg! c channel (av/leave-tags session-id instance))))))1830 (irc/tagmsg! c channel (av/leave-tags session-id instance))))))
1835 1831
1836-(defn channel-list1832+(def channel-list rooms/channel-list)
1837- "Buffers most recently opened first, filtered by the search box.
1838-
1839- A conversation list is read from the top, and the one you were just in is the
1840- one you are most likely to want again. Buffers never opened — a DM that
1841- arrived, a channel someone mentioned — sort under those, by name, rather than
1842- jumping the queue."
1843- []
1844- (let [q (str/lower-case (str/trim @search))]
1845- (->> (vals @channels)
1846- (filter #(or (str/blank? q)
1847- (str/includes? (str/lower-case (:name %)) q)))
1848- (sort-by (juxt #(- (:accessed % 0)) :name))
1849- vec)))
1850 1833
1851 (defn channel-order1834 (defn channel-order
1852 "The buffer names, most recently opened first — what gets written to disk.1835 "The buffer names, most recently opened first — what gets written to disk.
@@ -1997,15 +1980,16 @@
1997 (sort-by #(or (:at %) 0))1980 (sort-by #(or (:at %) 0))
1998 (take-last overview-limit)))1981 (take-last overview-limit)))
1999 1982
2000-(defn last-preview [buffer]1983+(def last-preview rooms/last-preview)
2001- (if-let [m (last (:messages buffer))]
2002- (str (:from m) ": " (:text m))
2003- "No messages yet"))
2004 1984
2005-;; What the shared connect screen calls. Installed here rather than in an1985+;; What the shared screens call. Installed here rather than in an entry point
2006-;; entry point because these are this namespace's own reducers, and the screen1986+;; because these are this namespace's own reducers, and the screens that call
2007-;; that calls them is no longer in a position to name them.1987+;; them are no longer in a position to name them.
2008 (actions/install!1988 (actions/install!
2009 {:connect! connect!1989 {:connect! connect!
2010 :disconnect! disconnect!1990 :disconnect! disconnect!
2011- :forget-session! forget-session!})1991+ :forget-session! forget-session!
1992+ :connected? connected?
1993+ :join! join!
1994+ :open-channel! open-channel!
1995+ :leave-channel! leave-channel!})