Name every line, and let the backlog hear about a jump
Two reasons a jump to a line in another room did nothing, both of them this side of the window. A line is named by the server's `msgid`, and not every line has one: a replayed backlog can arrive with no tags at all, and a line this client has just sent has none until it is echoed back. Those lines are on the screen and in the overview all the same, and pressing one should go to it — so they get a name of their own, made out of who said it, what it said, when, and where. A `:local-id` is never sent: a reply, an edit and a reaction all name a message to the server, and the server knows only the names it gave out. Everything that is the client's own business with a line goes through `rooms/row-id` instead, which prefers the server's name and falls back to ours. Which room you jumped into decided whether jumping worked at all, and this is half of why. The other half is that the backlog was never told. `scroll-here` came from a cell per row — the same trick that keeps a hover from waking a hundred rows — but a row's cell was deref'd while the seq of rows was realised, which is neither the row's render nor the backlog's, so the read was recorded against nobody. `jump-to` changed, the cells changed, and no component was woken to put the new answer in the tree: the prop reached the window on the render that opened the room and never again, so a jump that needed a second frame — which is every jump into a room that has to be built first — never got one. The backlog reads the pair itself now and hands them down as values. A jump is a click; re-rendering a backlog on one is affordable in a way that being wrong about it is not. Found with `JOLT_SCROLL_LOG`, which is in jolt-native for the next one: neither of these was going to be found by reading, and the first fix for the second one was an accident — a debug line that deref'd `jump-to` in the backlog's own render, and so subscribed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8d531f8 parent: 433c3c8 modified
common/frq/rooms.cljc +20 -0 | @@ -14,6 +14,26 @@ | ||
| 14 | 14 | [name] |
| 15 | 15 | (and (seq name) (not (str/starts-with? name "#")))) |
| 16 | 16 | |
| 17 | +(defn row-id | |
| 18 | + "What this client calls a line: the server's name for it where there is one, | |
| 19 | + and the name it was given here where there is not. | |
| 20 | + | |
| 21 | + freeq tags a message with a `msgid` and that is a line's identity everywhere | |
| 22 | + it matters — a reply points at one, an edit rewrites one, a reaction lands on | |
| 23 | + one. But not every line arrives with one: a replayed backlog can come over | |
| 24 | + with no tags at all, and a line this client has just sent has none until the | |
| 25 | + server echoes it back. | |
| 26 | + | |
| 27 | + Those lines are not nameless to the reader, though. They are on the screen, | |
| 28 | + they are in the overview, and pressing one should go to it — so `frq.state` | |
| 29 | + gives them a `:local-id` made out of what they are. It is never sent: a | |
| 30 | + reply, an edit and a reaction all name a message to the server, and the | |
| 31 | + server knows only the names it gave out. This is for everything that is the | |
| 32 | + client's own business with a line — which one to scroll to, and which one to | |
| 33 | + mark when it gets there." | |
| 34 | + [m] | |
| 35 | + (or (:id m) (:local-id m))) | |
| 36 | + | |
| 17 | 37 | (defn last-preview [buffer] |
| 18 | 38 | (if-let [m (last (:messages buffer))] |
| 19 | 39 | (str (:from m) ": " (:text m)) |
| @@ -14,6 +14,26 @@ | |||
| 14 | [name] | 14 | [name] |
| 15 | (and (seq name) (not (str/starts-with? name "#")))) | 15 | (and (seq name) (not (str/starts-with? name "#")))) |
| 16 | 16 | ||
| 17 | +(defn row-id | ||
| 18 | + "What this client calls a line: the server's name for it where there is one, | ||
| 19 | + and the name it was given here where there is not. | ||
| 20 | + | ||
| 21 | + freeq tags a message with a `msgid` and that is a line's identity everywhere | ||
| 22 | + it matters — a reply points at one, an edit rewrites one, a reaction lands on | ||
| 23 | + one. But not every line arrives with one: a replayed backlog can come over | ||
| 24 | + with no tags at all, and a line this client has just sent has none until the | ||
| 25 | + server echoes it back. | ||
| 26 | + | ||
| 27 | + Those lines are not nameless to the reader, though. They are on the screen, | ||
| 28 | + they are in the overview, and pressing one should go to it — so `frq.state` | ||
| 29 | + gives them a `:local-id` made out of what they are. It is never sent: a | ||
| 30 | + reply, an edit and a reaction all name a message to the server, and the | ||
| 31 | + server knows only the names it gave out. This is for everything that is the | ||
| 32 | + client's own business with a line — which one to scroll to, and which one to | ||
| 33 | + mark when it gets there." | ||
| 34 | + [m] | ||
| 35 | + (or (:id m) (:local-id m))) | ||
| 36 | + | ||
| 17 | (defn last-preview [buffer] | 37 | (defn last-preview [buffer] |
| 18 | (if-let [m (last (:messages buffer))] | 38 | (if-let [m (last (:messages buffer))] |
| 19 | (str (:from m) ": " (:text m)) | 39 | (str (:from m) ": " (:text m)) |
modified
common/frq/screens/chat.cljc +38 -24 | @@ -753,16 +753,16 @@ | ||
| 753 | 753 | Sender above the text, not beside it: a wrapping label in a horizontal row |
| 754 | 754 | lays out against the row's width rather than the column's, so one long URL |
| 755 | 755 | drags every line that follows it off the left edge." |
| 756 | - [i m] | |
| 757 | - (let [;; What a jump landed on wears a surface of its own for a moment, so | |
| 756 | + [i m jump highlight] | |
| 757 | + (let [;; What this line is called, and what a jump is aiming at and landed | |
| 758 | + ;; on. Handed down rather than asked for here — `message-rows` says | |
| 759 | + ;; why this pair is not two cells like everything else on this row. | |
| 760 | + ;; | |
| 761 | + ;; What a jump landed on wears a surface of its own for a moment, so | |
| 758 | 762 | ;; the answer to "which one was I sent to" is on the screen rather |
| 759 | 763 | ;; than in the reader's count of rows. |
| 760 | - ;; Through `derived`, as is everything below that asks about shared | |
| 761 | - ;; state: a row that read the highlight itself re-rendered for every | |
| 762 | - ;; jump anywhere in the backlog. | |
| 763 | - highlit? (boolean (and (:id m) | |
| 764 | - (cells/derived-value [:highlit (:id m)] | |
| 765 | - #(= (:id m) @cells/highlight))))] | |
| 764 | + rid (rooms/row-id m) | |
| 765 | + highlit? (boolean (and rid (= rid highlight)))] | |
| 766 | 766 | [:vbox {:key i :spacing 2 :margin 0 |
| 767 | 767 | ;; Clear of the right edge: the actions ride that edge, and the |
| 768 | 768 | ;; list's scrollbar rides it too — without this the ↩ is what the |
| @@ -773,9 +773,10 @@ | ||
| 773 | 773 | ;; closed-up line to belong to. |
| 774 | 774 | :margin-top 10 |
| 775 | 775 | ;; The jump target is what a "go to message" click scrolls to. |
| 776 | - :scroll-here (boolean (and (:id m) | |
| 777 | - (cells/derived-value [:jump (:id m)] | |
| 778 | - #(= (:id m) @cells/jump-to))))} | |
| 776 | + ;; By `row-id`, not by `:id`: a line the server never named is | |
| 777 | + ;; still a line on the screen, and a jump aimed at it has to be | |
| 778 | + ;; able to say which one it means. | |
| 779 | + :scroll-here (boolean (and rid (= rid jump)))} | |
| 779 | 780 | ;; The gap above, where the margin cannot be one. `:margin-top 10` is |
| 780 | 781 | ;; most of a row in a window and nothing at all in a terminal — ten points |
| 781 | 782 | ;; against a row of sixteen, rounded down, because a gap that thin is what |
| @@ -805,7 +806,24 @@ | ||
| 805 | 806 | A backlog can reach back weeks, and `11:04 AM` says nothing about which day |
| 806 | 807 | it was. The heading is what makes the time above it mean something." |
| 807 | 808 | [messages] |
| 808 | - (mapcat (fn [i m] | |
| 809 | + ;; What a jump is aiming at and what it landed on, read here — in a render — | |
| 810 | + ;; and handed down as two plain values. | |
| 811 | + ;; | |
| 812 | + ;; They were a cell per row, so that a jump woke the two rows it moved | |
| 813 | + ;; between rather than the whole backlog, which is what `derived-value` is | |
| 814 | + ;; for and what it still does for a hover. But a row's cell was deref'd | |
| 815 | + ;; while this seq was realised, which is neither the row's render nor this | |
| 816 | + ;; one, so the read was recorded against nobody: `jump-to` changed, the cells | |
| 817 | + ;; changed, and no component was woken to put the new answer in the tree. The | |
| 818 | + ;; prop reached the window on the render that opened the room and never | |
| 819 | + ;; again — so a jump that needed a second frame, which is every jump into a | |
| 820 | + ;; room that has to be built first, never got one. | |
| 821 | + ;; | |
| 822 | + ;; A jump is a click. Re-rendering a backlog on one is affordable in a way | |
| 823 | + ;; that being wrong about it is not. | |
| 824 | + (let [jump @cells/jump-to | |
| 825 | + highlight @cells/highlight] | |
| 826 | + (mapcat (fn [i m] | |
| 809 | 827 | (let [prev (when (pos? i) (nth messages (dec i))) |
| 810 | 828 | day (some-> (:at m) clock/day) |
| 811 | 829 | new-day? (and day (not= day (some-> (:at prev) clock/day)))] |
| @@ -818,10 +836,10 @@ | ||
| 818 | 836 | ;; and a line that arrives anywhere but the end — a backlog |
| 819 | 837 | ;; replay, an echo taking the place of what was sent — shifts |
| 820 | 838 | ;; every row after it onto the widgets of its neighbour. |
| 821 | - true (conj ^{:key (or (:id m) (str "row-" i))} | |
| 822 | - [message-row i m])))) | |
| 823 | - (range (count messages)) | |
| 824 | - messages)) | |
| 839 | + true (conj ^{:key (or (rooms/row-id m) (str "row-" i))} | |
| 840 | + [message-row i m jump highlight])))) | |
| 841 | + (range (count messages)) | |
| 842 | + messages))) | |
| 825 | 843 | |
| 826 | 844 | (defn- messages-scroll-key |
| 827 | 845 | "What the backlog's scroll position is remembered under. |
| @@ -894,9 +912,8 @@ | ||
| 894 | 912 | ;; about, and a jump that landed on the newest message instead would answer |
| 895 | 913 | ;; a question nobody asked from a strip that was showing the answer. |
| 896 | 914 | ;; |
| 897 | - ;; Unless the line has no id to aim at — one this client has sent and the | |
| 898 | - ;; server has not echoed back yet — and then the room is all there is to | |
| 899 | - ;; offer. | |
| 915 | + ;; Every line has a name to aim at — the server's where it gave one, and | |
| 916 | + ;; `rooms/row-id`'s where it did not — so every row here goes somewhere. | |
| 900 | 917 | ;; |
| 901 | 918 | ;; 600ms to land and five seconds marked: the room is a room away, so the |
| 902 | 919 | ;; screen it scrolls in has to be built before there is anything to scroll, |
| @@ -905,11 +922,8 @@ | ||
| 905 | 922 | ;; I pressed", and it has to still be there when they have finished |
| 906 | 923 | ;; recognising where they are. |
| 907 | 924 | [:button {:label (:channel m) |
| 908 | - :on-click (if (:id m) | |
| 909 | - #(do (actions/leaving-for-overview!) | |
| 910 | - (goto-message! (:channel m) (:id m) 600 5000)) | |
| 911 | - #(do (actions/leaving-for-overview!) | |
| 912 | - (actions/open-channel! (:channel m))))}] | |
| 925 | + :on-click #(do (actions/leaving-for-overview!) | |
| 926 | + (goto-message! (:channel m) (rooms/row-id m) 600 5000))}] | |
| 913 | 927 | ;; And the line itself, as something to read rather than to press. It was a |
| 914 | 928 | ;; link for a moment, which made the strip two things at once: a line the |
| 915 | 929 | ;; server has echoed back has an id to aim at and a line this client has |
| @@ -753,16 +753,16 @@ | |||
| 753 | Sender above the text, not beside it: a wrapping label in a horizontal row | 753 | Sender above the text, not beside it: a wrapping label in a horizontal row |
| 754 | lays out against the row's width rather than the column's, so one long URL | 754 | lays out against the row's width rather than the column's, so one long URL |
| 755 | drags every line that follows it off the left edge." | 755 | drags every line that follows it off the left edge." |
| 756 | - [i m] | 756 | + [i m jump highlight] |
| 757 | - (let [;; What a jump landed on wears a surface of its own for a moment, so | 757 | + (let [;; What this line is called, and what a jump is aiming at and landed |
| 758 | + ;; on. Handed down rather than asked for here — `message-rows` says | ||
| 759 | + ;; why this pair is not two cells like everything else on this row. | ||
| 760 | + ;; | ||
| 761 | + ;; What a jump landed on wears a surface of its own for a moment, so | ||
| 758 | ;; the answer to "which one was I sent to" is on the screen rather | 762 | ;; the answer to "which one was I sent to" is on the screen rather |
| 759 | ;; than in the reader's count of rows. | 763 | ;; than in the reader's count of rows. |
| 760 | - ;; Through `derived`, as is everything below that asks about shared | 764 | + rid (rooms/row-id m) |
| 761 | - ;; state: a row that read the highlight itself re-rendered for every | 765 | + highlit? (boolean (and rid (= rid highlight)))] |
| 762 | - ;; jump anywhere in the backlog. | ||
| 763 | - highlit? (boolean (and (:id m) | ||
| 764 | - (cells/derived-value [:highlit (:id m)] | ||
| 765 | - #(= (:id m) @cells/highlight))))] | ||
| 766 | [:vbox {:key i :spacing 2 :margin 0 | 766 | [:vbox {:key i :spacing 2 :margin 0 |
| 767 | ;; Clear of the right edge: the actions ride that edge, and the | 767 | ;; Clear of the right edge: the actions ride that edge, and the |
| 768 | ;; list's scrollbar rides it too — without this the ↩ is what the | 768 | ;; list's scrollbar rides it too — without this the ↩ is what the |
| @@ -773,9 +773,10 @@ | |||
| 773 | ;; closed-up line to belong to. | 773 | ;; closed-up line to belong to. |
| 774 | :margin-top 10 | 774 | :margin-top 10 |
| 775 | ;; The jump target is what a "go to message" click scrolls to. | 775 | ;; The jump target is what a "go to message" click scrolls to. |
| 776 | - :scroll-here (boolean (and (:id m) | 776 | + ;; By `row-id`, not by `:id`: a line the server never named is |
| 777 | - (cells/derived-value [:jump (:id m)] | 777 | + ;; still a line on the screen, and a jump aimed at it has to be |
| 778 | - #(= (:id m) @cells/jump-to))))} | 778 | + ;; able to say which one it means. |
| 779 | + :scroll-here (boolean (and rid (= rid jump)))} | ||
| 779 | ;; The gap above, where the margin cannot be one. `:margin-top 10` is | 780 | ;; The gap above, where the margin cannot be one. `:margin-top 10` is |
| 780 | ;; most of a row in a window and nothing at all in a terminal — ten points | 781 | ;; most of a row in a window and nothing at all in a terminal — ten points |
| 781 | ;; against a row of sixteen, rounded down, because a gap that thin is what | 782 | ;; against a row of sixteen, rounded down, because a gap that thin is what |
| @@ -805,7 +806,24 @@ | |||
| 805 | A backlog can reach back weeks, and `11:04 AM` says nothing about which day | 806 | A backlog can reach back weeks, and `11:04 AM` says nothing about which day |
| 806 | it was. The heading is what makes the time above it mean something." | 807 | it was. The heading is what makes the time above it mean something." |
| 807 | [messages] | 808 | [messages] |
| 808 | - (mapcat (fn [i m] | 809 | + ;; What a jump is aiming at and what it landed on, read here — in a render — |
| 810 | + ;; and handed down as two plain values. | ||
| 811 | + ;; | ||
| 812 | + ;; They were a cell per row, so that a jump woke the two rows it moved | ||
| 813 | + ;; between rather than the whole backlog, which is what `derived-value` is | ||
| 814 | + ;; for and what it still does for a hover. But a row's cell was deref'd | ||
| 815 | + ;; while this seq was realised, which is neither the row's render nor this | ||
| 816 | + ;; one, so the read was recorded against nobody: `jump-to` changed, the cells | ||
| 817 | + ;; changed, and no component was woken to put the new answer in the tree. The | ||
| 818 | + ;; prop reached the window on the render that opened the room and never | ||
| 819 | + ;; again — so a jump that needed a second frame, which is every jump into a | ||
| 820 | + ;; room that has to be built first, never got one. | ||
| 821 | + ;; | ||
| 822 | + ;; A jump is a click. Re-rendering a backlog on one is affordable in a way | ||
| 823 | + ;; that being wrong about it is not. | ||
| 824 | + (let [jump @cells/jump-to | ||
| 825 | + highlight @cells/highlight] | ||
| 826 | + (mapcat (fn [i m] | ||
| 809 | (let [prev (when (pos? i) (nth messages (dec i))) | 827 | (let [prev (when (pos? i) (nth messages (dec i))) |
| 810 | day (some-> (:at m) clock/day) | 828 | day (some-> (:at m) clock/day) |
| 811 | new-day? (and day (not= day (some-> (:at prev) clock/day)))] | 829 | new-day? (and day (not= day (some-> (:at prev) clock/day)))] |
| @@ -818,10 +836,10 @@ | |||
| 818 | ;; and a line that arrives anywhere but the end — a backlog | 836 | ;; and a line that arrives anywhere but the end — a backlog |
| 819 | ;; replay, an echo taking the place of what was sent — shifts | 837 | ;; replay, an echo taking the place of what was sent — shifts |
| 820 | ;; every row after it onto the widgets of its neighbour. | 838 | ;; every row after it onto the widgets of its neighbour. |
| 821 | - true (conj ^{:key (or (:id m) (str "row-" i))} | 839 | + true (conj ^{:key (or (rooms/row-id m) (str "row-" i))} |
| 822 | - [message-row i m])))) | 840 | + [message-row i m jump highlight])))) |
| 823 | - (range (count messages)) | 841 | + (range (count messages)) |
| 824 | - messages)) | 842 | + messages))) |
| 825 | 843 | ||
| 826 | (defn- messages-scroll-key | 844 | (defn- messages-scroll-key |
| 827 | "What the backlog's scroll position is remembered under. | 845 | "What the backlog's scroll position is remembered under. |
| @@ -894,9 +912,8 @@ | |||
| 894 | ;; about, and a jump that landed on the newest message instead would answer | 912 | ;; about, and a jump that landed on the newest message instead would answer |
| 895 | ;; a question nobody asked from a strip that was showing the answer. | 913 | ;; a question nobody asked from a strip that was showing the answer. |
| 896 | ;; | 914 | ;; |
| 897 | - ;; Unless the line has no id to aim at — one this client has sent and the | 915 | + ;; Every line has a name to aim at — the server's where it gave one, and |
| 898 | - ;; server has not echoed back yet — and then the room is all there is to | 916 | + ;; `rooms/row-id`'s where it did not — so every row here goes somewhere. |
| 899 | - ;; offer. | ||
| 900 | ;; | 917 | ;; |
| 901 | ;; 600ms to land and five seconds marked: the room is a room away, so the | 918 | ;; 600ms to land and five seconds marked: the room is a room away, so the |
| 902 | ;; screen it scrolls in has to be built before there is anything to scroll, | 919 | ;; screen it scrolls in has to be built before there is anything to scroll, |
| @@ -905,11 +922,8 @@ | |||
| 905 | ;; I pressed", and it has to still be there when they have finished | 922 | ;; I pressed", and it has to still be there when they have finished |
| 906 | ;; recognising where they are. | 923 | ;; recognising where they are. |
| 907 | [:button {:label (:channel m) | 924 | [:button {:label (:channel m) |
| 908 | - :on-click (if (:id m) | 925 | + :on-click #(do (actions/leaving-for-overview!) |
| 909 | - #(do (actions/leaving-for-overview!) | 926 | + (goto-message! (:channel m) (rooms/row-id m) 600 5000))}] |
| 910 | - (goto-message! (:channel m) (:id m) 600 5000)) | ||
| 911 | - #(do (actions/leaving-for-overview!) | ||
| 912 | - (actions/open-channel! (:channel m))))}] | ||
| 913 | ;; And the line itself, as something to read rather than to press. It was a | 927 | ;; And the line itself, as something to read rather than to press. It was a |
| 914 | ;; link for a moment, which made the strip two things at once: a line the | 928 | ;; link for a moment, which made the strip two things at once: a line the |
| 915 | ;; server has echoed back has an id to aim at and a line this client has | 929 | ;; server has echoed back has an id to aim at and a line this client has |
modified
src/frq/state.clj +28 -3 | @@ -344,6 +344,26 @@ | ||
| 344 | 344 | chat view re-renders without every message row watching the media cache."} |
| 345 | 345 | media-tick (atom 0)) |
| 346 | 346 | |
| 347 | +(defn local-id | |
| 348 | + "A name for a line the server did not name. | |
| 349 | + | |
| 350 | + freeq tags a message with a `msgid` and that is a line's identity everywhere | |
| 351 | + it matters — a reply points at one, an edit rewrites one, a reaction lands on | |
| 352 | + one. But not every line arrives with one: a replayed backlog can come over | |
| 353 | + with no tags at all, and a line this client has just sent has none until the | |
| 354 | + server echoes it back. | |
| 355 | + | |
| 356 | + Those lines are not nameless to the reader, though. They are on the screen, | |
| 357 | + they are in the overview, and pressing one should go to it. So they get a | |
| 358 | + name made out of what they are: who said it, what it said, when, and where. | |
| 359 | + Two lines identical in all four are the same line as far as anything this | |
| 360 | + client does with one is concerned. | |
| 361 | + | |
| 362 | + `local-` because it is this client's alone, and it is never sent: the | |
| 363 | + server knows only the names it gave out." | |
| 364 | + [channel from text at] | |
| 365 | + (str "local-" (hash [channel from text at]))) | |
| 366 | + | |
| 347 | 367 | (defn push-message! |
| 348 | 368 | "Append a line to a buffer, creating it if needed, and bump the unread count |
| 349 | 369 | unless that buffer is the one on screen. Any image it links to is fetched in |
| @@ -358,6 +378,8 @@ | ||
| 358 | 378 | ([channel from text] (push-message! channel from text {})) |
| 359 | 379 | ([channel from text {:keys [at did id reply-to reactions edited?]}] |
| 360 | 380 | (let [at (or at (clock/now-ms)) |
| 381 | + ;; A name of our own where the server gave none. See `local-id`. | |
| 382 | + mine (when-not id (local-id channel from text at)) | |
| 361 | 383 | who (avatars/actor did from) |
| 362 | 384 | ;; A room reaching the store matters more than the throttle does: a |
| 363 | 385 | ;; connection joins every channel at once, and the writes for all but |
| @@ -430,7 +452,7 @@ | ||
| 430 | 452 | :at at |
| 431 | 453 | ;; `:id` is what a reply points at, and |
| 432 | 454 | ;; `:reply-to` is what this one points at. |
| 433 | - :id id :reply-to reply-to | |
| 455 | + :id id :local-id mine :reply-to reply-to | |
| 434 | 456 | ;; The sender has since rewritten this line. |
| 435 | 457 | ;; Replay says so with a tag rather than by |
| 436 | 458 | ;; sending the revision, so a message can |
| @@ -1851,10 +1873,13 @@ | ||
| 1851 | 1873 | (count saved))) |
| 1852 | 1874 | |
| 1853 | 1875 | (defn message-by-id |
| 1854 | - "The message a reply points at, if this buffer still holds it." | |
| 1876 | + "The message a reply points at, if this buffer still holds it. | |
| 1877 | + | |
| 1878 | + By either name, since a jump may be aiming at a line the server never gave | |
| 1879 | + one to. See `local-id`." | |
| 1855 | 1880 | [channel id] |
| 1856 | 1881 | (when id |
| 1857 | - (first (filter #(= id (:id %)) (get-in @channels [channel :messages]))))) | |
| 1882 | + (first (filter #(= id (rooms/row-id %)) (get-in @channels [channel :messages]))))) | |
| 1858 | 1883 | |
| 1859 | 1884 | (defn react-from-picker! |
| 1860 | 1885 | "Put the chosen emoji on the message the picker was opened for, and close it. |
| @@ -344,6 +344,26 @@ | |||
| 344 | chat view re-renders without every message row watching the media cache."} | 344 | chat view re-renders without every message row watching the media cache."} |
| 345 | media-tick (atom 0)) | 345 | media-tick (atom 0)) |
| 346 | 346 | ||
| 347 | +(defn local-id | ||
| 348 | + "A name for a line the server did not name. | ||
| 349 | + | ||
| 350 | + freeq tags a message with a `msgid` and that is a line's identity everywhere | ||
| 351 | + it matters — a reply points at one, an edit rewrites one, a reaction lands on | ||
| 352 | + one. But not every line arrives with one: a replayed backlog can come over | ||
| 353 | + with no tags at all, and a line this client has just sent has none until the | ||
| 354 | + server echoes it back. | ||
| 355 | + | ||
| 356 | + Those lines are not nameless to the reader, though. They are on the screen, | ||
| 357 | + they are in the overview, and pressing one should go to it. So they get a | ||
| 358 | + name made out of what they are: who said it, what it said, when, and where. | ||
| 359 | + Two lines identical in all four are the same line as far as anything this | ||
| 360 | + client does with one is concerned. | ||
| 361 | + | ||
| 362 | + `local-` because it is this client's alone, and it is never sent: the | ||
| 363 | + server knows only the names it gave out." | ||
| 364 | + [channel from text at] | ||
| 365 | + (str "local-" (hash [channel from text at]))) | ||
| 366 | + | ||
| 347 | (defn push-message! | 367 | (defn push-message! |
| 348 | "Append a line to a buffer, creating it if needed, and bump the unread count | 368 | "Append a line to a buffer, creating it if needed, and bump the unread count |
| 349 | unless that buffer is the one on screen. Any image it links to is fetched in | 369 | unless that buffer is the one on screen. Any image it links to is fetched in |
| @@ -358,6 +378,8 @@ | |||
| 358 | ([channel from text] (push-message! channel from text {})) | 378 | ([channel from text] (push-message! channel from text {})) |
| 359 | ([channel from text {:keys [at did id reply-to reactions edited?]}] | 379 | ([channel from text {:keys [at did id reply-to reactions edited?]}] |
| 360 | (let [at (or at (clock/now-ms)) | 380 | (let [at (or at (clock/now-ms)) |
| 381 | + ;; A name of our own where the server gave none. See `local-id`. | ||
| 382 | + mine (when-not id (local-id channel from text at)) | ||
| 361 | who (avatars/actor did from) | 383 | who (avatars/actor did from) |
| 362 | ;; A room reaching the store matters more than the throttle does: a | 384 | ;; A room reaching the store matters more than the throttle does: a |
| 363 | ;; connection joins every channel at once, and the writes for all but | 385 | ;; connection joins every channel at once, and the writes for all but |
| @@ -430,7 +452,7 @@ | |||
| 430 | :at at | 452 | :at at |
| 431 | ;; `:id` is what a reply points at, and | 453 | ;; `:id` is what a reply points at, and |
| 432 | ;; `:reply-to` is what this one points at. | 454 | ;; `:reply-to` is what this one points at. |
| 433 | - :id id :reply-to reply-to | 455 | + :id id :local-id mine :reply-to reply-to |
| 434 | ;; The sender has since rewritten this line. | 456 | ;; The sender has since rewritten this line. |
| 435 | ;; Replay says so with a tag rather than by | 457 | ;; Replay says so with a tag rather than by |
| 436 | ;; sending the revision, so a message can | 458 | ;; sending the revision, so a message can |
| @@ -1851,10 +1873,13 @@ | |||
| 1851 | (count saved))) | 1873 | (count saved))) |
| 1852 | 1874 | ||
| 1853 | (defn message-by-id | 1875 | (defn message-by-id |
| 1854 | - "The message a reply points at, if this buffer still holds it." | 1876 | + "The message a reply points at, if this buffer still holds it. |
| 1877 | + | ||
| 1878 | + By either name, since a jump may be aiming at a line the server never gave | ||
| 1879 | + one to. See `local-id`." | ||
| 1855 | [channel id] | 1880 | [channel id] |
| 1856 | (when id | 1881 | (when id |
| 1857 | - (first (filter #(= id (:id %)) (get-in @channels [channel :messages]))))) | 1882 | + (first (filter #(= id (rooms/row-id %)) (get-in @channels [channel :messages]))))) |
| 1858 | 1883 | ||
| 1859 | (defn react-from-picker! | 1884 | (defn react-from-picker! |
| 1860 | "Put the chosen emoji on the message the picker was opened for, and close it. | 1885 | "Put the chosen emoji on the message the picker was opened for, and close it. |