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>
ff28020 parent: f4ea36a modified
nim/src/frq/reducer.nim +22 -0 | @@ -33,6 +33,14 @@ var | ||
| 33 | 33 | ## What this connection is signing in as, settled before the socket opens. |
| 34 | 34 | caps: HashSet[string] |
| 35 | 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 | 45 | proc setError(msg: string) = |
| 38 | 46 | app.error = msg |
| @@ -925,6 +933,20 @@ proc drain*() = | ||
| 925 | 933 | # JOINs are what fills an empty one. |
| 926 | 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 | 950 | of "PRIVMSG": |
| 929 | 951 | if p.params.len >= 2: |
| 930 | 952 | 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 = msg | 46 | 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 | ||
| 59 | 59 | else: |
| 60 | 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 | 79 | func mine*(m: Message, me: string): bool = |
| 63 | 80 | ## Whether we are the one who said this. |
| 64 | 81 | ## |
| @@ -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": | ||
| 197 | 197 | |
| 198 | 198 | test "different text is not adopted": |
| 199 | 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" | |
| @@ -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" | ||