Make the overview a second conversation, and give a jump somewhere to land
The strip under a conversation holds a hundred lines now and scrolls, which is a horizontal split rather than a caption: two children of a column that both fill it are two halves of it, each keeping its own scroll. A terminal keeps the fixed handful it had — its rows are cells, and two scrolling panes in a screen that is already a conversation, a compose bar and a tab bar leaves neither half worth reading. Which hundred is a turn to each room until they run out. The newest hundred outright would be the strip reporting on whichever room is busiest, and that is the room you can already see; a turn each means the room that said one thing all day is in the first handful, beside the room that has said a hundred. Not the room being read, which is on the screen already and in full. Newest at the top, which is the other way round from a conversation and right for the same reason a conversation is the way it is: what you came for is what has just happened. Days are ruled off as they are in the backlog, and needed more here — these lines come from rooms last spoken in at their own times, so ten rows can cross a day twice. Pressing a room chip goes to that line rather than to the room's end, and the way back to where you were is in the strip's heading. It comes and goes in a slot that holds its height either way, so the lines under it do not move when it appears. The line itself is read rather than pressed: it was a link for a moment, which made half the rows accent-coloured and half plain, since a line the server has echoed back has an id to aim at and one this client has just sent does not. A jump crossing a room is given longer to land and longer to stay marked: the screen has to be built before there is anything to scroll, and what the reader arrives at is a conversation they were not in a moment ago. And two pieces of furniture are gone, both of which were drawing a line under something nobody was going to miss: the rule above the compose bar, and the row held open under the backlog for a button that is usually not there. The three banners under it share one wrapper now — each was an empty row costing the column a gap of its own, which is two dozen points of nothing between the strip and the compose bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
478a726 parent: 1f55e68 modified
src/frq/app.clj +189 -72 | @@ -260,6 +260,33 @@ | ||
| 260 | 260 | (str (subs text 0 (dec limit)) "…") |
| 261 | 261 | text))) |
| 262 | 262 | |
| 263 | +(defn- goto-message! | |
| 264 | + "Show message `id`, in `channel`, and say which one it was. | |
| 265 | + | |
| 266 | + Three things in a fixed order: be in the room, aim the scroll at the line, | |
| 267 | + and mark it once it is there. `jump-to` comes off again as soon as the frame | |
| 268 | + that scrolled has been painted — a scroll target that stays set pins the | |
| 269 | + view and takes scrolling away from the reader — while the highlight outlives | |
| 270 | + it, because arriving at a screenful of messages says nothing about which one | |
| 271 | + was asked for. | |
| 272 | + | |
| 273 | + `settle` is how long that first frame is given, and `linger` how long the | |
| 274 | + mark stays. Within a room the scrolling frame is the next one and the line | |
| 275 | + is somewhere the reader was already looking. Crossing a room is a slower | |
| 276 | + and a stranger arrival — the screen has to be built before there is | |
| 277 | + anything to scroll, and what it opens on is a conversation the reader was | |
| 278 | + not in a moment ago — so a caller that crosses one asks for more of both." | |
| 279 | + [channel id settle linger] | |
| 280 | + (when (and channel (not= channel @s/current)) | |
| 281 | + (s/open-channel! channel)) | |
| 282 | + (reset! s/jump-to id) | |
| 283 | + (reset! s/highlight id) | |
| 284 | + (platform/after! settle (fn [] (reset! s/jump-to nil))) | |
| 285 | + ;; The highlight only clears itself: a later jump elsewhere owns the | |
| 286 | + ;; highlight from then on. | |
| 287 | + (platform/after! linger (fn [] (when (= id @s/highlight) | |
| 288 | + (reset! s/highlight nil))))) | |
| 289 | + | |
| 263 | 290 | (defn- reply-chip |
| 264 | 291 | "What a message is replying to, above it, and a way back to it. |
| 265 | 292 | |
| @@ -273,18 +300,9 @@ | ||
| 273 | 300 | ;; action, and a filled pill above every answer was the loudest thing in |
| 274 | 301 | ;; the column. |
| 275 | 302 | [:link {:label (str "↩ " (:from target) ": " (summarise target 48)) |
| 276 | - :on-click #(do (reset! s/jump-to id) | |
| 277 | - (reset! s/highlight id) | |
| 278 | - ;; Off again once the frame that scrolled has | |
| 279 | - ;; been painted, so the reader keeps the view. | |
| 280 | - (platform/after! 120 (fn [] (reset! s/jump-to nil))) | |
| 281 | - ;; The highlight stays long enough to be read, | |
| 282 | - ;; and only clears itself: a later jump elsewhere | |
| 283 | - ;; owns the highlight from then on. | |
| 284 | - (platform/after! 2000 | |
| 285 | - (fn [] | |
| 286 | - (when (= id @s/highlight) | |
| 287 | - (reset! s/highlight nil)))))}] | |
| 303 | + ;; The line being answered is in the room already open, so the | |
| 304 | + ;; next frame is the one that scrolls. | |
| 305 | + :on-click #(goto-message! channel id 120 2000)}] | |
| 288 | 306 | ;; The message it answers is older than this buffer goes. |
| 289 | 307 | [:dim-label {:label "↩ replying to an earlier message"}])]) |
| 290 | 308 | |
| @@ -780,11 +798,7 @@ | ||
| 780 | 798 | |
| 781 | 799 | (= :failed (:status pr)) |
| 782 | 800 | [:dim-label {:label "No Bluesky profile found"}])] |
| 783 | - ;; What the pointer is holding open says how to stop having to hold it. | |
| 784 | - ;; Pinned, that line has nothing left to tell anyone. | |
| 785 | - [:vbox {:key :hint} | |
| 786 | - (when-not pinned? | |
| 787 | - [:dim-label {:label "Click the face to keep this open"}])]] | |
| 801 | +] | |
| 788 | 802 | ;; `slot` is where libcosmic puts a button: the two actions go to the |
| 789 | 803 | ;; foot of the dialog, and anything else here would be another control |
| 790 | 804 | ;; stacked in the body. |
| @@ -1103,21 +1117,29 @@ | ||
| 1103 | 1117 | ;; what every other row under the message list has had to say for itself. |
| 1104 | 1118 | ;; |
| 1105 | 1119 | ;; Counted from the lines there actually are, not from the ceiling: the strip |
| 1106 | -;; holds a few per room, so its height is the client's rooms, and reserving | |
| 1107 | -;; for a full one in a client with two channels in it would take a third of | |
| 1108 | -;; the window off the conversation to leave it empty. | |
| 1120 | +;; holds at most `overview-lines` of them there, and a room with two lines in | |
| 1121 | +;; it should not have a screenful reserved against it. | |
| 1122 | +;; | |
| 1123 | +;; The window reserves nothing for the overview. Its pane scrolls and fills, | |
| 1124 | +;; so it takes its half of the column by being in it — there is no number to | |
| 1125 | +;; subtract, and subtracting one would take the room twice. | |
| 1109 | 1126 | (defn- overview-height [] |
| 1110 | - (* (chrome-scale) (+ 24 (* 20 (count (s/recent-everywhere))) 16))) | |
| 1127 | + (* (chrome-scale) | |
| 1128 | + (+ 24 (* 20 (min s/overview-lines (count (s/recent-everywhere)))) 16))) | |
| 1111 | 1129 | |
| 1112 | 1130 | (defn- below-messages [] |
| 1113 | - ;; 140 is that, counted: the gap under the row of columns, the jump button's | |
| 1114 | - ;; 34pt row, the separator and the two empty wrappers with a gap apiece, then | |
| 1115 | - ;; the compose row and the air under it — its own and the window's. Short by | |
| 1116 | - ;; any of it and the column runs past the bottom edge, which does not show as | |
| 1117 | - ;; a list that is too long: it shows as a compose bar sitting flat on the | |
| 1118 | - ;; bottom of the window with its margin cut off. | |
| 1131 | + ;; 115 is that, counted: the gap under the row of columns, the jump button's | |
| 1132 | + ;; 34pt row, the banners in their one wrapper with one gap, then the compose | |
| 1133 | + ;; row and the air under it — its own and the window's. Short by any of it | |
| 1134 | + ;; and the column runs past the bottom edge, which does not show as a list | |
| 1135 | + ;; that is too long: it shows as a compose bar sitting flat on the bottom of | |
| 1136 | + ;; the window with its margin cut off. | |
| 1137 | + ;; | |
| 1138 | + ;; It was 140 when a rule was drawn above the compose bar and the three | |
| 1139 | + ;; banners each cost the column a gap of their own. The rule went, and they | |
| 1140 | + ;; went into one wrapper: twenty-five points of nothing, given back. | |
| 1119 | 1141 | (+ (* (chrome-scale) |
| 1120 | - (+ 140 | |
| 1142 | + (+ 115 | |
| 1121 | 1143 | (if @s/replying-to 34 0) |
| 1122 | 1144 | (if @s/attachment 76 0))) |
| 1123 | 1145 | ;; The two extra rows the terminal's compose field wraps into, in points: |
| @@ -1136,7 +1158,7 @@ | ||
| 1136 | 1158 | ;; anywhere else because this is the number the backlog is laid out |
| 1137 | 1159 | ;; against: without it the strip is drawn past the bottom of the window |
| 1138 | 1160 | ;; and takes the compose bar with it. |
| 1139 | - (if @s/overview? (overview-height) 0))) | |
| 1161 | + (if (and @terminal? @s/overview?) (overview-height) 0))) | |
| 1140 | 1162 | |
| 1141 | 1163 | (defn- messages-width |
| 1142 | 1164 | "How wide the message list may be with the people panel beside it. |
| @@ -1234,34 +1256,113 @@ | ||
| 1234 | 1256 | conversation behind it." |
| 1235 | 1257 | [i m] |
| 1236 | 1258 | [:hbox {:key i :spacing 8 :wrap false} |
| 1259 | + ;; The room is the button, and pressing it goes to this line rather than to | |
| 1260 | + ;; the room's end: the line is what was read here and what the press was | |
| 1261 | + ;; about, and a jump that landed on the newest message instead would answer | |
| 1262 | + ;; a question nobody asked from a strip that was showing the answer. | |
| 1263 | + ;; | |
| 1264 | + ;; Unless the line has no id to aim at — one this client has sent and the | |
| 1265 | + ;; server has not echoed back yet — and then the room is all there is to | |
| 1266 | + ;; offer. | |
| 1267 | + ;; | |
| 1268 | + ;; 600ms to land and five seconds marked: the room is a room away, so the | |
| 1269 | + ;; screen it scrolls in has to be built before there is anything to scroll, | |
| 1270 | + ;; and what the reader arrives at is a conversation they were not in a | |
| 1271 | + ;; moment ago. The mark is the whole answer to "which of these was the line | |
| 1272 | + ;; I pressed", and it has to still be there when they have finished | |
| 1273 | + ;; recognising where they are. | |
| 1237 | 1274 | [:button {:label (:channel m) |
| 1238 | - :on-click #(s/open-channel! (:channel m))}] | |
| 1275 | + :on-click (if (:id m) | |
| 1276 | + #(do (s/leaving-for-overview!) | |
| 1277 | + (goto-message! (:channel m) (:id m) 600 5000)) | |
| 1278 | + #(do (s/leaving-for-overview!) | |
| 1279 | + (s/open-channel! (:channel m))))}] | |
| 1280 | + ;; And the line itself, as something to read rather than to press. It was a | |
| 1281 | + ;; link for a moment, which made the strip two things at once: a line the | |
| 1282 | + ;; server has echoed back has an id to aim at and a line this client has | |
| 1283 | + ;; just sent does not, so half the rows came out accent-coloured and half | |
| 1284 | + ;; plain, down the same list. The chip beside it is the way to the message | |
| 1285 | + ;; and is on every row either way. | |
| 1239 | 1286 | [:vbox {:key :said} |
| 1240 | 1287 | [:dim-label {:label (str (when-let [at (:at m)] (str (clock/clock-time at) " ")) |
| 1241 | 1288 | (:from m) ": " |
| 1242 | 1289 | (preview-line (:text m)))}]]]) |
| 1243 | 1290 | |
| 1244 | 1291 | (defn- overview-pane |
| 1245 | - "Every room's last few lines in one list, oldest at the top. | |
| 1292 | + "Every other room's recent lines in one list, newest at the top. | |
| 1246 | 1293 | |
| 1247 | 1294 | A strip under the conversation rather than a screen of its own: the question |
| 1248 | 1295 | it answers — is anything happening anywhere else — is one you ask while |
| 1249 | 1296 | reading something, and an answer you have to leave the room for is one you |
| 1250 | 1297 | stop asking for. |
| 1251 | 1298 | |
| 1252 | - It does not scroll. A fixed handful of lines is what makes the room it takes | |
| 1253 | - from the backlog a number this file can reserve; a scroll here would be a | |
| 1254 | - second thing on the screen sizing itself against the window, fighting the | |
| 1255 | - backlog above it for the same points." | |
| 1299 | + In a window it scrolls, and the column it is in fills the height: two | |
| 1300 | + children of a column that both fill it are two halves of it, which is the | |
| 1301 | + horizontal split this is. The backlog above keeps its own scroll and its own | |
| 1302 | + place in it, so reading down here does not move the conversation. | |
| 1303 | + | |
| 1304 | + A terminal gets a handful of lines and no scroll. Its rows are cells rather | |
| 1305 | + than points, and a second scrolling pane in a screen that is already a | |
| 1306 | + conversation, a compose bar and a tab bar leaves neither half enough rows to | |
| 1307 | + be worth reading — so there it stays the strip it was, and `below-messages` | |
| 1308 | + reserves it by the row." | |
| 1256 | 1309 | [] |
| 1257 | - (let [lines (s/recent-everywhere)] | |
| 1258 | - [:vbox {:key :overview :spacing 4 :margin-top 4} | |
| 1259 | - [:separator {}] | |
| 1260 | - [:dim-label {:label "Everywhere else"}] | |
| 1261 | - (if (seq lines) | |
| 1262 | - (for [[i m] (map-indexed vector lines)] | |
| 1263 | - ^{:key (str (:channel m) "-" (or (:id m) i))} [overview-row i m]) | |
| 1264 | - [:dim-label {:label "Nothing has been said in any room yet."}])])) | |
| 1310 | + (let [lines (s/recent-everywhere) | |
| 1311 | + ;; The way back, and only while there is somewhere to go: the strip | |
| 1312 | + ;; is the one thing here that moves you without your having chosen a | |
| 1313 | + ;; room, so it is the one thing that owes you an undo. Gone once you | |
| 1314 | + ;; are back in that room, where it would offer to take you where you | |
| 1315 | + ;; already are. | |
| 1316 | + back (let [room @s/overview-return] | |
| 1317 | + (when (and room (not= room @s/current)) | |
| 1318 | + [:button {:label (str "← Back to " room) | |
| 1319 | + :on-click s/overview-back!}])) | |
| 1320 | + ;; The row keeps its height whether or not the way back is in it. A | |
| 1321 | + ;; button is 34 points and a caption is not, so a heading that grew | |
| 1322 | + ;; one when you jumped and lost it when you came back moved every | |
| 1323 | + ;; line under it — the strip rendering two different ways depending | |
| 1324 | + ;; on where you had been. | |
| 1325 | + heading (fn [] | |
| 1326 | + [:hbox {:key :heading :spacing 8 :wrap false :align :center} | |
| 1327 | + [:dim-label {:label "Everywhere else"}] | |
| 1328 | + [:vbox {:key :back} | |
| 1329 | + (or back [:spacer {:size @chrome-row}])]]) | |
| 1330 | + ;; The same heading on the same rule the conversation puts between two | |
| 1331 | + ;; days, and needed more here than there: these lines come from rooms | |
| 1332 | + ;; that were last spoken in at their own times, so a strip of them can | |
| 1333 | + ;; cross a day twice in ten rows where a single room's backlog crosses | |
| 1334 | + ;; one once a day. A clock alone would then be the only thing saying | |
| 1335 | + ;; which, and `9:40` says nothing about how long ago it was. | |
| 1336 | + rows (fn [ms] | |
| 1337 | + (if (seq ms) | |
| 1338 | + (mapcat | |
| 1339 | + (fn [i m] | |
| 1340 | + (let [prev (when (pos? i) (nth ms (dec i))) | |
| 1341 | + day (some-> (:at m) clock/day) | |
| 1342 | + new-day? (and day (not= day (some-> (:at prev) clock/day)))] | |
| 1343 | + (cond-> [] | |
| 1344 | + new-day? | |
| 1345 | + (conj (day-separator (str "overview-day-" i) | |
| 1346 | + (clock/day-label (:at m)))) | |
| 1347 | + true | |
| 1348 | + (conj ^{:key (str (:channel m) "-" (or (:id m) i))} | |
| 1349 | + [overview-row i m])))) | |
| 1350 | + (range (count ms)) | |
| 1351 | + ms) | |
| 1352 | + [[:dim-label {:label "Nothing has been said in any other room yet."}]]))] | |
| 1353 | + (if @terminal? | |
| 1354 | + (into [:vbox {:key :overview :spacing 4 :margin-top 4} | |
| 1355 | + [:separator {}] | |
| 1356 | + [heading]] | |
| 1357 | + (rows (take s/overview-lines lines))) | |
| 1358 | + [:vbox {:key :overview :spacing 4 :margin-top 4 :fill-height true} | |
| 1359 | + [:separator {}] | |
| 1360 | + [heading] | |
| 1361 | + ;; Named, so coming back to a conversation does not throw away where | |
| 1362 | + ;; the reader had got to in here. | |
| 1363 | + (into [:scroll {:scroll-key "overview-list" :orientation :vertical | |
| 1364 | + :spacing 4}] | |
| 1365 | + (rows lines))]))) | |
| 1265 | 1366 | |
| 1266 | 1367 | (defn chat-screen [] |
| 1267 | 1368 | (let [name @s/current |
| @@ -1366,18 +1467,19 @@ | ||
| 1366 | 1467 | ;; compose bar sits in whether or not the panel was showing. |
| 1367 | 1468 | [:vbox {:key :people-pane} |
| 1368 | 1469 | (when show-users? [users-panel name])]] |
| 1369 | - ;; Under the conversation and above the jump button: it is a second list | |
| 1370 | - ;; of messages, so it belongs with the first rather than down among the | |
| 1371 | - ;; compose bar's rows. In a wrapper of its own, since it comes and goes. | |
| 1372 | - [:vbox {:key :overview-pane} | |
| 1373 | - (when @s/overview? [overview-pane])] | |
| 1374 | - ;; Only while it is needed, and directly above the compose bar: the way | |
| 1375 | - ;; back to the present belongs next to the thing that puts you there. | |
| 1376 | - ;; The row keeps its height whether or not the button is in it, so the | |
| 1377 | - ;; compose bar below stays where the reader last saw it. | |
| 1470 | + ;; Only while it is needed, and directly under the backlog: the way back | |
| 1471 | + ;; to the present belongs next to the thing that puts you there, which is | |
| 1472 | + ;; the conversation and not the overview. | |
| 1473 | + ;; | |
| 1474 | + ;; The row used to be held open by a spacer when the button was not in | |
| 1475 | + ;; it, so that the compose bar did not move when it came and went. It sat | |
| 1476 | + ;; under the backlog then, where the empty row read as the end of the | |
| 1477 | + ;; conversation. Above the strip it reads as a hole between two lists, | |
| 1478 | + ;; and there is no arguing a reader out of that — so the row is only | |
| 1479 | + ;; there when the button is, and what moves is a strip below it moving by | |
| 1480 | + ;; one row on the rare frame the reader has scrolled away from the end. | |
| 1378 | 1481 | [:vbox {:key :jump} |
| 1379 | - (if @s/at-present? | |
| 1380 | - [:spacer {:size 34}] | |
| 1482 | + (when-not @s/at-present? | |
| 1381 | 1483 | ;; With the key beside it where there is a key: a terminal is where a |
| 1382 | 1484 | ;; reader is least likely to reach for the mouse, and most likely to |
| 1383 | 1485 | ;; have paged up here with the keyboard in the first place. |
| @@ -1385,26 +1487,42 @@ | ||
| 1385 | 1487 | "↓ Jump to present (Ctrl-End)" |
| 1386 | 1488 | "↓ Jump to present") |
| 1387 | 1489 | :on-click s/jump-to-present!}])] |
| 1388 | - [:separator {}] | |
| 1389 | - ;; What the draft is answering, directly above where it is being typed. | |
| 1390 | - [:vbox {:key :replying} | |
| 1490 | + ;; The strip, under the conversation and the way back to it: it is a | |
| 1491 | + ;; second list of messages, so it belongs with the first rather than down | |
| 1492 | + ;; among the compose bar's rows. In a wrapper of its own, since it comes | |
| 1493 | + ;; and goes. | |
| 1494 | + [:vbox {:key :overview-pane :fill-height (boolean (and @s/overview? | |
| 1495 | + (not @terminal?)))} | |
| 1496 | + (when @s/overview? [overview-pane])] | |
| 1497 | + ;; No rule above the compose bar. The bar is already told apart from the | |
| 1498 | + ;; conversation by what it is — a framed box and a button on a strip of | |
| 1499 | + ;; air — and a line drawn over it as well was the app underlining the one | |
| 1500 | + ;; edge nobody was going to miss. | |
| 1501 | + ;; The three banners in one wrapper, at no spacing of its own. Each is a | |
| 1502 | + ;; row that is usually empty, and an empty child still costs the column | |
| 1503 | + ;; its gap — three of them stacked under the overview was two dozen | |
| 1504 | + ;; points of nothing between the strip and the compose bar. Inside, they | |
| 1505 | + ;; carry their own air only when they have something in them. | |
| 1506 | + [:vbox {:key :banners :spacing 0} | |
| 1507 | + ;; What the draft is answering, directly above where it is being typed. | |
| 1508 | + [:vbox {:key :replying} | |
| 1391 | 1509 | (when-let [target @s/replying-to] |
| 1392 | 1510 | [:hbox {:spacing 8} |
| 1393 | 1511 | [:dim-label {:label (str "↩ " (:from target) ": " (summarise target 36))}] |
| 1394 | 1512 | [:button {:label "✕" :on-click s/cancel-reply!}]])] |
| 1395 | - ;; And, in the same place, that the box holds a rewrite rather than | |
| 1396 | - ;; something new: the text in it is a copy of a line already on screen, | |
| 1397 | - ;; and without this Send would look like it was about to say it twice. | |
| 1398 | - [:vbox {:key :editing} | |
| 1513 | + ;; And, in the same place, that the box holds a rewrite rather than | |
| 1514 | + ;; something new: the text in it is a copy of a line already on screen, | |
| 1515 | + ;; and without this Send would look like it was about to say it twice. | |
| 1516 | + [:vbox {:key :editing} | |
| 1399 | 1517 | (when @s/editing |
| 1400 | 1518 | [:hbox {:spacing 8} |
| 1401 | 1519 | [:dim-label {:label "✏️ Editing your message"}] |
| 1402 | 1520 | [:button {:label "✕" :on-click s/cancel-edit!}]])] |
| 1403 | - ;; The pasted picture, above the line it will go out with. Shown rather | |
| 1404 | - ;; than written into the draft: what is being sent is a picture, and a URL | |
| 1405 | - ;; dropped into the entry would be an unreadable line of text sitting in | |
| 1406 | - ;; the middle of whatever the reader was in the middle of typing. | |
| 1407 | - [:vbox {:key :attachment} | |
| 1521 | + ;; The pasted picture, above the line it will go out with. Shown rather | |
| 1522 | + ;; than written into the draft: what is being sent is a picture, and a URL | |
| 1523 | + ;; dropped into the entry would be an unreadable line of text sitting in | |
| 1524 | + ;; the middle of whatever the reader was in the middle of typing. | |
| 1525 | + [:vbox {:key :attachment} | |
| 1408 | 1526 | (when-let [att @s/attachment] |
| 1409 | 1527 | [:hbox {:spacing 8} |
| 1410 | 1528 | ;; Small: it is a reminder of what is attached, not the picture |
| @@ -1413,7 +1531,7 @@ | ||
| 1413 | 1531 | [:dim-label {:label (if (= :uploading (:status att)) |
| 1414 | 1532 | "Uploading…" |
| 1415 | 1533 | "Picture attached")}] |
| 1416 | - [:button {:label "✕" :on-click s/clear-attachment!}]])] | |
| 1534 | + [:button {:label "✕" :on-click s/clear-attachment!}]])]] | |
| 1417 | 1535 | ;; The line and its buttons on the middle of the width rather than |
| 1418 | 1536 | ;; against its left edge: the entry asks for a fixed width, and on a |
| 1419 | 1537 | ;; window wider than that a left-aligned row leaves it stranded in the |
| @@ -1421,11 +1539,10 @@ | ||
| 1421 | 1539 | ;; window and centring costs nothing. |
| 1422 | 1540 | ;; Equal air above and below, so the row sits on the middle of the strip |
| 1423 | 1541 | ;; between the separator and the bottom edge rather than flat against it. |
| 1424 | - ;; Above it is four of the column's gaps: one after the separator and one | |
| 1425 | - ;; for each of the empty wrappers — the reply banner, the edit banner and | |
| 1426 | - ;; the attachment, which cost a gap apiece whether or not they have | |
| 1427 | - ;; anything in them. This margin plus the window's own is what answers | |
| 1428 | - ;; them underneath. | |
| 1542 | + ;; Above it is one of the column's gaps, where it used to be four: the | |
| 1543 | + ;; reply banner, the edit banner and the attachment share a wrapper now | |
| 1544 | + ;; and cost one between them whether or not they have anything in them. | |
| 1545 | + ;; This margin plus the window's own is what answers it underneath. | |
| 1429 | 1546 | [:hbox {:spacing 8 :align :center :margin-bottom 12} |
| 1430 | 1547 | ;; narrow enough that Send keeps its place on a phone-width row |
| 1431 | 1548 | ;; A picture is pasted where everything else is typed: Ctrl+V. The field |
| @@ -260,6 +260,33 @@ | |||
| 260 | (str (subs text 0 (dec limit)) "…") | 260 | (str (subs text 0 (dec limit)) "…") |
| 261 | text))) | 261 | text))) |
| 262 | 262 | ||
| 263 | +(defn- goto-message! | ||
| 264 | + "Show message `id`, in `channel`, and say which one it was. | ||
| 265 | + | ||
| 266 | + Three things in a fixed order: be in the room, aim the scroll at the line, | ||
| 267 | + and mark it once it is there. `jump-to` comes off again as soon as the frame | ||
| 268 | + that scrolled has been painted — a scroll target that stays set pins the | ||
| 269 | + view and takes scrolling away from the reader — while the highlight outlives | ||
| 270 | + it, because arriving at a screenful of messages says nothing about which one | ||
| 271 | + was asked for. | ||
| 272 | + | ||
| 273 | + `settle` is how long that first frame is given, and `linger` how long the | ||
| 274 | + mark stays. Within a room the scrolling frame is the next one and the line | ||
| 275 | + is somewhere the reader was already looking. Crossing a room is a slower | ||
| 276 | + and a stranger arrival — the screen has to be built before there is | ||
| 277 | + anything to scroll, and what it opens on is a conversation the reader was | ||
| 278 | + not in a moment ago — so a caller that crosses one asks for more of both." | ||
| 279 | + [channel id settle linger] | ||
| 280 | + (when (and channel (not= channel @s/current)) | ||
| 281 | + (s/open-channel! channel)) | ||
| 282 | + (reset! s/jump-to id) | ||
| 283 | + (reset! s/highlight id) | ||
| 284 | + (platform/after! settle (fn [] (reset! s/jump-to nil))) | ||
| 285 | + ;; The highlight only clears itself: a later jump elsewhere owns the | ||
| 286 | + ;; highlight from then on. | ||
| 287 | + (platform/after! linger (fn [] (when (= id @s/highlight) | ||
| 288 | + (reset! s/highlight nil))))) | ||
| 289 | + | ||
| 263 | (defn- reply-chip | 290 | (defn- reply-chip |
| 264 | "What a message is replying to, above it, and a way back to it. | 291 | "What a message is replying to, above it, and a way back to it. |
| 265 | 292 | ||
| @@ -273,18 +300,9 @@ | |||
| 273 | ;; action, and a filled pill above every answer was the loudest thing in | 300 | ;; action, and a filled pill above every answer was the loudest thing in |
| 274 | ;; the column. | 301 | ;; the column. |
| 275 | [:link {:label (str "↩ " (:from target) ": " (summarise target 48)) | 302 | [:link {:label (str "↩ " (:from target) ": " (summarise target 48)) |
| 276 | - :on-click #(do (reset! s/jump-to id) | 303 | + ;; The line being answered is in the room already open, so the |
| 277 | - (reset! s/highlight id) | 304 | + ;; next frame is the one that scrolls. |
| 278 | - ;; Off again once the frame that scrolled has | 305 | + :on-click #(goto-message! channel id 120 2000)}] |
| 279 | - ;; been painted, so the reader keeps the view. | ||
| 280 | - (platform/after! 120 (fn [] (reset! s/jump-to nil))) | ||
| 281 | - ;; The highlight stays long enough to be read, | ||
| 282 | - ;; and only clears itself: a later jump elsewhere | ||
| 283 | - ;; owns the highlight from then on. | ||
| 284 | - (platform/after! 2000 | ||
| 285 | - (fn [] | ||
| 286 | - (when (= id @s/highlight) | ||
| 287 | - (reset! s/highlight nil)))))}] | ||
| 288 | ;; The message it answers is older than this buffer goes. | 306 | ;; The message it answers is older than this buffer goes. |
| 289 | [:dim-label {:label "↩ replying to an earlier message"}])]) | 307 | [:dim-label {:label "↩ replying to an earlier message"}])]) |
| 290 | 308 | ||
| @@ -780,11 +798,7 @@ | |||
| 780 | 798 | ||
| 781 | (= :failed (:status pr)) | 799 | (= :failed (:status pr)) |
| 782 | [:dim-label {:label "No Bluesky profile found"}])] | 800 | [:dim-label {:label "No Bluesky profile found"}])] |
| 783 | - ;; What the pointer is holding open says how to stop having to hold it. | 801 | +] |
| 784 | - ;; Pinned, that line has nothing left to tell anyone. | ||
| 785 | - [:vbox {:key :hint} | ||
| 786 | - (when-not pinned? | ||
| 787 | - [:dim-label {:label "Click the face to keep this open"}])]] | ||
| 788 | ;; `slot` is where libcosmic puts a button: the two actions go to the | 802 | ;; `slot` is where libcosmic puts a button: the two actions go to the |
| 789 | ;; foot of the dialog, and anything else here would be another control | 803 | ;; foot of the dialog, and anything else here would be another control |
| 790 | ;; stacked in the body. | 804 | ;; stacked in the body. |
| @@ -1103,21 +1117,29 @@ | |||
| 1103 | ;; what every other row under the message list has had to say for itself. | 1117 | ;; what every other row under the message list has had to say for itself. |
| 1104 | ;; | 1118 | ;; |
| 1105 | ;; Counted from the lines there actually are, not from the ceiling: the strip | 1119 | ;; Counted from the lines there actually are, not from the ceiling: the strip |
| 1106 | -;; holds a few per room, so its height is the client's rooms, and reserving | 1120 | +;; holds at most `overview-lines` of them there, and a room with two lines in |
| 1107 | -;; for a full one in a client with two channels in it would take a third of | 1121 | +;; it should not have a screenful reserved against it. |
| 1108 | -;; the window off the conversation to leave it empty. | 1122 | +;; |
| 1123 | +;; The window reserves nothing for the overview. Its pane scrolls and fills, | ||
| 1124 | +;; so it takes its half of the column by being in it — there is no number to | ||
| 1125 | +;; subtract, and subtracting one would take the room twice. | ||
| 1109 | (defn- overview-height [] | 1126 | (defn- overview-height [] |
| 1110 | - (* (chrome-scale) (+ 24 (* 20 (count (s/recent-everywhere))) 16))) | 1127 | + (* (chrome-scale) |
| 1128 | + (+ 24 (* 20 (min s/overview-lines (count (s/recent-everywhere)))) 16))) | ||
| 1111 | 1129 | ||
| 1112 | (defn- below-messages [] | 1130 | (defn- below-messages [] |
| 1113 | - ;; 140 is that, counted: the gap under the row of columns, the jump button's | 1131 | + ;; 115 is that, counted: the gap under the row of columns, the jump button's |
| 1114 | - ;; 34pt row, the separator and the two empty wrappers with a gap apiece, then | 1132 | + ;; 34pt row, the banners in their one wrapper with one gap, then the compose |
| 1115 | - ;; the compose row and the air under it — its own and the window's. Short by | 1133 | + ;; row and the air under it — its own and the window's. Short by any of it |
| 1116 | - ;; any of it and the column runs past the bottom edge, which does not show as | 1134 | + ;; and the column runs past the bottom edge, which does not show as a list |
| 1117 | - ;; a list that is too long: it shows as a compose bar sitting flat on the | 1135 | + ;; that is too long: it shows as a compose bar sitting flat on the bottom of |
| 1118 | - ;; bottom of the window with its margin cut off. | 1136 | + ;; the window with its margin cut off. |
| 1137 | + ;; | ||
| 1138 | + ;; It was 140 when a rule was drawn above the compose bar and the three | ||
| 1139 | + ;; banners each cost the column a gap of their own. The rule went, and they | ||
| 1140 | + ;; went into one wrapper: twenty-five points of nothing, given back. | ||
| 1119 | (+ (* (chrome-scale) | 1141 | (+ (* (chrome-scale) |
| 1120 | - (+ 140 | 1142 | + (+ 115 |
| 1121 | (if @s/replying-to 34 0) | 1143 | (if @s/replying-to 34 0) |
| 1122 | (if @s/attachment 76 0))) | 1144 | (if @s/attachment 76 0))) |
| 1123 | ;; The two extra rows the terminal's compose field wraps into, in points: | 1145 | ;; The two extra rows the terminal's compose field wraps into, in points: |
| @@ -1136,7 +1158,7 @@ | |||
| 1136 | ;; anywhere else because this is the number the backlog is laid out | 1158 | ;; anywhere else because this is the number the backlog is laid out |
| 1137 | ;; against: without it the strip is drawn past the bottom of the window | 1159 | ;; against: without it the strip is drawn past the bottom of the window |
| 1138 | ;; and takes the compose bar with it. | 1160 | ;; and takes the compose bar with it. |
| 1139 | - (if @s/overview? (overview-height) 0))) | 1161 | + (if (and @terminal? @s/overview?) (overview-height) 0))) |
| 1140 | 1162 | ||
| 1141 | (defn- messages-width | 1163 | (defn- messages-width |
| 1142 | "How wide the message list may be with the people panel beside it. | 1164 | "How wide the message list may be with the people panel beside it. |
| @@ -1234,34 +1256,113 @@ | |||
| 1234 | conversation behind it." | 1256 | conversation behind it." |
| 1235 | [i m] | 1257 | [i m] |
| 1236 | [:hbox {:key i :spacing 8 :wrap false} | 1258 | [:hbox {:key i :spacing 8 :wrap false} |
| 1259 | + ;; The room is the button, and pressing it goes to this line rather than to | ||
| 1260 | + ;; the room's end: the line is what was read here and what the press was | ||
| 1261 | + ;; about, and a jump that landed on the newest message instead would answer | ||
| 1262 | + ;; a question nobody asked from a strip that was showing the answer. | ||
| 1263 | + ;; | ||
| 1264 | + ;; Unless the line has no id to aim at — one this client has sent and the | ||
| 1265 | + ;; server has not echoed back yet — and then the room is all there is to | ||
| 1266 | + ;; offer. | ||
| 1267 | + ;; | ||
| 1268 | + ;; 600ms to land and five seconds marked: the room is a room away, so the | ||
| 1269 | + ;; screen it scrolls in has to be built before there is anything to scroll, | ||
| 1270 | + ;; and what the reader arrives at is a conversation they were not in a | ||
| 1271 | + ;; moment ago. The mark is the whole answer to "which of these was the line | ||
| 1272 | + ;; I pressed", and it has to still be there when they have finished | ||
| 1273 | + ;; recognising where they are. | ||
| 1237 | [:button {:label (:channel m) | 1274 | [:button {:label (:channel m) |
| 1238 | - :on-click #(s/open-channel! (:channel m))}] | 1275 | + :on-click (if (:id m) |
| 1276 | + #(do (s/leaving-for-overview!) | ||
| 1277 | + (goto-message! (:channel m) (:id m) 600 5000)) | ||
| 1278 | + #(do (s/leaving-for-overview!) | ||
| 1279 | + (s/open-channel! (:channel m))))}] | ||
| 1280 | + ;; And the line itself, as something to read rather than to press. It was a | ||
| 1281 | + ;; link for a moment, which made the strip two things at once: a line the | ||
| 1282 | + ;; server has echoed back has an id to aim at and a line this client has | ||
| 1283 | + ;; just sent does not, so half the rows came out accent-coloured and half | ||
| 1284 | + ;; plain, down the same list. The chip beside it is the way to the message | ||
| 1285 | + ;; and is on every row either way. | ||
| 1239 | [:vbox {:key :said} | 1286 | [:vbox {:key :said} |
| 1240 | [:dim-label {:label (str (when-let [at (:at m)] (str (clock/clock-time at) " ")) | 1287 | [:dim-label {:label (str (when-let [at (:at m)] (str (clock/clock-time at) " ")) |
| 1241 | (:from m) ": " | 1288 | (:from m) ": " |
| 1242 | (preview-line (:text m)))}]]]) | 1289 | (preview-line (:text m)))}]]]) |
| 1243 | 1290 | ||
| 1244 | (defn- overview-pane | 1291 | (defn- overview-pane |
| 1245 | - "Every room's last few lines in one list, oldest at the top. | 1292 | + "Every other room's recent lines in one list, newest at the top. |
| 1246 | 1293 | ||
| 1247 | A strip under the conversation rather than a screen of its own: the question | 1294 | A strip under the conversation rather than a screen of its own: the question |
| 1248 | it answers — is anything happening anywhere else — is one you ask while | 1295 | it answers — is anything happening anywhere else — is one you ask while |
| 1249 | reading something, and an answer you have to leave the room for is one you | 1296 | reading something, and an answer you have to leave the room for is one you |
| 1250 | stop asking for. | 1297 | stop asking for. |
| 1251 | 1298 | ||
| 1252 | - It does not scroll. A fixed handful of lines is what makes the room it takes | 1299 | + In a window it scrolls, and the column it is in fills the height: two |
| 1253 | - from the backlog a number this file can reserve; a scroll here would be a | 1300 | + children of a column that both fill it are two halves of it, which is the |
| 1254 | - second thing on the screen sizing itself against the window, fighting the | 1301 | + horizontal split this is. The backlog above keeps its own scroll and its own |
| 1255 | - backlog above it for the same points." | 1302 | + place in it, so reading down here does not move the conversation. |
| 1303 | + | ||
| 1304 | + A terminal gets a handful of lines and no scroll. Its rows are cells rather | ||
| 1305 | + than points, and a second scrolling pane in a screen that is already a | ||
| 1306 | + conversation, a compose bar and a tab bar leaves neither half enough rows to | ||
| 1307 | + be worth reading — so there it stays the strip it was, and `below-messages` | ||
| 1308 | + reserves it by the row." | ||
| 1256 | [] | 1309 | [] |
| 1257 | - (let [lines (s/recent-everywhere)] | 1310 | + (let [lines (s/recent-everywhere) |
| 1258 | - [:vbox {:key :overview :spacing 4 :margin-top 4} | 1311 | + ;; The way back, and only while there is somewhere to go: the strip |
| 1259 | - [:separator {}] | 1312 | + ;; is the one thing here that moves you without your having chosen a |
| 1260 | - [:dim-label {:label "Everywhere else"}] | 1313 | + ;; room, so it is the one thing that owes you an undo. Gone once you |
| 1261 | - (if (seq lines) | 1314 | + ;; are back in that room, where it would offer to take you where you |
| 1262 | - (for [[i m] (map-indexed vector lines)] | 1315 | + ;; already are. |
| 1263 | - ^{:key (str (:channel m) "-" (or (:id m) i))} [overview-row i m]) | 1316 | + back (let [room @s/overview-return] |
| 1264 | - [:dim-label {:label "Nothing has been said in any room yet."}])])) | 1317 | + (when (and room (not= room @s/current)) |
| 1318 | + [:button {:label (str "← Back to " room) | ||
| 1319 | + :on-click s/overview-back!}])) | ||
| 1320 | + ;; The row keeps its height whether or not the way back is in it. A | ||
| 1321 | + ;; button is 34 points and a caption is not, so a heading that grew | ||
| 1322 | + ;; one when you jumped and lost it when you came back moved every | ||
| 1323 | + ;; line under it — the strip rendering two different ways depending | ||
| 1324 | + ;; on where you had been. | ||
| 1325 | + heading (fn [] | ||
| 1326 | + [:hbox {:key :heading :spacing 8 :wrap false :align :center} | ||
| 1327 | + [:dim-label {:label "Everywhere else"}] | ||
| 1328 | + [:vbox {:key :back} | ||
| 1329 | + (or back [:spacer {:size @chrome-row}])]]) | ||
| 1330 | + ;; The same heading on the same rule the conversation puts between two | ||
| 1331 | + ;; days, and needed more here than there: these lines come from rooms | ||
| 1332 | + ;; that were last spoken in at their own times, so a strip of them can | ||
| 1333 | + ;; cross a day twice in ten rows where a single room's backlog crosses | ||
| 1334 | + ;; one once a day. A clock alone would then be the only thing saying | ||
| 1335 | + ;; which, and `9:40` says nothing about how long ago it was. | ||
| 1336 | + rows (fn [ms] | ||
| 1337 | + (if (seq ms) | ||
| 1338 | + (mapcat | ||
| 1339 | + (fn [i m] | ||
| 1340 | + (let [prev (when (pos? i) (nth ms (dec i))) | ||
| 1341 | + day (some-> (:at m) clock/day) | ||
| 1342 | + new-day? (and day (not= day (some-> (:at prev) clock/day)))] | ||
| 1343 | + (cond-> [] | ||
| 1344 | + new-day? | ||
| 1345 | + (conj (day-separator (str "overview-day-" i) | ||
| 1346 | + (clock/day-label (:at m)))) | ||
| 1347 | + true | ||
| 1348 | + (conj ^{:key (str (:channel m) "-" (or (:id m) i))} | ||
| 1349 | + [overview-row i m])))) | ||
| 1350 | + (range (count ms)) | ||
| 1351 | + ms) | ||
| 1352 | + [[:dim-label {:label "Nothing has been said in any other room yet."}]]))] | ||
| 1353 | + (if @terminal? | ||
| 1354 | + (into [:vbox {:key :overview :spacing 4 :margin-top 4} | ||
| 1355 | + [:separator {}] | ||
| 1356 | + [heading]] | ||
| 1357 | + (rows (take s/overview-lines lines))) | ||
| 1358 | + [:vbox {:key :overview :spacing 4 :margin-top 4 :fill-height true} | ||
| 1359 | + [:separator {}] | ||
| 1360 | + [heading] | ||
| 1361 | + ;; Named, so coming back to a conversation does not throw away where | ||
| 1362 | + ;; the reader had got to in here. | ||
| 1363 | + (into [:scroll {:scroll-key "overview-list" :orientation :vertical | ||
| 1364 | + :spacing 4}] | ||
| 1365 | + (rows lines))]))) | ||
| 1265 | 1366 | ||
| 1266 | (defn chat-screen [] | 1367 | (defn chat-screen [] |
| 1267 | (let [name @s/current | 1368 | (let [name @s/current |
| @@ -1366,18 +1467,19 @@ | |||
| 1366 | ;; compose bar sits in whether or not the panel was showing. | 1467 | ;; compose bar sits in whether or not the panel was showing. |
| 1367 | [:vbox {:key :people-pane} | 1468 | [:vbox {:key :people-pane} |
| 1368 | (when show-users? [users-panel name])]] | 1469 | (when show-users? [users-panel name])]] |
| 1369 | - ;; Under the conversation and above the jump button: it is a second list | 1470 | + ;; Only while it is needed, and directly under the backlog: the way back |
| 1370 | - ;; of messages, so it belongs with the first rather than down among the | 1471 | + ;; to the present belongs next to the thing that puts you there, which is |
| 1371 | - ;; compose bar's rows. In a wrapper of its own, since it comes and goes. | 1472 | + ;; the conversation and not the overview. |
| 1372 | - [:vbox {:key :overview-pane} | 1473 | + ;; |
| 1373 | - (when @s/overview? [overview-pane])] | 1474 | + ;; The row used to be held open by a spacer when the button was not in |
| 1374 | - ;; Only while it is needed, and directly above the compose bar: the way | 1475 | + ;; it, so that the compose bar did not move when it came and went. It sat |
| 1375 | - ;; back to the present belongs next to the thing that puts you there. | 1476 | + ;; under the backlog then, where the empty row read as the end of the |
| 1376 | - ;; The row keeps its height whether or not the button is in it, so the | 1477 | + ;; conversation. Above the strip it reads as a hole between two lists, |
| 1377 | - ;; compose bar below stays where the reader last saw it. | 1478 | + ;; and there is no arguing a reader out of that — so the row is only |
| 1479 | + ;; there when the button is, and what moves is a strip below it moving by | ||
| 1480 | + ;; one row on the rare frame the reader has scrolled away from the end. | ||
| 1378 | [:vbox {:key :jump} | 1481 | [:vbox {:key :jump} |
| 1379 | - (if @s/at-present? | 1482 | + (when-not @s/at-present? |
| 1380 | - [:spacer {:size 34}] | ||
| 1381 | ;; With the key beside it where there is a key: a terminal is where a | 1483 | ;; With the key beside it where there is a key: a terminal is where a |
| 1382 | ;; reader is least likely to reach for the mouse, and most likely to | 1484 | ;; reader is least likely to reach for the mouse, and most likely to |
| 1383 | ;; have paged up here with the keyboard in the first place. | 1485 | ;; have paged up here with the keyboard in the first place. |
| @@ -1385,26 +1487,42 @@ | |||
| 1385 | "↓ Jump to present (Ctrl-End)" | 1487 | "↓ Jump to present (Ctrl-End)" |
| 1386 | "↓ Jump to present") | 1488 | "↓ Jump to present") |
| 1387 | :on-click s/jump-to-present!}])] | 1489 | :on-click s/jump-to-present!}])] |
| 1388 | - [:separator {}] | 1490 | + ;; The strip, under the conversation and the way back to it: it is a |
| 1389 | - ;; What the draft is answering, directly above where it is being typed. | 1491 | + ;; second list of messages, so it belongs with the first rather than down |
| 1390 | - [:vbox {:key :replying} | 1492 | + ;; among the compose bar's rows. In a wrapper of its own, since it comes |
| 1493 | + ;; and goes. | ||
| 1494 | + [:vbox {:key :overview-pane :fill-height (boolean (and @s/overview? | ||
| 1495 | + (not @terminal?)))} | ||
| 1496 | + (when @s/overview? [overview-pane])] | ||
| 1497 | + ;; No rule above the compose bar. The bar is already told apart from the | ||
| 1498 | + ;; conversation by what it is — a framed box and a button on a strip of | ||
| 1499 | + ;; air — and a line drawn over it as well was the app underlining the one | ||
| 1500 | + ;; edge nobody was going to miss. | ||
| 1501 | + ;; The three banners in one wrapper, at no spacing of its own. Each is a | ||
| 1502 | + ;; row that is usually empty, and an empty child still costs the column | ||
| 1503 | + ;; its gap — three of them stacked under the overview was two dozen | ||
| 1504 | + ;; points of nothing between the strip and the compose bar. Inside, they | ||
| 1505 | + ;; carry their own air only when they have something in them. | ||
| 1506 | + [:vbox {:key :banners :spacing 0} | ||
| 1507 | + ;; What the draft is answering, directly above where it is being typed. | ||
| 1508 | + [:vbox {:key :replying} | ||
| 1391 | (when-let [target @s/replying-to] | 1509 | (when-let [target @s/replying-to] |
| 1392 | [:hbox {:spacing 8} | 1510 | [:hbox {:spacing 8} |
| 1393 | [:dim-label {:label (str "↩ " (:from target) ": " (summarise target 36))}] | 1511 | [:dim-label {:label (str "↩ " (:from target) ": " (summarise target 36))}] |
| 1394 | [:button {:label "✕" :on-click s/cancel-reply!}]])] | 1512 | [:button {:label "✕" :on-click s/cancel-reply!}]])] |
| 1395 | - ;; And, in the same place, that the box holds a rewrite rather than | 1513 | + ;; And, in the same place, that the box holds a rewrite rather than |
| 1396 | - ;; something new: the text in it is a copy of a line already on screen, | 1514 | + ;; something new: the text in it is a copy of a line already on screen, |
| 1397 | - ;; and without this Send would look like it was about to say it twice. | 1515 | + ;; and without this Send would look like it was about to say it twice. |
| 1398 | - [:vbox {:key :editing} | 1516 | + [:vbox {:key :editing} |
| 1399 | (when @s/editing | 1517 | (when @s/editing |
| 1400 | [:hbox {:spacing 8} | 1518 | [:hbox {:spacing 8} |
| 1401 | [:dim-label {:label "✏️ Editing your message"}] | 1519 | [:dim-label {:label "✏️ Editing your message"}] |
| 1402 | [:button {:label "✕" :on-click s/cancel-edit!}]])] | 1520 | [:button {:label "✕" :on-click s/cancel-edit!}]])] |
| 1403 | - ;; The pasted picture, above the line it will go out with. Shown rather | 1521 | + ;; The pasted picture, above the line it will go out with. Shown rather |
| 1404 | - ;; than written into the draft: what is being sent is a picture, and a URL | 1522 | + ;; than written into the draft: what is being sent is a picture, and a URL |
| 1405 | - ;; dropped into the entry would be an unreadable line of text sitting in | 1523 | + ;; dropped into the entry would be an unreadable line of text sitting in |
| 1406 | - ;; the middle of whatever the reader was in the middle of typing. | 1524 | + ;; the middle of whatever the reader was in the middle of typing. |
| 1407 | - [:vbox {:key :attachment} | 1525 | + [:vbox {:key :attachment} |
| 1408 | (when-let [att @s/attachment] | 1526 | (when-let [att @s/attachment] |
| 1409 | [:hbox {:spacing 8} | 1527 | [:hbox {:spacing 8} |
| 1410 | ;; Small: it is a reminder of what is attached, not the picture | 1528 | ;; Small: it is a reminder of what is attached, not the picture |
| @@ -1413,7 +1531,7 @@ | |||
| 1413 | [:dim-label {:label (if (= :uploading (:status att)) | 1531 | [:dim-label {:label (if (= :uploading (:status att)) |
| 1414 | "Uploading…" | 1532 | "Uploading…" |
| 1415 | "Picture attached")}] | 1533 | "Picture attached")}] |
| 1416 | - [:button {:label "✕" :on-click s/clear-attachment!}]])] | 1534 | + [:button {:label "✕" :on-click s/clear-attachment!}]])]] |
| 1417 | ;; The line and its buttons on the middle of the width rather than | 1535 | ;; The line and its buttons on the middle of the width rather than |
| 1418 | ;; against its left edge: the entry asks for a fixed width, and on a | 1536 | ;; against its left edge: the entry asks for a fixed width, and on a |
| 1419 | ;; window wider than that a left-aligned row leaves it stranded in the | 1537 | ;; window wider than that a left-aligned row leaves it stranded in the |
| @@ -1421,11 +1539,10 @@ | |||
| 1421 | ;; window and centring costs nothing. | 1539 | ;; window and centring costs nothing. |
| 1422 | ;; Equal air above and below, so the row sits on the middle of the strip | 1540 | ;; Equal air above and below, so the row sits on the middle of the strip |
| 1423 | ;; between the separator and the bottom edge rather than flat against it. | 1541 | ;; between the separator and the bottom edge rather than flat against it. |
| 1424 | - ;; Above it is four of the column's gaps: one after the separator and one | 1542 | + ;; Above it is one of the column's gaps, where it used to be four: the |
| 1425 | - ;; for each of the empty wrappers — the reply banner, the edit banner and | 1543 | + ;; reply banner, the edit banner and the attachment share a wrapper now |
| 1426 | - ;; the attachment, which cost a gap apiece whether or not they have | 1544 | + ;; and cost one between them whether or not they have anything in them. |
| 1427 | - ;; anything in them. This margin plus the window's own is what answers | 1545 | + ;; This margin plus the window's own is what answers it underneath. |
| 1428 | - ;; them underneath. | ||
| 1429 | [:hbox {:spacing 8 :align :center :margin-bottom 12} | 1546 | [:hbox {:spacing 8 :align :center :margin-bottom 12} |
| 1430 | ;; narrow enough that Send keeps its place on a phone-width row | 1547 | ;; narrow enough that Send keeps its place on a phone-width row |
| 1431 | ;; A picture is pasted where everything else is typed: Ctrl+V. The field | 1548 | ;; A picture is pasted where everything else is typed: Ctrl+V. The field |
modified
src/frq/state.clj +70 -22 | @@ -131,6 +131,32 @@ | ||
| 131 | 131 | (swap! overview? not) |
| 132 | 132 | (save-prefs!)) |
| 133 | 133 | |
| 134 | +;; The room the reader was in when a line in the overview took them somewhere | |
| 135 | +;; else, or nil. The strip is the one place in the app that moves you without | |
| 136 | +;; you having asked to leave where you were — everything else is a room you | |
| 137 | +;; chose — so it is the one place that owes you the way back. | |
| 138 | +(defonce overview-return (atom nil)) | |
| 139 | + | |
| 140 | +(declare open-channel!) | |
| 141 | + | |
| 142 | +(defn leaving-for-overview! | |
| 143 | + "Remember where we are, because a line in the strip is about to take us out | |
| 144 | + of it. Nothing to remember if there is nowhere to go back to." | |
| 145 | + [] | |
| 146 | + (reset! overview-return @current)) | |
| 147 | + | |
| 148 | +(defn overview-back! | |
| 149 | + "Back to the room the strip took you out of. | |
| 150 | + | |
| 151 | + The room, and not the place in it: the backlog's scroll is remembered under | |
| 152 | + one name for every conversation — see `messages-scroll-key` — so what comes | |
| 153 | + back is the room and whatever that one viewport is currently showing of it. | |
| 154 | + A place of your own in every room is a bigger change than this button." | |
| 155 | + [] | |
| 156 | + (when-let [room @overview-return] | |
| 157 | + (reset! overview-return nil) | |
| 158 | + (open-channel! room))) | |
| 159 | + | |
| 134 | 160 | ;; The window's content width in points, polled from the backend a few times a |
| 135 | 161 | ;; second. The app is laid out for a phone-width window, and this is what lets |
| 136 | 162 | ;; a wide one be more than a phone with margins: past `wide-width` the channel |
| @@ -1939,28 +1965,39 @@ | ||
| 1939 | 1965 | |
| 1940 | 1966 | |
| 1941 | 1967 | |
| 1942 | -;; How many lines the overview takes from each room. Per room rather than | |
| 1943 | -;; across all of them: the strip is there to say what is happening everywhere, | |
| 1944 | -;; and one busy channel taking the whole of a shared budget is the strip | |
| 1945 | -;; answering about one room — which is the room you are probably already in. | |
| 1946 | -(def overview-per-channel 3) | |
| 1968 | +;; How many lines the overview holds in all. | |
| 1969 | +(def overview-limit 100) | |
| 1970 | + | |
| 1971 | +;; How many it shows without being scrolled — the terminal's whole answer, | |
| 1972 | +;; since a strip there is a handful of rows taken off a conversation that is | |
| 1973 | +;; measured in how many of those fit. | |
| 1974 | +(def overview-lines 8) | |
| 1975 | + | |
| 1976 | +(defn- round-robin | |
| 1977 | + "The colls' firsts, then their seconds, and so on until they are spent. | |
| 1947 | 1978 | |
| 1948 | -;; And a ceiling on the lot, because the rooms are not a fixed number: at three | |
| 1949 | -;; apiece, a client in twenty channels would hand the strip sixty rows and the | |
| 1950 | -;; conversation above it nothing. Past this the newest win, which is the same | |
| 1951 | -;; bargain every other list here makes. | |
| 1952 | -(def overview-limit 24) | |
| 1979 | + This is how the overview stays about every room while still being a fixed | |
| 1980 | + number of lines. Taking the newest hundred outright would be the strip | |
| 1981 | + answering about whichever room is busiest — which is the one you can already | |
| 1982 | + see. A turn each means a room that said one thing all day is in the first | |
| 1983 | + handful, beside the room that has said a hundred." | |
| 1984 | + [colls] | |
| 1985 | + (lazy-seq | |
| 1986 | + (let [colls (remove empty? colls)] | |
| 1987 | + (when (seq colls) | |
| 1988 | + (concat (map first colls) | |
| 1989 | + (round-robin (map rest colls))))))) | |
| 1953 | 1990 | |
| 1954 | 1991 | (defn recent-everywhere |
| 1955 | - "The last `overview-per-channel` lines from every buffer at once, oldest | |
| 1956 | - first, and at most `overview-limit` of them. | |
| 1992 | + "The newest lines from every buffer at once, newest first, and at most | |
| 1993 | + `overview-limit` of them — a turn to each room until they run out. | |
| 1957 | 1994 | |
| 1958 | 1995 | Each carries the room it was said in, since that is the one thing a line |
| 1959 | 1996 | taken out of its own conversation no longer says for itself. |
| 1960 | 1997 | |
| 1961 | - Bounded per room before the sort, so the cost is the number of rooms rather | |
| 1962 | - than the length of their backlogs: a channel with a week of history in it | |
| 1963 | - must not make this the most expensive thing on the screen. | |
| 1998 | + Bounded per room before anything else, so the cost is the number of rooms | |
| 1999 | + rather than the length of their backlogs: a channel with a week of history | |
| 2000 | + in it must not make this the most expensive thing on the screen. | |
| 1964 | 2001 | |
| 1965 | 2002 | Joins, parts and the rest of the system's own chatter are left out. They are |
| 1966 | 2003 | the noise this strip would drown in: a room nobody has spoken in for a day |
| @@ -1972,13 +2009,24 @@ | ||
| 1972 | 2009 | rooms you are not looking at." |
| 1973 | 2010 | [] |
| 1974 | 2011 | (->> (dissoc @channels @current) |
| 1975 | - (mapcat (fn [[name buffer]] | |
| 1976 | - (->> (:messages buffer) | |
| 1977 | - (remove :system?) | |
| 1978 | - (take-last overview-per-channel) | |
| 1979 | - (map #(assoc % :channel name))))) | |
| 1980 | - (sort-by #(or (:at %) 0)) | |
| 1981 | - (take-last overview-limit))) | |
| 2012 | + (map (fn [[name buffer]] | |
| 2013 | + ;; Newest first, which is the order a turn each has to be taken | |
| 2014 | + ;; in: the first round is every room's latest line. | |
| 2015 | + (->> (:messages buffer) | |
| 2016 | + (remove :system?) | |
| 2017 | + (take-last overview-limit) | |
| 2018 | + reverse | |
| 2019 | + (map #(assoc % :channel name))))) | |
| 2020 | + (remove empty?) | |
| 2021 | + round-robin | |
| 2022 | + (take overview-limit) | |
| 2023 | + ;; Newest at the top, which is the other way round from a conversation | |
| 2024 | + ;; and right for the same reason a conversation is the way it is: what | |
| 2025 | + ;; you came to the strip for is what has just happened, and a list you | |
| 2026 | + ;; have to scroll to the bottom of to find it is a list that answers | |
| 2027 | + ;; last. The turn-taking above is about which lines are in it, not | |
| 2028 | + ;; about where they sit. | |
| 2029 | + (sort-by #(or (:at %) 0) >))) | |
| 1982 | 2030 | |
| 1983 | 2031 | (def last-preview rooms/last-preview) |
| 1984 | 2032 | |
| @@ -131,6 +131,32 @@ | |||
| 131 | (swap! overview? not) | 131 | (swap! overview? not) |
| 132 | (save-prefs!)) | 132 | (save-prefs!)) |
| 133 | 133 | ||
| 134 | +;; The room the reader was in when a line in the overview took them somewhere | ||
| 135 | +;; else, or nil. The strip is the one place in the app that moves you without | ||
| 136 | +;; you having asked to leave where you were — everything else is a room you | ||
| 137 | +;; chose — so it is the one place that owes you the way back. | ||
| 138 | +(defonce overview-return (atom nil)) | ||
| 139 | + | ||
| 140 | +(declare open-channel!) | ||
| 141 | + | ||
| 142 | +(defn leaving-for-overview! | ||
| 143 | + "Remember where we are, because a line in the strip is about to take us out | ||
| 144 | + of it. Nothing to remember if there is nowhere to go back to." | ||
| 145 | + [] | ||
| 146 | + (reset! overview-return @current)) | ||
| 147 | + | ||
| 148 | +(defn overview-back! | ||
| 149 | + "Back to the room the strip took you out of. | ||
| 150 | + | ||
| 151 | + The room, and not the place in it: the backlog's scroll is remembered under | ||
| 152 | + one name for every conversation — see `messages-scroll-key` — so what comes | ||
| 153 | + back is the room and whatever that one viewport is currently showing of it. | ||
| 154 | + A place of your own in every room is a bigger change than this button." | ||
| 155 | + [] | ||
| 156 | + (when-let [room @overview-return] | ||
| 157 | + (reset! overview-return nil) | ||
| 158 | + (open-channel! room))) | ||
| 159 | + | ||
| 134 | ;; The window's content width in points, polled from the backend a few times a | 160 | ;; The window's content width in points, polled from the backend a few times a |
| 135 | ;; second. The app is laid out for a phone-width window, and this is what lets | 161 | ;; second. The app is laid out for a phone-width window, and this is what lets |
| 136 | ;; a wide one be more than a phone with margins: past `wide-width` the channel | 162 | ;; a wide one be more than a phone with margins: past `wide-width` the channel |
| @@ -1939,28 +1965,39 @@ | |||
| 1939 | 1965 | ||
| 1940 | 1966 | ||
| 1941 | 1967 | ||
| 1942 | -;; How many lines the overview takes from each room. Per room rather than | 1968 | +;; How many lines the overview holds in all. |
| 1943 | -;; across all of them: the strip is there to say what is happening everywhere, | 1969 | +(def overview-limit 100) |
| 1944 | -;; and one busy channel taking the whole of a shared budget is the strip | 1970 | + |
| 1945 | -;; answering about one room — which is the room you are probably already in. | 1971 | +;; How many it shows without being scrolled — the terminal's whole answer, |
| 1946 | -(def overview-per-channel 3) | 1972 | +;; since a strip there is a handful of rows taken off a conversation that is |
| 1973 | +;; measured in how many of those fit. | ||
| 1974 | +(def overview-lines 8) | ||
| 1975 | + | ||
| 1976 | +(defn- round-robin | ||
| 1977 | + "The colls' firsts, then their seconds, and so on until they are spent. | ||
| 1947 | 1978 | ||
| 1948 | -;; And a ceiling on the lot, because the rooms are not a fixed number: at three | 1979 | + This is how the overview stays about every room while still being a fixed |
| 1949 | -;; apiece, a client in twenty channels would hand the strip sixty rows and the | 1980 | + number of lines. Taking the newest hundred outright would be the strip |
| 1950 | -;; conversation above it nothing. Past this the newest win, which is the same | 1981 | + answering about whichever room is busiest — which is the one you can already |
| 1951 | -;; bargain every other list here makes. | 1982 | + see. A turn each means a room that said one thing all day is in the first |
| 1952 | -(def overview-limit 24) | 1983 | + handful, beside the room that has said a hundred." |
| 1984 | + [colls] | ||
| 1985 | + (lazy-seq | ||
| 1986 | + (let [colls (remove empty? colls)] | ||
| 1987 | + (when (seq colls) | ||
| 1988 | + (concat (map first colls) | ||
| 1989 | + (round-robin (map rest colls))))))) | ||
| 1953 | 1990 | ||
| 1954 | (defn recent-everywhere | 1991 | (defn recent-everywhere |
| 1955 | - "The last `overview-per-channel` lines from every buffer at once, oldest | 1992 | + "The newest lines from every buffer at once, newest first, and at most |
| 1956 | - first, and at most `overview-limit` of them. | 1993 | + `overview-limit` of them — a turn to each room until they run out. |
| 1957 | 1994 | ||
| 1958 | Each carries the room it was said in, since that is the one thing a line | 1995 | Each carries the room it was said in, since that is the one thing a line |
| 1959 | taken out of its own conversation no longer says for itself. | 1996 | taken out of its own conversation no longer says for itself. |
| 1960 | 1997 | ||
| 1961 | - Bounded per room before the sort, so the cost is the number of rooms rather | 1998 | + Bounded per room before anything else, so the cost is the number of rooms |
| 1962 | - than the length of their backlogs: a channel with a week of history in it | 1999 | + rather than the length of their backlogs: a channel with a week of history |
| 1963 | - must not make this the most expensive thing on the screen. | 2000 | + in it must not make this the most expensive thing on the screen. |
| 1964 | 2001 | ||
| 1965 | Joins, parts and the rest of the system's own chatter are left out. They are | 2002 | Joins, parts and the rest of the system's own chatter are left out. They are |
| 1966 | the noise this strip would drown in: a room nobody has spoken in for a day | 2003 | the noise this strip would drown in: a room nobody has spoken in for a day |
| @@ -1972,13 +2009,24 @@ | |||
| 1972 | rooms you are not looking at." | 2009 | rooms you are not looking at." |
| 1973 | [] | 2010 | [] |
| 1974 | (->> (dissoc @channels @current) | 2011 | (->> (dissoc @channels @current) |
| 1975 | - (mapcat (fn [[name buffer]] | 2012 | + (map (fn [[name buffer]] |
| 1976 | - (->> (:messages buffer) | 2013 | + ;; Newest first, which is the order a turn each has to be taken |
| 1977 | - (remove :system?) | 2014 | + ;; in: the first round is every room's latest line. |
| 1978 | - (take-last overview-per-channel) | 2015 | + (->> (:messages buffer) |
| 1979 | - (map #(assoc % :channel name))))) | 2016 | + (remove :system?) |
| 1980 | - (sort-by #(or (:at %) 0)) | 2017 | + (take-last overview-limit) |
| 1981 | - (take-last overview-limit))) | 2018 | + reverse |
| 2019 | + (map #(assoc % :channel name))))) | ||
| 2020 | + (remove empty?) | ||
| 2021 | + round-robin | ||
| 2022 | + (take overview-limit) | ||
| 2023 | + ;; Newest at the top, which is the other way round from a conversation | ||
| 2024 | + ;; and right for the same reason a conversation is the way it is: what | ||
| 2025 | + ;; you came to the strip for is what has just happened, and a list you | ||
| 2026 | + ;; have to scroll to the bottom of to find it is a list that answers | ||
| 2027 | + ;; last. The turn-taking above is about which lines are in it, not | ||
| 2028 | + ;; about where they sit. | ||
| 2029 | + (sort-by #(or (:at %) 0) >))) | ||
| 1982 | 2030 | ||
| 1983 | (def last-preview rooms/last-preview) | 2031 | (def last-preview rooms/last-preview) |
| 1984 | 2032 | ||