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

Move the chat screen's cells and actions ahead of the screen

Nine cells — the draft and its three companions, the jump tick, and the four
toggles — into frq.cells, and the conversation screen's fourteen commands and
two queries into frq.actions beside them. Also the call bar's three, which a
phone answers by not installing, and the two assets: where a face or a picture
is on disk once it is.

Nothing moved yet that draws anything. The screen follows; this is the ground
it stands on, and the TUI renders unchanged over it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-11T22:07:00-07:00 Browse files
0addab3 parent: 8c903d8
modified common/frq/actions.cljc +44 -0
@@ -37,3 +37,47 @@
3737 (defn join! [name] (call :join! [name]))
3838 (defn open-channel! [name] (call :open-channel! [name]))
3939 (defn leave-channel! [name] (call :leave-channel! [name]))
40+
41+;; ------------------------------------------------------------------- chat
42+;;
43+;; The conversation screen's half. Commands where the screen asks for
44+;; something to happen, queries where it asks what is true — `wide?` is the
45+;; window's width and `member-count` is the server's answer, and neither is a
46+;; cell any screen could read for itself.
47+
48+(defn send-draft! [] (call :send-draft! []))
49+(defn cancel-edit! [] (call :cancel-edit! []))
50+(defn cancel-reply! [] (call :cancel-reply! []))
51+(defn clear-attachment! [] (call :clear-attachment! []))
52+(defn open-image-picker! [] (call :open-image-picker! []))
53+(defn paste-image! [] (call :paste-image! []))
54+(defn jump-to-present! [] (call :jump-to-present! []))
55+(defn scrolled! [& args] (call :scrolled! args))
56+(defn toggle-users! [] (call :toggle-users! []))
57+(defn toggle-chat-list! [] (call :toggle-chat-list! []))
58+(defn toggle-overview! [] (call :toggle-overview! []))
59+
60+(defn wide? [] (boolean (call :wide? [])))
61+(defn member-count [name] (call :member-count [name]))
62+
63+;; ------------------------------------------------------------------ calls
64+;;
65+;; The media plane, which a phone has none of — `frq.av/available?` says so on
66+;; the desktop and nothing installs these here, so the call bar renders its
67+;; unavailable shape rather than being special-cased in the screen.
68+
69+(defn start-call! [] (call :start-call! []))
70+(defn in-call? [] (boolean (call :in-call? [])))
71+(defn call-in [name] (call :call-in [name]))
72+(defn call-available? [] (boolean (call :call-available? [])))
73+
74+;; ----------------------------------------------------------------- assets
75+;;
76+;; Where a face or a picture is on disk, once it is. Both are a fetch and a
77+;; cache on the desktop, behind a glimmer reaction so one arriving wakes only
78+;; the rows that read it; the phone has neither yet and answers nil, which the
79+;; screens already treat as "not here".
80+
81+(defn avatar-path [actor] (call :avatar-path [actor]))
82+(defn image-path [url] (call :image-path [url]))
83+
@@ -37,3 +37,47 @@
37 (defn join! [name] (call :join! [name]))37 (defn join! [name] (call :join! [name]))
38 (defn open-channel! [name] (call :open-channel! [name]))38 (defn open-channel! [name] (call :open-channel! [name]))
39 (defn leave-channel! [name] (call :leave-channel! [name]))39 (defn leave-channel! [name] (call :leave-channel! [name]))
40+
41+;; ------------------------------------------------------------------- chat
42+;;
43+;; The conversation screen's half. Commands where the screen asks for
44+;; something to happen, queries where it asks what is true — `wide?` is the
45+;; window's width and `member-count` is the server's answer, and neither is a
46+;; cell any screen could read for itself.
47+
48+(defn send-draft! [] (call :send-draft! []))
49+(defn cancel-edit! [] (call :cancel-edit! []))
50+(defn cancel-reply! [] (call :cancel-reply! []))
51+(defn clear-attachment! [] (call :clear-attachment! []))
52+(defn open-image-picker! [] (call :open-image-picker! []))
53+(defn paste-image! [] (call :paste-image! []))
54+(defn jump-to-present! [] (call :jump-to-present! []))
55+(defn scrolled! [& args] (call :scrolled! args))
56+(defn toggle-users! [] (call :toggle-users! []))
57+(defn toggle-chat-list! [] (call :toggle-chat-list! []))
58+(defn toggle-overview! [] (call :toggle-overview! []))
59+
60+(defn wide? [] (boolean (call :wide? [])))
61+(defn member-count [name] (call :member-count [name]))
62+
63+;; ------------------------------------------------------------------ calls
64+;;
65+;; The media plane, which a phone has none of — `frq.av/available?` says so on
66+;; the desktop and nothing installs these here, so the call bar renders its
67+;; unavailable shape rather than being special-cased in the screen.
68+
69+(defn start-call! [] (call :start-call! []))
70+(defn in-call? [] (boolean (call :in-call? [])))
71+(defn call-in [name] (call :call-in [name]))
72+(defn call-available? [] (boolean (call :call-available? [])))
73+
74+;; ----------------------------------------------------------------- assets
75+;;
76+;; Where a face or a picture is on disk, once it is. Both are a fetch and a
77+;; cache on the desktop, behind a glimmer reaction so one arriving wakes only
78+;; the rows that read it; the phone has neither yet and answers nil, which the
79+;; screens already treat as "not here".
80+
81+(defn avatar-path [actor] (call :avatar-path [actor]))
82+(defn image-path [url] (call :image-path [url]))
83+
modified common/frq/cells.cljc +62 -0
@@ -52,3 +52,65 @@
5252
5353 (defonce broker-token (atom nil))
5454 (defonce login-url (atom nil)) ; shown while the browser is open
55+
56+;; ------------------------------------------------------------------- chat
57+;; What the conversation screen reads. The compose bar's three companions —
58+;; a reply, an edit and an attachment are apart from the draft rather than
59+;; written into it: a link pasted into the box is a line of unreadable text in
60+;; the middle of whatever the reader was typing.
61+
62+(defonce draft (atom ""))
63+
64+;; The message the draft is a rewrite of, as `{:channel :id}`, or nil when the
65+;; box is being used for something new. Only the id is kept: what is being
66+;; rewritten is in the box, and the line on screen is the thing it will replace.
67+(defonce editing (atom nil))
68+
69+;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
70+;; rather than as an id alone so the compose bar can say who is being answered
71+;; without going looking for them.
72+(defonce replying-to (atom nil))
73+
74+;; The picture waiting to go out with the next line, or nil:
75+;;
76+;; {:path the copy on disk, which is what the preview paints
77+;; :url where freeq serves it, once the upload has landed
78+;; :status :uploading | :ready}
79+;;
80+;; Held apart from the draft rather than written into it. A link pasted into
81+;; the entry is a line of unreadable text in the middle of whatever the reader
82+;; was typing, and it puts their cursor somewhere they did not put it. The
83+;; picture is a picture until it is sent; the draft stays theirs.
84+;;
85+;; One at a time a second paste replaces the first, which is what a reader
86+;; who pasted the wrong thing means by pasting the right one.
87+(defonce attachment (atom nil))
88+
89+(defonce jump-tick (atom 0))
90+
91+;; Whether the chat screen is showing who is in the channel. Off by default:
92+;; the panel costs the conversation a column, and the reader is here for the
93+;; conversation.
94+(defonce show-users? (atom false))
95+
96+;; Whether the wide window is holding the chats list back, leaving the whole
97+;; row to the conversation. Only a wide window has anything to hide: below
98+;; `wide-width` the list and the conversation already take turns, and hiding
99+;; the list there would be hiding the only way to another room.
100+;;
101+;; Saved with the other settings rather than reset per launch: it is a choice
102+;; about how this screen is read, and a reader who wants the conversation
103+;; whole wants it whole again tomorrow.
104+(defonce hide-chat-list? (atom false))
105+
106+;; Whether the conversation is sharing its room with the overview strip
107+;; every channel's last lines in one list, under the one you are reading.
108+;; Saved for the same reason as the fold above it: it is a choice about how
109+;; this screen is laid out, and a layout a reader chose should be the one they
110+;; come back to.
111+(defonce overview? (atom false))
112+
113+;; Whether the chat view is showing the newest line, and a counter the view
114+;; watches to be told to go back to it. A counter rather than a flag: a flag
115+;; would need clearing, and there is no frame in which to clear it.
116+(defonce at-present? (atom true))
@@ -52,3 +52,65 @@
52 52
53 (defonce broker-token (atom nil))53 (defonce broker-token (atom nil))
54 (defonce login-url (atom nil)) ; shown while the browser is open54 (defonce login-url (atom nil)) ; shown while the browser is open
55+
56+;; ------------------------------------------------------------------- chat
57+;; What the conversation screen reads. The compose bar's three companions —
58+;; a reply, an edit and an attachment are apart from the draft rather than
59+;; written into it: a link pasted into the box is a line of unreadable text in
60+;; the middle of whatever the reader was typing.
61+
62+(defonce draft (atom ""))
63+
64+;; The message the draft is a rewrite of, as `{:channel :id}`, or nil when the
65+;; box is being used for something new. Only the id is kept: what is being
66+;; rewritten is in the box, and the line on screen is the thing it will replace.
67+(defonce editing (atom nil))
68+
69+;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
70+;; rather than as an id alone so the compose bar can say who is being answered
71+;; without going looking for them.
72+(defonce replying-to (atom nil))
73+
74+;; The picture waiting to go out with the next line, or nil:
75+;;
76+;; {:path the copy on disk, which is what the preview paints
77+;; :url where freeq serves it, once the upload has landed
78+;; :status :uploading | :ready}
79+;;
80+;; Held apart from the draft rather than written into it. A link pasted into
81+;; the entry is a line of unreadable text in the middle of whatever the reader
82+;; was typing, and it puts their cursor somewhere they did not put it. The
83+;; picture is a picture until it is sent; the draft stays theirs.
84+;;
85+;; One at a time a second paste replaces the first, which is what a reader
86+;; who pasted the wrong thing means by pasting the right one.
87+(defonce attachment (atom nil))
88+
89+(defonce jump-tick (atom 0))
90+
91+;; Whether the chat screen is showing who is in the channel. Off by default:
92+;; the panel costs the conversation a column, and the reader is here for the
93+;; conversation.
94+(defonce show-users? (atom false))
95+
96+;; Whether the wide window is holding the chats list back, leaving the whole
97+;; row to the conversation. Only a wide window has anything to hide: below
98+;; `wide-width` the list and the conversation already take turns, and hiding
99+;; the list there would be hiding the only way to another room.
100+;;
101+;; Saved with the other settings rather than reset per launch: it is a choice
102+;; about how this screen is read, and a reader who wants the conversation
103+;; whole wants it whole again tomorrow.
104+(defonce hide-chat-list? (atom false))
105+
106+;; Whether the conversation is sharing its room with the overview strip
107+;; every channel's last lines in one list, under the one you are reading.
108+;; Saved for the same reason as the fold above it: it is a choice about how
109+;; this screen is laid out, and a layout a reader chose should be the one they
110+;; come back to.
111+(defonce overview? (atom false))
112+
113+;; Whether the chat view is showing the newest line, and a counter the view
114+;; watches to be told to go back to it. A counter rather than a flag: a flag
115+;; would need clearing, and there is no frame in which to clear it.
116+(defonce at-present? (atom true))
modified src/frq/state.clj +27 -48
@@ -69,19 +69,13 @@
6969 ;; How much backlog to ask for when the server did not volunteer any.
7070 (def history-limit 100)
7171
72-(defonce draft (atom ""))
73-;; The message the draft is answering, as {:id :from :text}, or nil. Held whole
74-;; rather than as an id alone so the compose bar can say who is being answered
75-;; without going looking for them.
76-(defonce replying-to (atom nil))
72+(def draft cells/draft)
73+(def replying-to cells/replying-to)
7774
7875 (defn reply-to! [m] (reset! replying-to (select-keys m [:id :from :text])))
7976 (defn cancel-reply! [] (reset! replying-to nil))
8077
81-;; The message the draft is a rewrite of, as `{:channel :id}`, or nil when the
82-;; box is being used for something new. Only the id is kept: what is being
83-;; rewritten is in the box, and the line on screen is the thing it will replace.
84-(defonce editing (atom nil))
78+(def editing cells/editing)
8579
8680 ;; The message the emoji picker is choosing for, as `{:channel :id}`, or nil
8781 ;; when it is closed. The picker is a panel over the compose bar rather than a
@@ -97,22 +91,11 @@
9791 ;; so this is a screen of its own rather than a layer over the chat.
9892 (defonce lightbox (atom nil)) ; {:path :url}
9993
100-;; Whether the chat screen is showing who is in the channel. Off by default:
101-;; the panel costs the conversation a column, and the reader is here for the
102-;; conversation.
103-(defonce show-users? (atom false))
94+(def show-users? cells/show-users?)
10495
10596 (defn toggle-users! [] (swap! show-users? not))
10697
107-;; Whether the wide window is holding the chats list back, leaving the whole
108-;; row to the conversation. Only a wide window has anything to hide: below
109-;; `wide-width` the list and the conversation already take turns, and hiding
110-;; the list there would be hiding the only way to another room.
111-;;
112-;; Saved with the other settings rather than reset per launch: it is a choice
113-;; about how this screen is read, and a reader who wants the conversation
114-;; whole wants it whole again tomorrow.
115-(defonce hide-chat-list? (atom false))
98+(def hide-chat-list? cells/hide-chat-list?)
11699
117100 (declare save-prefs!)
118101
@@ -120,12 +103,7 @@
120103 (swap! hide-chat-list? not)
121104 (save-prefs!))
122105
123-;; Whether the conversation is sharing its room with the overview strip —
124-;; every channel's last lines in one list, under the one you are reading.
125-;; Saved for the same reason as the fold above it: it is a choice about how
126-;; this screen is laid out, and a layout a reader chose should be the one they
127-;; come back to.
128-(defonce overview? (atom false))
106+(def overview? cells/overview?)
129107
130108 (defn toggle-overview! []
131109 (swap! overview? not)
@@ -189,11 +167,8 @@
189167 (or (= :chat @screen)
190168 (and (wide?) (= :chats @screen))))
191169
192-;; Whether the chat view is showing the newest line, and a counter the view
193-;; watches to be told to go back to it. A counter rather than a flag: a flag
194-;; would need clearing, and there is no frame in which to clear it.
195-(defonce at-present? (atom true))
196-(defonce jump-tick (atom 0))
170+(def at-present? cells/at-present?)
171+(def jump-tick cells/jump-tick)
197172
198173 ;; How a backend answers "did that scroll end at the end?".
199174 ;;
@@ -1340,20 +1315,7 @@
13401315
13411316 ;; ------------------------------------------------------------------ pasting
13421317
1343-;; The picture waiting to go out with the next line, or nil:
1344-;;
1345-;; {:path the copy on disk, which is what the preview paints
1346-;; :url where freeq serves it, once the upload has landed
1347-;; :status :uploading | :ready}
1348-;;
1349-;; Held apart from the draft rather than written into it. A link pasted into
1350-;; the entry is a line of unreadable text in the middle of whatever the reader
1351-;; was typing, and it puts their cursor somewhere they did not put it. The
1352-;; picture is a picture until it is sent; the draft stays theirs.
1353-;;
1354-;; One at a time — a second paste replaces the first, which is what a reader
1355-;; who pasted the wrong thing means by pasting the right one.
1356-(defonce attachment (atom nil))
1318+(def attachment cells/attachment)
13571319
13581320 ;; Each paste gets a file of its own rather than overwriting the last: the
13591321 ;; preview is painted from the file, and an upload may still be reading it.
@@ -2040,4 +2002,21 @@
20402002 :connected? connected?
20412003 :join! join!
20422004 :open-channel! open-channel!
2043- :leave-channel! leave-channel!})
2005+ :leave-channel! leave-channel!
2006+ :send-draft! send-draft!
2007+ :cancel-edit! cancel-edit!
2008+ :cancel-reply! cancel-reply!
2009+ :clear-attachment! clear-attachment!
2010+ :open-image-picker! open-image-picker!
2011+ :paste-image! paste-image!
2012+ :jump-to-present! jump-to-present!
2013+ :scrolled! scrolled!
2014+ :toggle-users! toggle-users!
2015+ :toggle-chat-list! toggle-chat-list!
2016+ :toggle-overview! toggle-overview!
2017+ :wide? wide?
2018+ :member-count member-count
2019+ :start-call! start-call!
2020+ :in-call? av/in-call?
2021+ :call-in av/call-in
2022+ :call-available? av/available?})
@@ -69,19 +69,13 @@
69 ;; 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.
70 (def history-limit 100)70 (def history-limit 100)
71 71
72-(defonce draft (atom ""))72+(def draft cells/draft)
73-;; The message the draft is answering, as {:id :from :text}, or nil. Held whole73+(def replying-to cells/replying-to)
74-;; rather than as an id alone so the compose bar can say who is being answered
75-;; without going looking for them.
76-(defonce replying-to (atom nil))
77 74
78 (defn reply-to! [m] (reset! replying-to (select-keys m [:id :from :text])))75 (defn reply-to! [m] (reset! replying-to (select-keys m [:id :from :text])))
79 (defn cancel-reply! [] (reset! replying-to nil))76 (defn cancel-reply! [] (reset! replying-to nil))
80 77
81-;; The message the draft is a rewrite of, as `{:channel :id}`, or nil when the78+(def editing cells/editing)
82-;; box is being used for something new. Only the id is kept: what is being
83-;; rewritten is in the box, and the line on screen is the thing it will replace.
84-(defonce editing (atom nil))
85 79
86 ;; The message the emoji picker is choosing for, as `{:channel :id}`, or nil80 ;; The message the emoji picker is choosing for, as `{:channel :id}`, or nil
87 ;; when it is closed. The picker is a panel over the compose bar rather than a81 ;; when it is closed. The picker is a panel over the compose bar rather than a
@@ -97,22 +91,11 @@
97 ;; so this is a screen of its own rather than a layer over the chat.91 ;; so this is a screen of its own rather than a layer over the chat.
98 (defonce lightbox (atom nil)) ; {:path :url}92 (defonce lightbox (atom nil)) ; {:path :url}
99 93
100-;; Whether the chat screen is showing who is in the channel. Off by default:94+(def show-users? cells/show-users?)
101-;; the panel costs the conversation a column, and the reader is here for the
102-;; conversation.
103-(defonce show-users? (atom false))
104 95
105 (defn toggle-users! [] (swap! show-users? not))96 (defn toggle-users! [] (swap! show-users? not))
106 97
107-;; Whether the wide window is holding the chats list back, leaving the whole98+(def hide-chat-list? cells/hide-chat-list?)
108-;; row to the conversation. Only a wide window has anything to hide: below
109-;; `wide-width` the list and the conversation already take turns, and hiding
110-;; the list there would be hiding the only way to another room.
111-;;
112-;; Saved with the other settings rather than reset per launch: it is a choice
113-;; about how this screen is read, and a reader who wants the conversation
114-;; whole wants it whole again tomorrow.
115-(defonce hide-chat-list? (atom false))
116 99
117 (declare save-prefs!)100 (declare save-prefs!)
118 101
@@ -120,12 +103,7 @@
120 (swap! hide-chat-list? not)103 (swap! hide-chat-list? not)
121 (save-prefs!))104 (save-prefs!))
122 105
123-;; Whether the conversation is sharing its room with the overview strip —106+(def overview? cells/overview?)
124-;; every channel's last lines in one list, under the one you are reading.
125-;; Saved for the same reason as the fold above it: it is a choice about how
126-;; this screen is laid out, and a layout a reader chose should be the one they
127-;; come back to.
128-(defonce overview? (atom false))
129 107
130 (defn toggle-overview! []108 (defn toggle-overview! []
131 (swap! overview? not)109 (swap! overview? not)
@@ -189,11 +167,8 @@
189 (or (= :chat @screen)167 (or (= :chat @screen)
190 (and (wide?) (= :chats @screen))))168 (and (wide?) (= :chats @screen))))
191 169
192-;; Whether the chat view is showing the newest line, and a counter the view170+(def at-present? cells/at-present?)
193-;; watches to be told to go back to it. A counter rather than a flag: a flag171+(def jump-tick cells/jump-tick)
194-;; would need clearing, and there is no frame in which to clear it.
195-(defonce at-present? (atom true))
196-(defonce jump-tick (atom 0))
197 172
198 ;; How a backend answers "did that scroll end at the end?".173 ;; How a backend answers "did that scroll end at the end?".
199 ;;174 ;;
@@ -1340,20 +1315,7 @@
1340 1315
1341 ;; ------------------------------------------------------------------ pasting1316 ;; ------------------------------------------------------------------ pasting
1342 1317
1343-;; The picture waiting to go out with the next line, or nil:1318+(def attachment cells/attachment)
1344-;;
1345-;; {:path the copy on disk, which is what the preview paints
1346-;; :url where freeq serves it, once the upload has landed
1347-;; :status :uploading | :ready}
1348-;;
1349-;; Held apart from the draft rather than written into it. A link pasted into
1350-;; the entry is a line of unreadable text in the middle of whatever the reader
1351-;; was typing, and it puts their cursor somewhere they did not put it. The
1352-;; picture is a picture until it is sent; the draft stays theirs.
1353-;;
1354-;; One at a time — a second paste replaces the first, which is what a reader
1355-;; who pasted the wrong thing means by pasting the right one.
1356-(defonce attachment (atom nil))
1357 1319
1358 ;; Each paste gets a file of its own rather than overwriting the last: the1320 ;; Each paste gets a file of its own rather than overwriting the last: the
1359 ;; preview is painted from the file, and an upload may still be reading it.1321 ;; preview is painted from the file, and an upload may still be reading it.
@@ -2040,4 +2002,21 @@
2040 :connected? connected?2002 :connected? connected?
2041 :join! join!2003 :join! join!
2042 :open-channel! open-channel!2004 :open-channel! open-channel!
2043- :leave-channel! leave-channel!})2005+ :leave-channel! leave-channel!
2006+ :send-draft! send-draft!
2007+ :cancel-edit! cancel-edit!
2008+ :cancel-reply! cancel-reply!
2009+ :clear-attachment! clear-attachment!
2010+ :open-image-picker! open-image-picker!
2011+ :paste-image! paste-image!
2012+ :jump-to-present! jump-to-present!
2013+ :scrolled! scrolled!
2014+ :toggle-users! toggle-users!
2015+ :toggle-chat-list! toggle-chat-list!
2016+ :toggle-overview! toggle-overview!
2017+ :wide? wide?
2018+ :member-count member-count
2019+ :start-call! start-call!
2020+ :in-call? av/in-call?
2021+ :call-in av/call-in
2022+ :call-available? av/available?})