Rewrite a line from the phone
The pencil, the banner and the wire form were already here; what was not was the half that makes a rewrite show. Without `echo-message` the server sends nothing back, so the edit went out and the line on screen stayed as it was said — and with the cap, an edit that added or dropped a picture link left the row drawing `:images` read off the old text. Both are `edit-message!` now, the fold the inbound side was already doing inline, with the links read again the way `frq.state/edit-message!` reads them. The three things Send could do wrong with an edit in the box, said out loud: an attachment is refused rather than dropped on the floor, an empty box is not a way to unsay a line, and no connection keeps the draft instead of falling through to PRIVMSG and posting the rewrite as a new message. And opening another room cancels the edit, because the next Send would otherwise rewrite a message nobody in that room can see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
479ec83 parent: f50db56 modified
flutter/src/frq/main.cljd +67 -16 | @@ -115,6 +115,20 @@ | ||
| 115 | 115 | (doseq [url urls] (media/fetch! url bump!)) |
| 116 | 116 | urls)) |
| 117 | 117 | |
| 118 | +(defn- edit-message! | |
| 119 | + "Rewrite a message in place, and say so. | |
| 120 | + | |
| 121 | + `frq.edits` is the fold and what it answers; the cell and the picture links | |
| 122 | + are this half's. The links are read again off the new text because a | |
| 123 | + revision can add one or take one away, and a row drawing the picture its | |
| 124 | + line no longer mentions is the shape that gave it away. The desktop's | |
| 125 | + `frq.state/edit-message!` is the same function over its own atom." | |
| 126 | + [room msgid from text] | |
| 127 | + (let [out (edits/apply-edit @cells/channels room msgid from text | |
| 128 | + #(assoc % :images (images-in text)))] | |
| 129 | + (reset! cells/channels (:channels out)) | |
| 130 | + (:result out))) | |
| 131 | + | |
| 118 | 132 | (def ^:private history-limit |
| 119 | 133 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." |
| 120 | 134 | 100) |
| @@ -205,17 +219,15 @@ | ||
| 205 | 219 | ;; that line's own id and never the revision's own wire msgid, which |
| 206 | 220 | ;; nothing else refers to. One older than the backlog we hold has |
| 207 | 221 | ;; nothing here to replace, and is shown as itself rather than lost. |
| 208 | - (let [out (edits/apply-edit @cells/channels name edit-of who text)] | |
| 209 | - (reset! cells/channels (:channels out)) | |
| 210 | - (when (= :absent (:result out)) | |
| 211 | - (swap! cells/channels update name | |
| 212 | - #(-> (merge {:name name :messages [] :unread 0} %) | |
| 213 | - (update :messages conj {:from who | |
| 214 | - :text text | |
| 215 | - :images (images-in text) | |
| 216 | - :did (:account m) | |
| 217 | - :id edit-of | |
| 218 | - :edited? true}))))) | |
| 222 | + (when (= :absent (edit-message! name edit-of who text)) | |
| 223 | + (swap! cells/channels update name | |
| 224 | + #(-> (merge {:name name :messages [] :unread 0} %) | |
| 225 | + (update :messages conj {:from who | |
| 226 | + :text text | |
| 227 | + :images (images-in text) | |
| 228 | + :did (:account m) | |
| 229 | + :id edit-of | |
| 230 | + :edited? true})))) | |
| 219 | 231 | (swap! cells/channels update name |
| 220 | 232 | #(-> (merge {:name name :messages [] :unread 0} %) |
| 221 | 233 | (update :messages conj |
| @@ -636,17 +648,47 @@ | ||
| 636 | 648 | room (str @cells/current) |
| 637 | 649 | edit @cells/editing] |
| 638 | 650 | (cond |
| 651 | + (empty? room) nil | |
| 652 | + | |
| 653 | + ;; A rewrite replaces what was said, and what was said is a line of | |
| 654 | + ;; text: there is no wire form for adding a picture to a message already | |
| 655 | + ;; sent, so the attachment is held back rather than silently dropped. | |
| 656 | + ;; `frq.state/send-draft!` refuses the same pair for the same reason. | |
| 657 | + (and edit @cells/attachment) | |
| 658 | + (reset! cells/error "Finish the edit before sending a picture.") | |
| 659 | + | |
| 660 | + ;; An edit of nothing is not a way to unsay a line: there is no delete | |
| 661 | + ;; on the wire here, and blanking the message is not what emptying the | |
| 662 | + ;; box asks for. Send waits for something to say instead. | |
| 663 | + (and edit (empty? (.trim text))) nil | |
| 664 | + | |
| 639 | 665 | ;; A rewrite replaces what was said. It carries no new msgid of its own |
| 640 | 666 | ;; — the server files it under the original's id — so nothing is added |
| 641 | - ;; here and the echo folds it in where the line already is. | |
| 642 | - (and edit (seq text) (seq room) @conn) | |
| 643 | - (do (net/send-line! | |
| 644 | - @conn | |
| 667 | + ;; here and the echo folds it in where the line already is. The room is | |
| 668 | + ;; the edit's own and not whichever one is open: they are the same room | |
| 669 | + ;; while the banner is up, because leaving cancels the edit, and naming | |
| 670 | + ;; the one being rewritten is what makes that true rather than assumed. | |
| 671 | + edit | |
| 672 | + (if-let [c @conn] | |
| 673 | + (let [room (str (:channel edit)) | |
| 674 | + text (.trim text)] | |
| 675 | + (net/send-line! | |
| 676 | + c | |
| 645 | 677 | (mutate/edit-line room (:id edit) text |
| 646 | 678 | (reactions/peer-did @cells/channels room |
| 647 | 679 | (str @cells/form-nick)))) |
| 680 | + ;; Same reason as a new message: the server's echo is the copy every | |
| 681 | + ;; other client sees, and folding this one in as well would rewrite | |
| 682 | + ;; the line twice. Without `echo-message` nothing comes back, so the | |
| 683 | + ;; rewrite has to be applied here or it never shows. | |
| 684 | + (when-not (handshake/acked? @caps "echo-message") | |
| 685 | + (edit-message! room (:id edit) (str @cells/form-nick) text)) | |
| 648 | 686 | (reset! cells/editing nil) |
| 649 | 687 | (reset! cells/draft "")) |
| 688 | + ;; The draft stays in the box, with the banner still over it: the | |
| 689 | + ;; rewrite is still what the reader wants to send once there is | |
| 690 | + ;; somewhere to send it. | |
| 691 | + (reset! cells/error "Not connected.")) | |
| 650 | 692 | |
| 651 | 693 | ;; A picture still on its way up holds the send rather than losing it: |
| 652 | 694 | ;; the line stays in the box, said so, and the reader presses send again |
| @@ -655,7 +697,7 @@ | ||
| 655 | 697 | (= :uploading (:status @cells/attachment)) |
| 656 | 698 | (reset! cells/error "The picture is still uploading.") |
| 657 | 699 | |
| 658 | - (and (or (seq text) (:url @cells/attachment)) (seq room) @conn) | |
| 700 | + (and (or (seq text) (:url @cells/attachment)) @conn) | |
| 659 | 701 | (let [url (:url @cells/attachment) |
| 660 | 702 | ;; The picture becomes its link, at the end of the line: what goes |
| 661 | 703 | ;; on the wire is the text the reader wrote and a URL after it, |
| @@ -913,6 +955,15 @@ | ||
| 913 | 955 | :jump-to-present! (fn [] (reset! cells/at-present? true)) |
| 914 | 956 | :open-channel! (fn [name] |
| 915 | 957 | (reset! cells/current name) |
| 958 | + ;; An edit belongs to a line in the buffer being left: | |
| 959 | + ;; carried across, the next Send would rewrite a | |
| 960 | + ;; message nobody in this room can see. The box empties | |
| 961 | + ;; with it, because what is in it is a copy of that | |
| 962 | + ;; line — `frq.state/open-channel!` does this too. | |
| 963 | + (when (and @cells/editing | |
| 964 | + (not= name (:channel @cells/editing))) | |
| 965 | + (reset! cells/editing nil) | |
| 966 | + (reset! cells/draft "")) | |
| 916 | 967 | ;; `frq.state/open-channel!` does this and more: it |
| 917 | 968 | ;; marks the buffer read, remembers the room list and |
| 918 | 969 | ;; asks for NAMES. What it also does, and what made |
| @@ -115,6 +115,20 @@ | |||
| 115 | (doseq [url urls] (media/fetch! url bump!)) | 115 | (doseq [url urls] (media/fetch! url bump!)) |
| 116 | urls)) | 116 | urls)) |
| 117 | 117 | ||
| 118 | +(defn- edit-message! | ||
| 119 | + "Rewrite a message in place, and say so. | ||
| 120 | + | ||
| 121 | + `frq.edits` is the fold and what it answers; the cell and the picture links | ||
| 122 | + are this half's. The links are read again off the new text because a | ||
| 123 | + revision can add one or take one away, and a row drawing the picture its | ||
| 124 | + line no longer mentions is the shape that gave it away. The desktop's | ||
| 125 | + `frq.state/edit-message!` is the same function over its own atom." | ||
| 126 | + [room msgid from text] | ||
| 127 | + (let [out (edits/apply-edit @cells/channels room msgid from text | ||
| 128 | + #(assoc % :images (images-in text)))] | ||
| 129 | + (reset! cells/channels (:channels out)) | ||
| 130 | + (:result out))) | ||
| 131 | + | ||
| 118 | (def ^:private history-limit | 132 | (def ^:private history-limit |
| 119 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." | 133 | "How many lines of backlog to ask a room for, as `frq.state` asks for them." |
| 120 | 100) | 134 | 100) |
| @@ -205,17 +219,15 @@ | |||
| 205 | ;; that line's own id and never the revision's own wire msgid, which | 219 | ;; that line's own id and never the revision's own wire msgid, which |
| 206 | ;; nothing else refers to. One older than the backlog we hold has | 220 | ;; nothing else refers to. One older than the backlog we hold has |
| 207 | ;; nothing here to replace, and is shown as itself rather than lost. | 221 | ;; nothing here to replace, and is shown as itself rather than lost. |
| 208 | - (let [out (edits/apply-edit @cells/channels name edit-of who text)] | 222 | + (when (= :absent (edit-message! name edit-of who text)) |
| 209 | - (reset! cells/channels (:channels out)) | 223 | + (swap! cells/channels update name |
| 210 | - (when (= :absent (:result out)) | 224 | + #(-> (merge {:name name :messages [] :unread 0} %) |
| 211 | - (swap! cells/channels update name | 225 | + (update :messages conj {:from who |
| 212 | - #(-> (merge {:name name :messages [] :unread 0} %) | 226 | + :text text |
| 213 | - (update :messages conj {:from who | 227 | + :images (images-in text) |
| 214 | - :text text | 228 | + :did (:account m) |
| 215 | - :images (images-in text) | 229 | + :id edit-of |
| 216 | - :did (:account m) | 230 | + :edited? true})))) |
| 217 | - :id edit-of | ||
| 218 | - :edited? true}))))) | ||
| 219 | (swap! cells/channels update name | 231 | (swap! cells/channels update name |
| 220 | #(-> (merge {:name name :messages [] :unread 0} %) | 232 | #(-> (merge {:name name :messages [] :unread 0} %) |
| 221 | (update :messages conj | 233 | (update :messages conj |
| @@ -636,17 +648,47 @@ | |||
| 636 | room (str @cells/current) | 648 | room (str @cells/current) |
| 637 | edit @cells/editing] | 649 | edit @cells/editing] |
| 638 | (cond | 650 | (cond |
| 651 | + (empty? room) nil | ||
| 652 | + | ||
| 653 | + ;; A rewrite replaces what was said, and what was said is a line of | ||
| 654 | + ;; text: there is no wire form for adding a picture to a message already | ||
| 655 | + ;; sent, so the attachment is held back rather than silently dropped. | ||
| 656 | + ;; `frq.state/send-draft!` refuses the same pair for the same reason. | ||
| 657 | + (and edit @cells/attachment) | ||
| 658 | + (reset! cells/error "Finish the edit before sending a picture.") | ||
| 659 | + | ||
| 660 | + ;; An edit of nothing is not a way to unsay a line: there is no delete | ||
| 661 | + ;; on the wire here, and blanking the message is not what emptying the | ||
| 662 | + ;; box asks for. Send waits for something to say instead. | ||
| 663 | + (and edit (empty? (.trim text))) nil | ||
| 664 | + | ||
| 639 | ;; A rewrite replaces what was said. It carries no new msgid of its own | 665 | ;; A rewrite replaces what was said. It carries no new msgid of its own |
| 640 | ;; — the server files it under the original's id — so nothing is added | 666 | ;; — the server files it under the original's id — so nothing is added |
| 641 | - ;; here and the echo folds it in where the line already is. | 667 | + ;; here and the echo folds it in where the line already is. The room is |
| 642 | - (and edit (seq text) (seq room) @conn) | 668 | + ;; the edit's own and not whichever one is open: they are the same room |
| 643 | - (do (net/send-line! | 669 | + ;; while the banner is up, because leaving cancels the edit, and naming |
| 644 | - @conn | 670 | + ;; the one being rewritten is what makes that true rather than assumed. |
| 671 | + edit | ||
| 672 | + (if-let [c @conn] | ||
| 673 | + (let [room (str (:channel edit)) | ||
| 674 | + text (.trim text)] | ||
| 675 | + (net/send-line! | ||
| 676 | + c | ||
| 645 | (mutate/edit-line room (:id edit) text | 677 | (mutate/edit-line room (:id edit) text |
| 646 | (reactions/peer-did @cells/channels room | 678 | (reactions/peer-did @cells/channels room |
| 647 | (str @cells/form-nick)))) | 679 | (str @cells/form-nick)))) |
| 680 | + ;; Same reason as a new message: the server's echo is the copy every | ||
| 681 | + ;; other client sees, and folding this one in as well would rewrite | ||
| 682 | + ;; the line twice. Without `echo-message` nothing comes back, so the | ||
| 683 | + ;; rewrite has to be applied here or it never shows. | ||
| 684 | + (when-not (handshake/acked? @caps "echo-message") | ||
| 685 | + (edit-message! room (:id edit) (str @cells/form-nick) text)) | ||
| 648 | (reset! cells/editing nil) | 686 | (reset! cells/editing nil) |
| 649 | (reset! cells/draft "")) | 687 | (reset! cells/draft "")) |
| 688 | + ;; The draft stays in the box, with the banner still over it: the | ||
| 689 | + ;; rewrite is still what the reader wants to send once there is | ||
| 690 | + ;; somewhere to send it. | ||
| 691 | + (reset! cells/error "Not connected.")) | ||
| 650 | 692 | ||
| 651 | ;; A picture still on its way up holds the send rather than losing it: | 693 | ;; A picture still on its way up holds the send rather than losing it: |
| 652 | ;; the line stays in the box, said so, and the reader presses send again | 694 | ;; the line stays in the box, said so, and the reader presses send again |
| @@ -655,7 +697,7 @@ | |||
| 655 | (= :uploading (:status @cells/attachment)) | 697 | (= :uploading (:status @cells/attachment)) |
| 656 | (reset! cells/error "The picture is still uploading.") | 698 | (reset! cells/error "The picture is still uploading.") |
| 657 | 699 | ||
| 658 | - (and (or (seq text) (:url @cells/attachment)) (seq room) @conn) | 700 | + (and (or (seq text) (:url @cells/attachment)) @conn) |
| 659 | (let [url (:url @cells/attachment) | 701 | (let [url (:url @cells/attachment) |
| 660 | ;; The picture becomes its link, at the end of the line: what goes | 702 | ;; The picture becomes its link, at the end of the line: what goes |
| 661 | ;; on the wire is the text the reader wrote and a URL after it, | 703 | ;; on the wire is the text the reader wrote and a URL after it, |
| @@ -913,6 +955,15 @@ | |||
| 913 | :jump-to-present! (fn [] (reset! cells/at-present? true)) | 955 | :jump-to-present! (fn [] (reset! cells/at-present? true)) |
| 914 | :open-channel! (fn [name] | 956 | :open-channel! (fn [name] |
| 915 | (reset! cells/current name) | 957 | (reset! cells/current name) |
| 958 | + ;; An edit belongs to a line in the buffer being left: | ||
| 959 | + ;; carried across, the next Send would rewrite a | ||
| 960 | + ;; message nobody in this room can see. The box empties | ||
| 961 | + ;; with it, because what is in it is a copy of that | ||
| 962 | + ;; line — `frq.state/open-channel!` does this too. | ||
| 963 | + (when (and @cells/editing | ||
| 964 | + (not= name (:channel @cells/editing))) | ||
| 965 | + (reset! cells/editing nil) | ||
| 966 | + (reset! cells/draft "")) | ||
| 916 | ;; `frq.state/open-channel!` does this and more: it | 967 | ;; `frq.state/open-channel!` does this and more: it |
| 917 | ;; marks the buffer read, remembers the room list and | 968 | ;; marks the buffer read, remembers the room list and |
| 918 | ;; asks for NAMES. What it also does, and what made | 969 | ;; asks for NAMES. What it also does, and what made |