nandi/frqpublic Fork
⑂ fc42e9b
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.

Download ZIP

A keyboard that does not help, and an overview that scrolls

Two things a phone was making worse.

A handle is not a sentence. A phone keyboard capitalises the first
letter, suggests a word it guessed at, and -- the one that prompted
this -- puts a space after a full stop, because a full stop ends a
sentence. It is also the middle of `alice.bsky.social`, and a handle
with a space in it resolves to nothing. `verbatim` asks for the text
to be taken as typed: no autocorrect, no suggestions, no
capitalisation, and the URL keyboard, which has a full stop on the
main plane and no space bar to be helpful with. Every field holding an
identifier gets it -- handle, host, port, nick, app password, and the
join box. The compose box is prose and keeps its help.

And the overview could not be scrolled, which is the whole reason it
showed eight entries: eight was what fitted, and anything below the
fold could not be reached at all. It scrolls now and shows forty, and
the pane takes a share of the column only while it is open -- an empty
wrapper claiming one would hold half the screen to show nothing.

The heading scrolls with the entries rather than staying above them,
and that is the arrangement that works rather than the nicer one: a
fixed heading wants an `Expanded` inside a pane that is already
claiming its own share with `expand`, and a Column cannot shrink-wrap
and fill at once.

`frq_ui_demo` gains a third room with a dozen lines in it, because a
test that the overview scrolls would otherwise pass on a list that
fits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-20T15:07:43-07:00 Browse files
fc42e9b parent: 535605d
modified flutter/lib/nim_renderer.dart +18 -0
@@ -888,10 +888,28 @@ class _NimAppState extends State<NimApp> {
888888 selection: TextSelection.collapsed(offset: value.length),
889889 );
890890 }
891+ // A field holding an identifier, not a sentence.
892+ //
893+ // A phone keyboard is built for prose and every one of its helps
894+ // is wrong here: it capitalises the first letter of a handle,
895+ // suggests a word it guessed at, and -- the one that prompted
896+ // this -- inserts a space after a full stop, because a full stop
897+ // ends a sentence. It is also the middle of `alice.bsky.social`,
898+ // and a handle with a space in it resolves to nothing.
899+ //
900+ // `TextInputType.url` is the layout that suits it: a full stop on
901+ // the main plane and no space bar to be helpful with.
902+ final verbatim = n.prop('verbatim', false);
891903 final field = TextField(
892904 controller: c,
893905 focusNode: _focus.putIfAbsent(key, FocusNode.new),
894906 style: _style(t.textBody, t.onBg),
907+ autocorrect: !verbatim,
908+ enableSuggestions: !verbatim,
909+ textCapitalization: verbatim
910+ ? TextCapitalization.none
911+ : TextCapitalization.sentences,
912+ keyboardType: verbatim ? TextInputType.url : null,
895913 decoration: InputDecoration(
896914 hintText: n.prop('placeholder', ''),
897915 hintStyle: _style(t.textBody, t.dim),
@@ -888,10 +888,28 @@ class _NimAppState extends State<NimApp> {
888 selection: TextSelection.collapsed(offset: value.length),888 selection: TextSelection.collapsed(offset: value.length),
889 );889 );
890 }890 }
891+ // A field holding an identifier, not a sentence.
892+ //
893+ // A phone keyboard is built for prose and every one of its helps
894+ // is wrong here: it capitalises the first letter of a handle,
895+ // suggests a word it guessed at, and -- the one that prompted
896+ // this -- inserts a space after a full stop, because a full stop
897+ // ends a sentence. It is also the middle of `alice.bsky.social`,
898+ // and a handle with a space in it resolves to nothing.
899+ //
900+ // `TextInputType.url` is the layout that suits it: a full stop on
901+ // the main plane and no space bar to be helpful with.
902+ final verbatim = n.prop('verbatim', false);
891 final field = TextField(903 final field = TextField(
892 controller: c,904 controller: c,
893 focusNode: _focus.putIfAbsent(key, FocusNode.new),905 focusNode: _focus.putIfAbsent(key, FocusNode.new),
894 style: _style(t.textBody, t.onBg),906 style: _style(t.textBody, t.onBg),
907+ autocorrect: !verbatim,
908+ enableSuggestions: !verbatim,
909+ textCapitalization: verbatim
910+ ? TextCapitalization.none
911+ : TextCapitalization.sentences,
912+ keyboardType: verbatim ? TextInputType.url : null,
895 decoration: InputDecoration(913 decoration: InputDecoration(
896 hintText: n.prop('placeholder', ''),914 hintText: n.prop('placeholder', ''),
897 hintStyle: _style(t.textBody, t.dim),915 hintStyle: _style(t.textBody, t.dim),
modified flutter/test/nim_layout_test.dart +24 -0
@@ -579,6 +579,30 @@ void main() {
579579 expectLaidOut(tester, 'the overview on a phone');
580580 });
581581
582+
583+ testWidgets('scrolls, so what is below the fold can be reached',
584+ (tester) async {
585+ // It did not. The pane drew its entries and whatever did not fit was
586+ // simply unreachable -- which is why there were only ever eight.
587+ core.demoUi();
588+ core.dispatch('overview.toggle');
589+ await layOut(tester, sizes['phone']!);
590+
591+ final list = find
592+ .ancestor(
593+ of: find.textContaining('sandbox-01'),
594+ matching: find.byType(Scrollable))
595+ .first;
596+ expect(list, findsOneWidget, reason: 'the overview is not scrollable');
597+
598+ final pos = tester.widget<Scrollable>(list).controller!.position;
599+ expect(pos.maxScrollExtent, greaterThan(0),
600+ reason: 'the overview has nothing below the fold to scroll to');
601+
602+ pos.jumpTo(pos.maxScrollExtent);
603+ await tester.pumpAndSettle();
604+ expectLaidOut(tester, 'the overview scrolled to its end');
605+ });
582606 testWidgets('and the whole entry is the control, not an arrow',
583607 (tester) async {
584608 core.demoUi();
@@ -579,6 +579,30 @@ void main() {
579 expectLaidOut(tester, 'the overview on a phone');579 expectLaidOut(tester, 'the overview on a phone');
580 });580 });
581 581
582+
583+ testWidgets('scrolls, so what is below the fold can be reached',
584+ (tester) async {
585+ // It did not. The pane drew its entries and whatever did not fit was
586+ // simply unreachable -- which is why there were only ever eight.
587+ core.demoUi();
588+ core.dispatch('overview.toggle');
589+ await layOut(tester, sizes['phone']!);
590+
591+ final list = find
592+ .ancestor(
593+ of: find.textContaining('sandbox-01'),
594+ matching: find.byType(Scrollable))
595+ .first;
596+ expect(list, findsOneWidget, reason: 'the overview is not scrollable');
597+
598+ final pos = tester.widget<Scrollable>(list).controller!.position;
599+ expect(pos.maxScrollExtent, greaterThan(0),
600+ reason: 'the overview has nothing below the fold to scroll to');
601+
602+ pos.jumpTo(pos.maxScrollExtent);
603+ await tester.pumpAndSettle();
604+ expectLaidOut(tester, 'the overview scrolled to its end');
605+ });
582 testWidgets('and the whole entry is the control, not an arrow',606 testWidgets('and the whole entry is the control, not an arrow',
583 (tester) async {607 (tester) async {
584 core.demoUi();608 core.demoUi();
modified nim/src/frq/screens/chat.nim +21 -5
@@ -23,7 +23,7 @@ const
2323 faceSize = 32
2424 pillSize = 20
2525 chipGap = 4
26- overviewLines* = 8
26+ overviewLines* = 40
2727
2828 sidePanelWidth = 150
2929 ## What a strip beside the backlog takes. Wide enough for a room name or
@@ -297,14 +297,14 @@ proc overviewPane(s: State): Node =
297297 # So the arrow is gone and the whole card takes the press, which is both
298298 # the larger target and the smaller thing to draw. What is left is what a
299299 # reader actually skims: who said it and where, then what they said.
300- var body = vbox(%*{"spacing": 4}, title2("Overview"))
300+ var list = vbox(%*{"spacing": 4})
301301 var n = 0
302302 for m in recentEverywhere(s.rooms, s.current):
303303 if n >= overviewLines: break
304304 n += 1
305305 # Each line carries the room it was said in, since that is the one thing a
306306 # line taken out of its own conversation no longer says for itself.
307- body.children.add card(
307+ list.children.add card(
308308 %*{"onClick": "overview.goto:" & m.room & ":" & rowId(m),
309309 "spacing": 2},
310310 hbox(%*{"spacing": 6}, dimLabel(m.room), label(m.frm)),
@@ -315,8 +315,19 @@ proc overviewPane(s: State): Node =
315315 # what a reader sees is bounded by the thing they are looking at.
316316 text(summarise(m.text, 160), lines = 2))
317317 if n == 0:
318- body.children.add dimLabel("Nothing has happened anywhere else.")
319- result = body
318+ list.children.add dimLabel("Nothing has happened anywhere else.")
319+
320+ # The whole pane scrolls, heading and all. There were eight entries
321+ # because eight was all that could be seen: nothing here scrolled, so
322+ # what fell below the fold could not be reached at all.
323+ #
324+ # The heading scrolls with them rather than staying put, and that is not
325+ # the nicer arrangement -- it is the one that works. A fixed heading over
326+ # a scrolling list wants an `Expanded` inside this pane, and `expand` is
327+ # already how the pane claims its own share of the column; a second one
328+ # nested in the first asks a Column to shrink-wrap and fill at once.
329+ result = scroll(%*{"scrollKey": "overview", "orientation": "vertical"},
330+ vbox(%*{"spacing": 4}, title2("Overview"), list))
320331
321332 proc lightboxPane(s: State): Node =
322333 ## The picture being looked at, as large as the window allows.
@@ -539,8 +550,13 @@ proc chatScreen*(s: State, connected: bool): Node =
539550
540551 # Both panels are in wrappers that are always there, for the reason the
541552 # error note is: a child that comes and goes renumbers the row.
553+ # `expand` only while it is open, and that is not a detail: an empty
554+ # wrapper claiming a share of the column would take half the screen to
555+ # show nothing. Open, it splits the height with the backlog above it --
556+ # both scroll, so neither has to be given up for the other.
542557 var overview = vbox(%*{"key": "overview-pane"})
543558 if s.overview:
559+ overview.props["expand"] = %true
544560 overview.children.add overviewPane(s)
545561
546562 var profile = vbox(%*{"key": "profile-pane"})
@@ -23,7 +23,7 @@ const
23 faceSize = 3223 faceSize = 32
24 pillSize = 2024 pillSize = 20
25 chipGap = 425 chipGap = 4
26- overviewLines* = 826+ overviewLines* = 40
27 27
28 sidePanelWidth = 15028 sidePanelWidth = 150
29 ## What a strip beside the backlog takes. Wide enough for a room name or29 ## What a strip beside the backlog takes. Wide enough for a room name or
@@ -297,14 +297,14 @@ proc overviewPane(s: State): Node =
297 # So the arrow is gone and the whole card takes the press, which is both297 # So the arrow is gone and the whole card takes the press, which is both
298 # the larger target and the smaller thing to draw. What is left is what a298 # the larger target and the smaller thing to draw. What is left is what a
299 # reader actually skims: who said it and where, then what they said.299 # reader actually skims: who said it and where, then what they said.
300- var body = vbox(%*{"spacing": 4}, title2("Overview"))300+ var list = vbox(%*{"spacing": 4})
301 var n = 0301 var n = 0
302 for m in recentEverywhere(s.rooms, s.current):302 for m in recentEverywhere(s.rooms, s.current):
303 if n >= overviewLines: break303 if n >= overviewLines: break
304 n += 1304 n += 1
305 # Each line carries the room it was said in, since that is the one thing a305 # Each line carries the room it was said in, since that is the one thing a
306 # line taken out of its own conversation no longer says for itself.306 # line taken out of its own conversation no longer says for itself.
307- body.children.add card(307+ list.children.add card(
308 %*{"onClick": "overview.goto:" & m.room & ":" & rowId(m),308 %*{"onClick": "overview.goto:" & m.room & ":" & rowId(m),
309 "spacing": 2},309 "spacing": 2},
310 hbox(%*{"spacing": 6}, dimLabel(m.room), label(m.frm)),310 hbox(%*{"spacing": 6}, dimLabel(m.room), label(m.frm)),
@@ -315,8 +315,19 @@ proc overviewPane(s: State): Node =
315 # what a reader sees is bounded by the thing they are looking at.315 # what a reader sees is bounded by the thing they are looking at.
316 text(summarise(m.text, 160), lines = 2))316 text(summarise(m.text, 160), lines = 2))
317 if n == 0:317 if n == 0:
318- body.children.add dimLabel("Nothing has happened anywhere else.")318+ list.children.add dimLabel("Nothing has happened anywhere else.")
319- result = body319+
320+ # The whole pane scrolls, heading and all. There were eight entries
321+ # because eight was all that could be seen: nothing here scrolled, so
322+ # what fell below the fold could not be reached at all.
323+ #
324+ # The heading scrolls with them rather than staying put, and that is not
325+ # the nicer arrangement -- it is the one that works. A fixed heading over
326+ # a scrolling list wants an `Expanded` inside this pane, and `expand` is
327+ # already how the pane claims its own share of the column; a second one
328+ # nested in the first asks a Column to shrink-wrap and fill at once.
329+ result = scroll(%*{"scrollKey": "overview", "orientation": "vertical"},
330+ vbox(%*{"spacing": 4}, title2("Overview"), list))
320 331
321 proc lightboxPane(s: State): Node =332 proc lightboxPane(s: State): Node =
322 ## The picture being looked at, as large as the window allows.333 ## The picture being looked at, as large as the window allows.
@@ -539,8 +550,13 @@ proc chatScreen*(s: State, connected: bool): Node =
539 550
540 # Both panels are in wrappers that are always there, for the reason the551 # Both panels are in wrappers that are always there, for the reason the
541 # error note is: a child that comes and goes renumbers the row.552 # error note is: a child that comes and goes renumbers the row.
553+ # `expand` only while it is open, and that is not a detail: an empty
554+ # wrapper claiming a share of the column would take half the screen to
555+ # show nothing. Open, it splits the height with the backlog above it --
556+ # both scroll, so neither has to be given up for the other.
542 var overview = vbox(%*{"key": "overview-pane"})557 var overview = vbox(%*{"key": "overview-pane"})
543 if s.overview:558 if s.overview:
559+ overview.props["expand"] = %true
544 overview.children.add overviewPane(s)560 overview.children.add overviewPane(s)
545 561
546 var profile = vbox(%*{"key": "profile-pane"})562 var profile = vbox(%*{"key": "profile-pane"})
modified nim/src/frq/screens/chats.nim +1 -1
@@ -54,7 +54,7 @@ func chatsScreen*(s: State, connected: bool): Node =
5454 var joinRow = hbox(%*{"spacing": 8, "align": "end"},
5555 button(if s.joinInput.startsWith("@"): "Message" else: "Join", "join"),
5656 entry("join-input", s.joinInput, "#channel or @nick", "join-input.change",
57- onSubmit = "join"))
57+ onSubmit = "join", verbatim = true))
5858
5959 var searchRow = hbox(%*{"spacing": 8, "align": "end"})
6060 if s.search.len > 0:
@@ -54,7 +54,7 @@ func chatsScreen*(s: State, connected: bool): Node =
54 var joinRow = hbox(%*{"spacing": 8, "align": "end"},54 var joinRow = hbox(%*{"spacing": 8, "align": "end"},
55 button(if s.joinInput.startsWith("@"): "Message" else: "Join", "join"),55 button(if s.joinInput.startsWith("@"): "Message" else: "Join", "join"),
56 entry("join-input", s.joinInput, "#channel or @nick", "join-input.change",56 entry("join-input", s.joinInput, "#channel or @nick", "join-input.change",
57- onSubmit = "join"))57+ onSubmit = "join", verbatim = true))
58 58
59 var searchRow = hbox(%*{"spacing": 8, "align": "end"})59 var searchRow = hbox(%*{"spacing": 8, "align": "end"})
60 if s.search.len > 0:60 if s.search.len > 0:
modified nim/src/frq/screens/connect.nim +9 -6
@@ -45,8 +45,10 @@ func serverFields(s: State): Node =
4545 vbox(%*{"spacing": 6},
4646 label("Server"),
4747 hbox(%*{"spacing": 8},
48- entry("host", s.formHost, "host", "host.change", width = 220),
49- entry("port", s.formPort, "6697", "port.change", width = 90)),
48+ entry("host", s.formHost, "host", "host.change", width = 220,
49+ verbatim = true),
50+ entry("port", s.formPort, "6697", "port.change", width = 90,
51+ verbatim = true)),
5052 checkbutton("TLS", s.formTls, "tls.toggle"))
5153
5254 func authFields(s: State): Node =
@@ -63,7 +65,7 @@ func authFields(s: State): Node =
6365 "never reaches this app."),
6466 label("Handle"),
6567 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",
66- width = 320))
68+ width = 320, verbatim = true))
6769 # Both of these are stable wrappers for the reason `errorNote` is one.
6870 var remembered = vbox(%*{"key": "remembered", "spacing": 4})
6971 if s.brokerToken.len > 0:
@@ -86,17 +88,18 @@ func authFields(s: State): Node =
8688 "is handed the session it mints."),
8789 label("Handle"),
8890 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",
89- width = 320),
91+ width = 320, verbatim = true),
9092 label("App password"),
9193 entry("app-password", s.formAppPassword, "xxxx-xxxx-xxxx-xxxx",
92- "app-password.change", width = 320),
94+ "app-password.change", width = 320, verbatim = true),
9395 dimLabel("Make one at bsky.app → Settings → App Passwords."))
9496
9597 of amGuest:
9698 result = vbox(%*{"spacing": 6},
9799 title2("Connect as guest"),
98100 label("Nick"),
99- entry("nick", s.formNick, "your nick", "nick.change", width = 320))
101+ entry("nick", s.formNick, "your nick", "nick.change", width = 320,
102+ verbatim = true))
100103
101104 func connectAction(s: State): Node =
102105 if s.connecting:
@@ -45,8 +45,10 @@ func serverFields(s: State): Node =
45 vbox(%*{"spacing": 6},45 vbox(%*{"spacing": 6},
46 label("Server"),46 label("Server"),
47 hbox(%*{"spacing": 8},47 hbox(%*{"spacing": 8},
48- entry("host", s.formHost, "host", "host.change", width = 220),48+ entry("host", s.formHost, "host", "host.change", width = 220,
49- entry("port", s.formPort, "6697", "port.change", width = 90)),49+ verbatim = true),
50+ entry("port", s.formPort, "6697", "port.change", width = 90,
51+ verbatim = true)),
50 checkbutton("TLS", s.formTls, "tls.toggle"))52 checkbutton("TLS", s.formTls, "tls.toggle"))
51 53
52 func authFields(s: State): Node =54 func authFields(s: State): Node =
@@ -63,7 +65,7 @@ func authFields(s: State): Node =
63 "never reaches this app."),65 "never reaches this app."),
64 label("Handle"),66 label("Handle"),
65 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",67 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",
66- width = 320))68+ width = 320, verbatim = true))
67 # Both of these are stable wrappers for the reason `errorNote` is one.69 # Both of these are stable wrappers for the reason `errorNote` is one.
68 var remembered = vbox(%*{"key": "remembered", "spacing": 4})70 var remembered = vbox(%*{"key": "remembered", "spacing": 4})
69 if s.brokerToken.len > 0:71 if s.brokerToken.len > 0:
@@ -86,17 +88,18 @@ func authFields(s: State): Node =
86 "is handed the session it mints."),88 "is handed the session it mints."),
87 label("Handle"),89 label("Handle"),
88 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",90 entry("handle", s.formHandle, "alice.bsky.social", "handle.change",
89- width = 320),91+ width = 320, verbatim = true),
90 label("App password"),92 label("App password"),
91 entry("app-password", s.formAppPassword, "xxxx-xxxx-xxxx-xxxx",93 entry("app-password", s.formAppPassword, "xxxx-xxxx-xxxx-xxxx",
92- "app-password.change", width = 320),94+ "app-password.change", width = 320, verbatim = true),
93 dimLabel("Make one at bsky.app → Settings → App Passwords."))95 dimLabel("Make one at bsky.app → Settings → App Passwords."))
94 96
95 of amGuest:97 of amGuest:
96 result = vbox(%*{"spacing": 6},98 result = vbox(%*{"spacing": 6},
97 title2("Connect as guest"),99 title2("Connect as guest"),
98 label("Nick"),100 label("Nick"),
99- entry("nick", s.formNick, "your nick", "nick.change", width = 320))101+ entry("nick", s.formNick, "your nick", "nick.change", width = 320,
102+ verbatim = true))
100 103
101 func connectAction(s: State): Node =104 func connectAction(s: State): Node =
102 if s.connecting:105 if s.connecting:
modified nim/src/frq/ui.nim +7 -1
@@ -80,13 +80,19 @@ func button*(text: string, onClick: string, kind = "default"): Node =
8080 n("button", %*{"label": text, "kind": kind, "onClick": onClick})
8181
8282 func entry*(key, text, placeholder, onChange: string, width = 0,
83- onSubmit = ""): Node =
83+ onSubmit = "", verbatim = false): Node =
8484 ## Every entry carries a key, and for the reason the Clojure's comment
8585 ## gives: a renderer that keeps a text controller per field needs a stable
8686 ## name for it, and without one the host and the port shared a controller
8787 ## and both showed the port.
8888 var p = %*{"key": key, "text": text, "placeholder": placeholder,
8989 "onChange": onChange}
90+ # An identifier rather than prose: a handle, a host, a nick. A phone
91+ # keyboard helps with prose and the help is wrong here -- it capitalises
92+ # the first letter, offers a word it guesses at, and puts a space after a
93+ # full stop, which is a sentence ending and is also the middle of
94+ # `alice.bsky.social`. `verbatim` asks for the text to be taken as typed.
95+ if verbatim: p["verbatim"] = %true
9096 if width > 0: p["widthRequest"] = %width
9197 # Enter, where the field has something to do with it. A compose box that
9298 # only sends on a button click is one nobody can type into at speed.
@@ -80,13 +80,19 @@ func button*(text: string, onClick: string, kind = "default"): Node =
80 n("button", %*{"label": text, "kind": kind, "onClick": onClick})80 n("button", %*{"label": text, "kind": kind, "onClick": onClick})
81 81
82 func entry*(key, text, placeholder, onChange: string, width = 0,82 func entry*(key, text, placeholder, onChange: string, width = 0,
83- onSubmit = ""): Node =83+ onSubmit = "", verbatim = false): Node =
84 ## Every entry carries a key, and for the reason the Clojure's comment84 ## Every entry carries a key, and for the reason the Clojure's comment
85 ## gives: a renderer that keeps a text controller per field needs a stable85 ## gives: a renderer that keeps a text controller per field needs a stable
86 ## name for it, and without one the host and the port shared a controller86 ## name for it, and without one the host and the port shared a controller
87 ## and both showed the port.87 ## and both showed the port.
88 var p = %*{"key": key, "text": text, "placeholder": placeholder,88 var p = %*{"key": key, "text": text, "placeholder": placeholder,
89 "onChange": onChange}89 "onChange": onChange}
90+ # An identifier rather than prose: a handle, a host, a nick. A phone
91+ # keyboard helps with prose and the help is wrong here -- it capitalises
92+ # the first letter, offers a word it guesses at, and puts a space after a
93+ # full stop, which is a sentence ending and is also the middle of
94+ # `alice.bsky.social`. `verbatim` asks for the text to be taken as typed.
95+ if verbatim: p["verbatim"] = %true
90 if width > 0: p["widthRequest"] = %width96 if width > 0: p["widthRequest"] = %width
91 # Enter, where the field has something to do with it. A compose box that97 # Enter, where the field has something to do with it. A compose box that
92 # only sends on a button click is one nobody can type into at speed.98 # only sends on a button click is one nobody can type into at speed.
modified nim/src/frq_core.nim +12 -0
@@ -265,6 +265,18 @@ proc frq_ui_demo*() {.exportc, dynlib.} =
265265 at: t0 + 200_000)]
266266 app.rooms["#tasks"] = other
267267
268+ # A third room, and enough in it that the overview is taller than a phone.
269+ # Without one nothing here would ever need to scroll, and a test that the
270+ # overview scrolls would pass on a list that fits.
271+ app.rooms.ensureRoom("#busy")
272+ var busy = app.rooms["#busy"]
273+ busy.joined = true
274+ for i in 1 .. 12:
275+ busy.messages.add Message(id: "b" & $i, frm: "carol",
276+ text: "line " & $i & " of a long day",
277+ at: t0 + 300_000 + i.int64 * 1000)
278+ app.rooms["#busy"] = busy
279+
268280 app.current = "#test"
269281 app.screen = scChat
270282 app.status = "Connected as me"
@@ -265,6 +265,18 @@ proc frq_ui_demo*() {.exportc, dynlib.} =
265 at: t0 + 200_000)]265 at: t0 + 200_000)]
266 app.rooms["#tasks"] = other266 app.rooms["#tasks"] = other
267 267
268+ # A third room, and enough in it that the overview is taller than a phone.
269+ # Without one nothing here would ever need to scroll, and a test that the
270+ # overview scrolls would pass on a list that fits.
271+ app.rooms.ensureRoom("#busy")
272+ var busy = app.rooms["#busy"]
273+ busy.joined = true
274+ for i in 1 .. 12:
275+ busy.messages.add Message(id: "b" & $i, frm: "carol",
276+ text: "line " & $i & " of a long day",
277+ at: t0 + 300_000 + i.int64 * 1000)
278+ app.rooms["#busy"] = busy
279+
268 app.current = "#test"280 app.current = "#test"
269 app.screen = scChat281 app.screen = scChat
270 app.status = "Connected as me"282 app.status = "Connected as me"
modified nim/tests/tscreens.nim +12 -0
@@ -32,6 +32,18 @@ suite "the connect screen":
3232 check "frq" in t.labels("title")
3333 check cs.transportNote in t.labels("dim-label")
3434
35+ test "the fields holding an identifier ask to be taken as typed":
36+ # A phone keyboard puts a space after a full stop, because a full stop
37+ # ends a sentence -- and is also the middle of `alice.bsky.social`. The
38+ # compose box is prose and is left alone; these are not.
39+ for mode in [amGuest, amBluesky, amAppPassword]:
40+ var f = initState()
41+ f.authMode = mode
42+ let fields = cs.connectScreen(f).find("entry")
43+ check fields.len > 0
44+ for e in fields:
45+ check e.props{"verbatim"}.getBool() == true
46+
3547 test "is pure — twice with no change is the same tree":
3648 check $cs.connectScreen(s).toJson == $cs.connectScreen(s).toJson
3749
@@ -32,6 +32,18 @@ suite "the connect screen":
32 check "frq" in t.labels("title")32 check "frq" in t.labels("title")
33 check cs.transportNote in t.labels("dim-label")33 check cs.transportNote in t.labels("dim-label")
34 34
35+ test "the fields holding an identifier ask to be taken as typed":
36+ # A phone keyboard puts a space after a full stop, because a full stop
37+ # ends a sentence -- and is also the middle of `alice.bsky.social`. The
38+ # compose box is prose and is left alone; these are not.
39+ for mode in [amGuest, amBluesky, amAppPassword]:
40+ var f = initState()
41+ f.authMode = mode
42+ let fields = cs.connectScreen(f).find("entry")
43+ check fields.len > 0
44+ for e in fields:
45+ check e.props{"verbatim"}.getBool() == true
46+
35 test "is pure — twice with no change is the same tree":47 test "is pure — twice with no change is the same tree":
36 check $cs.connectScreen(s).toJson == $cs.connectScreen(s).toJson48 check $cs.connectScreen(s).toJson == $cs.connectScreen(s).toJson
37 49