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

Back in the room you were last in

Connecting put the reader on the overview, which is the right answer for
somebody who has just arrived and the wrong one for somebody coming back to
a conversation they were in the middle of.

No new thing is stored. `accessed` is already saved beside each room name
and already means "when this was last opened", so the largest of them is
the last room visited -- the same key `channelList` sorts the overview by,
which is why the answer always agrees with the top of that list. A
separately remembered name would be a second fact to keep in step with the
first, and the one that drifts is the one nothing reads.

Zero is never-opened rather than long-ago: a channel the server put us in,
or a DM that arrived while we were reading something else. A list of
nothing but those has no last room, so a first run still lands on the
overview exactly as it did.

Only on a run's first connection. 001 arrives again every time a dropped
socket registers, and without that gate a reader who had deliberately gone
out to the overview would be thrown back into a room by a network blip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-20T18:51:34-07:00 Browse files
ff28020 parent: f4ea36a
modified nim/src/frq/reducer.nim +22 -0
@@ -33,6 +33,14 @@ var
3333 ## What this connection is signing in as, settled before the socket opens.
3434 caps: HashSet[string]
3535 ## What the server has ACKed so far, threaded through `handshake.step`.
36+ landed: bool
37+ ## Whether this run has already been put back where it left off.
38+ ##
39+ ## A flag and not a check of the screen, because 001 arrives more than
40+ ## once: a dropped socket reconnects and registers again, and a reader
41+ ## who had deliberately gone out to the overview should not be thrown
42+ ## back into a room by a network blip. The restore is a thing this run
43+ ## does once, on the first connection it makes.
3644
3745 proc setError(msg: string) =
3846 app.error = msg
@@ -925,6 +933,20 @@ proc drain*() =
925933 # JOINs are what fills an empty one.
926934 if asked == 0: send("JOIN #test")
927935
936+ # Back into the room the reader was last in, rather than the list of
937+ # them. The name is not stored separately: `accessed` is already saved
938+ # per room and already means "when this was last opened", so the most
939+ # recent of them is the answer and there is no second thing to keep in
940+ # step with the first.
941+ #
942+ # Only on this run's first connection — see `landed`. And only where
943+ # there is one: a first run has a list of rooms it has never opened,
944+ # and lands on the overview as it always did.
945+ if not landed:
946+ landed = true
947+ let last = app.rooms.lastVisited
948+ if last.len > 0: openRoom(last)
949+
928950 of "PRIVMSG":
929951 if p.params.len >= 2:
930952 let target = p.params[0]
@@ -33,6 +33,14 @@ var
33 ## What this connection is signing in as, settled before the socket opens.33 ## What this connection is signing in as, settled before the socket opens.
34 caps: HashSet[string]34 caps: HashSet[string]
35 ## What the server has ACKed so far, threaded through `handshake.step`.35 ## What the server has ACKed so far, threaded through `handshake.step`.
36+ landed: bool
37+ ## Whether this run has already been put back where it left off.
38+ ##
39+ ## A flag and not a check of the screen, because 001 arrives more than
40+ ## once: a dropped socket reconnects and registers again, and a reader
41+ ## who had deliberately gone out to the overview should not be thrown
42+ ## back into a room by a network blip. The restore is a thing this run
43+ ## does once, on the first connection it makes.
36 44
37 proc setError(msg: string) =45 proc setError(msg: string) =
38 app.error = msg46 app.error = msg
@@ -925,6 +933,20 @@ proc drain*() =
925 # JOINs are what fills an empty one.933 # JOINs are what fills an empty one.
926 if asked == 0: send("JOIN #test")934 if asked == 0: send("JOIN #test")
927 935
936+ # Back into the room the reader was last in, rather than the list of
937+ # them. The name is not stored separately: `accessed` is already saved
938+ # per room and already means "when this was last opened", so the most
939+ # recent of them is the answer and there is no second thing to keep in
940+ # step with the first.
941+ #
942+ # Only on this run's first connection — see `landed`. And only where
943+ # there is one: a first run has a list of rooms it has never opened,
944+ # and lands on the overview as it always did.
945+ if not landed:
946+ landed = true
947+ let last = app.rooms.lastVisited
948+ if last.len > 0: openRoom(last)
949+
928 of "PRIVMSG":950 of "PRIVMSG":
929 if p.params.len >= 2:951 if p.params.len >= 2:
930 let target = p.params[0]952 let target = p.params[0]
modified nim/src/frq/rooms.nim +17 -0
@@ -59,6 +59,23 @@ func channelList*(channels: OrderedTable[string, Room], search: string): seq[Roo
5959 else:
6060 cmp(a.name, b.name))
6161
62+func lastVisited*(channels: OrderedTable[string, Room]): string =
63+ ## The room the reader had open when they last put the client down, or "".
64+ ##
65+ ## `accessed` is stamped by `openRoom` and saved beside the name, so the
66+ ## largest one is the last room opened — the same key `channelList` sorts
67+ ## by, which is why this always agrees with the top of that list.
68+ ##
69+ ## Zero is never-opened rather than long-ago: a channel the server put us
70+ ## in, or a DM that arrived while we were reading something else. A list
71+ ## with nothing but those has no last room, and the overview is the honest
72+ ## answer for a reader who has not yet been anywhere.
73+ var best: int64 = 0
74+ for _, ch in channels:
75+ if ch.accessed > best:
76+ best = ch.accessed
77+ result = ch.name
78+
6279 func mine*(m: Message, me: string): bool =
6380 ## Whether we are the one who said this.
6481 ##
@@ -59,6 +59,23 @@ func channelList*(channels: OrderedTable[string, Room], search: string): seq[Roo
59 else:59 else:
60 cmp(a.name, b.name))60 cmp(a.name, b.name))
61 61
62+func lastVisited*(channels: OrderedTable[string, Room]): string =
63+ ## The room the reader had open when they last put the client down, or "".
64+ ##
65+ ## `accessed` is stamped by `openRoom` and saved beside the name, so the
66+ ## largest one is the last room opened — the same key `channelList` sorts
67+ ## by, which is why this always agrees with the top of that list.
68+ ##
69+ ## Zero is never-opened rather than long-ago: a channel the server put us
70+ ## in, or a DM that arrived while we were reading something else. A list
71+ ## with nothing but those has no last room, and the overview is the honest
72+ ## answer for a reader who has not yet been anywhere.
73+ var best: int64 = 0
74+ for _, ch in channels:
75+ if ch.accessed > best:
76+ best = ch.accessed
77+ result = ch.name
78+
62 func mine*(m: Message, me: string): bool =79 func mine*(m: Message, me: string): bool =
63 ## Whether we are the one who said this.80 ## Whether we are the one who said this.
64 ##81 ##
modified nim/tests/trooms.nim +32 -0
@@ -197,3 +197,35 @@ suite "adoptEcho":
197197
198198 test "different text is not adopted":
199199 check not r.adoptEcho("me", "something else", "srv-9", 250, "")
200+
201+suite "lastVisited":
202+ ## Where the client puts a returning reader down.
203+ proc roomAt(name: string, accessed: int64): Room =
204+ result = initRoom(name)
205+ result.accessed = accessed
206+
207+ test "the most recently opened room":
208+ var t = initOrderedTable[string, Room]()
209+ t["#a"] = roomAt("#a", 100)
210+ t["#b"] = roomAt("#b", 300)
211+ t["#c"] = roomAt("#c", 200)
212+ check t.lastVisited == "#b"
213+
214+ test "rooms that were never opened are not a last room":
215+ # A channel the server put us in, or a DM that arrived while we were
216+ # elsewhere: `accessed` is zero, and zero is never rather than long ago.
217+ var t = initOrderedTable[string, Room]()
218+ t["#a"] = roomAt("#a", 0)
219+ t["#b"] = roomAt("#b", 0)
220+ check t.lastVisited == ""
221+
222+ test "an empty list has no last room":
223+ var t = initOrderedTable[string, Room]()
224+ check t.lastVisited == ""
225+
226+ test "one opened room among unopened ones wins":
227+ var t = initOrderedTable[string, Room]()
228+ t["#a"] = roomAt("#a", 0)
229+ t["#b"] = roomAt("#b", 42)
230+ t["#c"] = roomAt("#c", 0)
231+ check t.lastVisited == "#b"
@@ -197,3 +197,35 @@ suite "adoptEcho":
197 197
198 test "different text is not adopted":198 test "different text is not adopted":
199 check not r.adoptEcho("me", "something else", "srv-9", 250, "")199 check not r.adoptEcho("me", "something else", "srv-9", 250, "")
200+
201+suite "lastVisited":
202+ ## Where the client puts a returning reader down.
203+ proc roomAt(name: string, accessed: int64): Room =
204+ result = initRoom(name)
205+ result.accessed = accessed
206+
207+ test "the most recently opened room":
208+ var t = initOrderedTable[string, Room]()
209+ t["#a"] = roomAt("#a", 100)
210+ t["#b"] = roomAt("#b", 300)
211+ t["#c"] = roomAt("#c", 200)
212+ check t.lastVisited == "#b"
213+
214+ test "rooms that were never opened are not a last room":
215+ # A channel the server put us in, or a DM that arrived while we were
216+ # elsewhere: `accessed` is zero, and zero is never rather than long ago.
217+ var t = initOrderedTable[string, Room]()
218+ t["#a"] = roomAt("#a", 0)
219+ t["#b"] = roomAt("#b", 0)
220+ check t.lastVisited == ""
221+
222+ test "an empty list has no last room":
223+ var t = initOrderedTable[string, Room]()
224+ check t.lastVisited == ""
225+
226+ test "one opened room among unopened ones wins":
227+ var t = initOrderedTable[string, Room]()
228+ t["#a"] = roomAt("#a", 0)
229+ t["#b"] = roomAt("#b", 42)
230+ t["#c"] = roomAt("#c", 0)
231+ check t.lastVisited == "#b"