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

tchat.nim · 190 lines · 7.5 KBNim Blame HistoryRaw
The chat screen, in Nim 87a28cc nandi 18h ago1## The conversation screen.
2
3import std/[json, sequtils, strutils, tables, unicode, unittest]
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi 15h ago4import frq/[ui, cells, model, textruns]
The chat screen, in Nim 87a28cc nandi 18h ago5import frq/screens/chat as cs
6
7proc find(node: Node, tag: string): seq[Node] =
8 if node.isNil: return
9 if node.tag == tag: result.add node
10 for c in node.children: result.add c.find(tag)
11
12proc labels(node: Node, tag: string): seq[string] =
13 node.find(tag).mapIt(it.props{"label"}.getStr())
14
15proc texts(node: Node): seq[string] =
16 node.find("text").mapIt(it.props{"text"}.getStr())
17
18proc withRoom(): State =
19 result = initState()
20 var r = initRoom("#test")
21 r.joined = true
Four more modules into Nim: members, glyphs, emoji, store 3975b4b nandi 11h ago22 r.users = {"alice": "@", "bob": ""}.toTable
The chat screen, in Nim 87a28cc nandi 18h ago23 r.messages = @[
24 Message(id: "1", frm: "alice", text: "hello", at: 1_700_000_000_000),
25 Message(id: "2", frm: "frq-guest", text: "hi back", at: 1_700_000_060_000)]
26 result.rooms["#test"] = r
27 result.current = "#test"
28 result.formNick = "frq-guest"
29
30suite "summarise":
31 test "collapses whitespace and cuts to fit":
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi 15h ago32 check summarise("a\n b", 36) == "a b"
33 check summarise("x".repeat(50), 10).runeLen == 10
34 check summarise("x".repeat(50), 10).endsWith("")
The chat screen, in Nim 87a28cc nandi 18h ago35 test "leaves a short line alone":
Quality pass: reuse, dead weight, and two real costs 4dfc719 nandi 15h ago36 check summarise("short", 36) == "short"
The chat screen, in Nim 87a28cc nandi 18h ago37
38suite "the chat screen":
39 setup:
40 var s = withRoom()
41
42 test "shows the room name and its messages":
43 let t = cs.chatScreen(s, true)
44 check "#test" in t.labels("title")
45 check "hello" in t.texts
46 check "hi back" in t.texts
47
48 test "is pure":
49 check $cs.chatScreen(s, true).toJson == $cs.chatScreen(s, true).toJson
50
51 test "an empty room says so rather than showing nothing":
52 var e = initState()
53 e.rooms["#empty"] = initRoom("#empty")
54 e.current = "#empty"
55 check "Nothing here yet." in cs.chatScreen(e, true).labels("dim-label")
56
57 test "every line names its sender":
58 # Not the first of a run only: answering the fourth line of a collapsed
59 # run quotes back a line with no name on it.
60 check cs.chatScreen(s, true).labels("label").countIt(it == "alice") == 1
61 check cs.chatScreen(s, true).labels("label").countIt(it == "frq-guest") == 1
62
63 test "a day heading appears where the day changes, once":
64 # `at` is milliseconds. Testing it with seconds put every message on the
65 # same day in 1970 and the headings quietly stopped appearing, which is
66 # how the unit mismatch in the model was found.
67 let t = cs.chatScreen(s, true)
68 # Both messages are the same day, so one heading for the pair.
69 check t.find("separator").len >= 1
70 var r = s.rooms["#test"]
71 r.messages.add Message(id: "3", frm: "a", text: "next day",
72 at: 1_700_200_000_000)
73 s.rooms["#test"] = r
74 let t2 = cs.chatScreen(s, true)
75 check t2.find("separator").len > t.find("separator").len
76
77 test "only our own lines get a pencil":
78 let chips = cs.chatScreen(s, true).find("reaction")
79 .mapIt(it.props{"emoji"}.getStr())
80 # 🙂 and ↩️ on both messages, ✏️ on ours alone.
81 check chips.countIt(it == "✏️") == 1
82 check chips.countIt(it == "🙂") == 2
83
84 test "a line with no msgid gets a spacer where the chips would be":
85 var r = s.rooms["#test"]
86 r.messages.add Message(frm: "x", text: "no id", at: 1_700_000_120_000)
87 s.rooms["#test"] = r
88 # A row of the same shape rather than a row with a hole in it.
89 check cs.chatScreen(s, true).find("spacer").len >= 1
90
91 test "reaction pills carry their count and whether they are mine":
92 var r = s.rooms["#test"]
93 r.messages[0].reactions = @[Reaction(emoji: "👍", nicks: @["frq-guest", "bob"])]
94 s.rooms["#test"] = r
95 let pills = cs.chatScreen(s, true).find("reaction")
96 .filterIt(it.props{"emoji"}.getStr() == "👍")
97 check pills.len == 1
98 check pills[0].props{"count"}.getInt() == 2
99 check pills[0].props{"mine"}.getBool()
100
101 test "links in a message become link runs":
102 var r = s.rooms["#test"]
103 r.messages[0].text = "see https://example.com now"
104 s.rooms["#test"] = r
105 check cs.chatScreen(s, true).find("link").len == 1
106
107 test "a picture becomes an image with a lightbox click":
108 var r = s.rooms["#test"]
109 r.messages[0].imageUrl = "https://x.com/a.png"
110 s.rooms["#test"] = r
111 let img = cs.chatScreen(s, true).find("image")
112 .filterIt(it.props{"src"}.getStr() == "https://x.com/a.png")
113 check img.len == 1
114 check img[0].props{"onClick"}.getStr().startsWith("lightbox:")
115
116 test "a reply quotes what it answers, and offers a way there":
117 var r = s.rooms["#test"]
118 r.messages[1].replyTo = "1"
119 s.rooms["#test"] = r
120 let t = cs.chatScreen(s, true)
121 check t.labels("dim-label").anyIt("↩ alice: hello" in it)
122 check "" in t.labels("button")
123
124 test "a reply to something we no longer hold says so":
125 var r = s.rooms["#test"]
126 r.messages[1].replyTo = "gone"
127 s.rooms["#test"] = r
128 check "↩ (an earlier message)" in cs.chatScreen(s, true).labels("dim-label")
129
130 test "hiding join/part takes the system lines out":
131 var r = s.rooms["#test"]
132 r.messages.add Message(frm: "*", text: "bob joined", system: true,
133 at: 1_700_000_120_000)
134 s.rooms["#test"] = r
135 check "bob joined" in cs.chatScreen(s, true).texts
136 s.hideJoinPart = true
137 check "bob joined" notin cs.chatScreen(s, true).texts
138
139 test "the three banners hold their place whether or not they show":
140 let bare = cs.chatScreen(s, true).find("vbox").mapIt(it.props{"key"}.getStr())
141 s.replyingTo = ReplyTarget(has: true, frm: "alice", text: "hello")
142 s.editing = EditTarget(has: true, id: "2")
143 s.attachment = Attachment(has: true, path: "/tmp/a.png", status: usUploading)
144 let full = cs.chatScreen(s, true).find("vbox").mapIt(it.props{"key"}.getStr())
145 for k in ["replying", "editing", "attachment"]:
146 check k in bare
147 check k in full
148
149 test "the banners say what they are for":
150 s.replyingTo = ReplyTarget(has: true, frm: "alice", text: "hello")
151 s.attachment = Attachment(has: true, path: "/p.png", status: usUploading)
152 let t = cs.chatScreen(s, true)
153 check t.labels("dim-label").anyIt("↩ alice: hello" in it)
154 check "Uploading…" in t.labels("dim-label")
155 s.attachment.status = usReady
156 check "Picture attached" in cs.chatScreen(s, true).labels("dim-label")
157
158 test "Back to chats on a narrow window, fold on a wide one":
159 check "← Chats" in cs.chatScreen(s, true).labels("button")
160 s.windowWidth = 1200
161 let wide = cs.chatScreen(s, true)
162 check "← Chats" notin wide.labels("button")
163 check "☰ Chats" in wide.labels("button")
164
165 test "People is offered in a channel and not in a DM":
166 check cs.chatScreen(s, true).labels("button").anyIt(it.startsWith("People"))
167 var dmState = initState()
168 var d = initRoom("alice")
169 dmState.rooms["alice"] = d
170 dmState.current = "alice"
171 check not cs.chatScreen(dmState, true).labels("button")
172 .anyIt(it.startsWith("People"))
173
174 test "the people panel lists who is here, only when asked":
175 check "People" notin cs.chatScreen(s, true).labels("title-2")
176 s.showUsers = true
177 s.windowWidth = 1200
178 let t = cs.chatScreen(s, true)
179 check "People" in t.labels("title-2")
180 check "alice" in t.labels("label")
181
182 test "Jump to present only when we are not at it":
183 check "↓ Jump to present" notin cs.chatScreen(s, true).labels("button")
184 s.atPresent = false
185 check "↓ Jump to present" in cs.chatScreen(s, true).labels("button")
186
187 test "the compose bar is always there, with a send":
188 let t = cs.chatScreen(s, true)
189 check "draft" in t.find("entry").mapIt(it.props{"key"}.getStr())
190 check "Send" in t.labels("button")