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

Rewrite a line from the phone

A message keeps the id it was born with across every revision — that is
what keeps its reactions, replies and pins attached to it — so an edit is
not a new line, it replaces the one it names in place. `frq.edits` is that
fold, pure over the channels map and answering what became of it: applied,
refused for one that was not the sender's to make, or absent for an edit of
something older than the backlog we hold, which is the one case shown as a
line of its own rather than lost.

Authorship is checked here and not only at the server, unchanged from what
the desktop did: a client that believed the wire alone would let a hostile
relay put words in somebody else's mouth.

The picture links stayed behind. `frq.media` is not portable, so the fold
takes a function to decorate the rewritten message and the desktop passes
one that re-reads the links out of the new text — rather than dragging
media into common/ for one line of it.

The pencil had never appeared on the phone at all, and not because of the
chip: `mine?` is asked about a message and the phone had installed
something that took a nick, so every line answered false. It is
`frq.rooms/mine?` now, nick against nick and case-insensitively, which is
what the server falls back to for an account with no DID — an edit it would
refuse is one not worth offering.

Verified on a Pixel 6a signed in: the pencil shows on our own lines and not
on anyone else's, the box opens on the old text, and sending replaces the
line where it stands and marks it edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-12T01:57:46-07:00 Browse files
dcb50e0 parent: 0ee5786
added common/frq/edits.cljc +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+(ns frq.edits
2+ "Rewriting a line that is already said.
3+
4+ A message keeps the id it was born with across every revision, which is what
5+ keeps its reactions, replies and pins attached to it. So an edit is not a new
6+ line: it replaces the one it names, in place, under that line's own id.
7+
8+ Pure over the channels map, like `frq.members` and `frq.reactions`, and
9+ shared for the same reason — who may rewrite what is the server's rule and
10+ neither half of frq gets a say in it."
11+ (:require [clojure.string :as str]))
12+
13+(defn apply-edit
14+ "The revision folded into the buffer it belongs to.
15+
16+ Answers `{:channels :result}`, where the result says what became of it:
17+ `:applied`; `:refused` for one that was not the sender's to make; or
18+ `:absent` when no line here has that id — an edit of something older than the
19+ backlog we asked for, which is the one case the caller shows as a line of its
20+ own rather than losing what it says.
21+
22+ Only the sender may rewrite their own line, so an edit whose nick is not the
23+ one on the message is dropped. The server checks authorship too, and a client
24+ that believed the wire alone would let a hostile relay put words in somebody
25+ else's mouth.
26+
27+ `decorate` is applied to the rewritten message, for whatever the caller
28+ derives from the text it now carries — the desktop re-reads the picture links
29+ out of it there. `frq.media` is not portable and this does not need it to be."
30+ ([channels channel msgid from text]
31+ (apply-edit channels channel msgid from text identity))
32+ ([channels channel msgid from text decorate]
33+ (if-not (and channel msgid)
34+ {:channels channels :result :absent}
35+ (let [msgs (get-in channels [channel :messages])]
36+ (if-not msgs
37+ {:channels channels :result :absent}
38+ (let [result (volatile! :absent)
39+ msgs' (mapv (fn [msg]
40+ (if (= msgid (:id msg))
41+ (if (= (str/lower-case (or (:from msg) ""))
42+ (str/lower-case (or from "")))
43+ (do (vreset! result :applied)
44+ (decorate (assoc msg
45+ :text text
46+ :edited? true)))
47+ (do (vreset! result :refused) msg))
48+ msg))
49+ msgs)]
50+ {:channels (assoc-in channels [channel :messages] msgs')
51+ :result @result}))))))
new file mode 100644
@@ -0,0 +1,51 @@
1+(ns frq.edits
2+ "Rewriting a line that is already said.
3+
4+ A message keeps the id it was born with across every revision, which is what
5+ keeps its reactions, replies and pins attached to it. So an edit is not a new
6+ line: it replaces the one it names, in place, under that line's own id.
7+
8+ Pure over the channels map, like `frq.members` and `frq.reactions`, and
9+ shared for the same reason — who may rewrite what is the server's rule and
10+ neither half of frq gets a say in it."
11+ (:require [clojure.string :as str]))
12+
13+(defn apply-edit
14+ "The revision folded into the buffer it belongs to.
15+
16+ Answers `{:channels :result}`, where the result says what became of it:
17+ `:applied`; `:refused` for one that was not the sender's to make; or
18+ `:absent` when no line here has that id — an edit of something older than the
19+ backlog we asked for, which is the one case the caller shows as a line of its
20+ own rather than losing what it says.
21+
22+ Only the sender may rewrite their own line, so an edit whose nick is not the
23+ one on the message is dropped. The server checks authorship too, and a client
24+ that believed the wire alone would let a hostile relay put words in somebody
25+ else's mouth.
26+
27+ `decorate` is applied to the rewritten message, for whatever the caller
28+ derives from the text it now carries — the desktop re-reads the picture links
29+ out of it there. `frq.media` is not portable and this does not need it to be."
30+ ([channels channel msgid from text]
31+ (apply-edit channels channel msgid from text identity))
32+ ([channels channel msgid from text decorate]
33+ (if-not (and channel msgid)
34+ {:channels channels :result :absent}
35+ (let [msgs (get-in channels [channel :messages])]
36+ (if-not msgs
37+ {:channels channels :result :absent}
38+ (let [result (volatile! :absent)
39+ msgs' (mapv (fn [msg]
40+ (if (= msgid (:id msg))
41+ (if (= (str/lower-case (or (:from msg) ""))
42+ (str/lower-case (or from "")))
43+ (do (vreset! result :applied)
44+ (decorate (assoc msg
45+ :text text
46+ :edited? true)))
47+ (do (vreset! result :refused) msg))
48+ msg))
49+ msgs)]
50+ {:channels (assoc-in channels [channel :messages] msgs')
51+ :result @result}))))))
modified common/frq/irc/mutate.cljc +16 -0
@@ -49,3 +49,19 @@
4949 (tag-line (merge {"+freeq.at/unreact" emoji "+reply" msgid}
5050 (msgsig/mutation-tags "unreact" target msgid emoji peer-did))
5151 (str "TAGMSG " target))))
52+
53+
54+(defn edit-line
55+ "Rewrite something already said.
56+
57+ The `+draft/edit` tag names the message being replaced and what follows is
58+ its new text — the server checks that the message was ours, files the
59+ revision under the original's id, and sends the new line on to the channel
60+ for every client to fold in.
61+
62+ A PRIVMSG rather than a TAGMSG, because an edit carries a body."
63+ ([target msgid text] (edit-line target msgid text nil))
64+ ([target msgid text peer-did]
65+ (tag-line (assoc (msgsig/edit-tags target msgid text nil peer-did)
66+ "+draft/edit" msgid)
67+ (str "PRIVMSG " target " :" text))))
@@ -49,3 +49,19 @@
49 (tag-line (merge {"+freeq.at/unreact" emoji "+reply" msgid}49 (tag-line (merge {"+freeq.at/unreact" emoji "+reply" msgid}
50 (msgsig/mutation-tags "unreact" target msgid emoji peer-did))50 (msgsig/mutation-tags "unreact" target msgid emoji peer-did))
51 (str "TAGMSG " target))))51 (str "TAGMSG " target))))
52+
53+
54+(defn edit-line
55+ "Rewrite something already said.
56+
57+ The `+draft/edit` tag names the message being replaced and what follows is
58+ its new text — the server checks that the message was ours, files the
59+ revision under the original's id, and sends the new line on to the channel
60+ for every client to fold in.
61+
62+ A PRIVMSG rather than a TAGMSG, because an edit carries a body."
63+ ([target msgid text] (edit-line target msgid text nil))
64+ ([target msgid text peer-did]
65+ (tag-line (assoc (msgsig/edit-tags target msgid text nil peer-did)
66+ "+draft/edit" msgid)
67+ (str "PRIVMSG " target " :" text))))
modified common/frq/rooms.cljc +13 -0
@@ -53,3 +53,16 @@
5353 (str/includes? (str/lower-case (:name %)) q)))
5454 (sort-by (juxt #(- (:accessed % 0)) :name))
5555 vec)))
56+
57+
58+(defn mine?
59+ "Whether we are the one who said this.
60+
61+ Nick against nick, which is what the server itself falls back to for an
62+ account with no DID — and an edit it would refuse is one not worth offering.
63+ A system line is nobody's to rewrite."
64+ [m me]
65+ (and (not (:system? m))
66+ (seq (or (:from m) ""))
67+ (= (str/lower-case (:from m))
68+ (str/lower-case (or me "")))))
@@ -53,3 +53,16 @@
53 (str/includes? (str/lower-case (:name %)) q)))53 (str/includes? (str/lower-case (:name %)) q)))
54 (sort-by (juxt #(- (:accessed % 0)) :name))54 (sort-by (juxt #(- (:accessed % 0)) :name))
55 vec)))55 vec)))
56+
57+
58+(defn mine?
59+ "Whether we are the one who said this.
60+
61+ Nick against nick, which is what the server itself falls back to for an
62+ account with no DID — and an edit it would refuse is one not worth offering.
63+ A system line is nobody's to rewrite."
64+ [m me]
65+ (and (not (:system? m))
66+ (seq (or (:from m) ""))
67+ (= (str/lower-case (:from m))
68+ (str/lower-case (or me "")))))
modified flutter/src/frq/main.cljd +60 -7
@@ -41,6 +41,7 @@
4141 [frq.rooms :as rooms]
4242 [frq.members :as members]
4343 [frq.reactions :as reactions]
44+ [frq.edits :as edits]
4445 [frq.irc.mutate :as mutate]
4546 [frq.oauth.core :as oauth]
4647 [frq.oauth.dart :as oauth-dart]
@@ -128,8 +129,25 @@
128129 (let [tags (:tags m)
129130 target (first params)
130131 text (last params)
131- name (if (rooms/dm? target) who target)]
132- (swap! cells/channels update name
132+ name (if (rooms/dm? target) who target)
133+ edit-of (or (irc/tag-value tags "+draft/edit")
134+ (irc/tag-value tags "+edit"))]
135+ (if edit-of
136+ ;; A revision is not a new line: it replaces the one it names, under
137+ ;; that line's own id and never the revision's own wire msgid, which
138+ ;; nothing else refers to. One older than the backlog we hold has
139+ ;; nothing here to replace, and is shown as itself rather than lost.
140+ (let [out (edits/apply-edit @cells/channels name edit-of who text)]
141+ (reset! cells/channels (:channels out))
142+ (when (= :absent (:result out))
143+ (swap! cells/channels update name
144+ #(-> (merge {:name name :messages [] :unread 0} %)
145+ (update :messages conj {:from who
146+ :text text
147+ :did (:account m)
148+ :id edit-of
149+ :edited? true})))))
150+ (swap! cells/channels update name
133151 #(-> (merge {:name name :messages [] :unread 0} %)
134152 (update :messages conj
135153 ;; The msgid is what everything after a message
@@ -147,9 +165,15 @@
147165 (irc/tag-value tags "+draft/reply"))
148166 ;; What is already on it, so a reconnect does not
149167 ;; start every message empty.
168+ ;; What the server says about a line it has
169+ ;; already collapsed: replay sends the current
170+ ;; text and no `+draft/edit` to hint that it is
171+ ;; not the original. This tag is the only trace.
172+ :edited? (= "1" (irc/tag-value tags
173+ "+freeq.at/edited"))
150174 :reactions (reactions/parse-tally
151175 (irc/tag-value tags
152- "+freeq.at/reactions"))}))))
176+ "+freeq.at/reactions"))})))))
153177
154178 ;; A message that is only tags. A reaction is the one this reads:
155179 ;; `+react` puts an emoji on the message `+reply` names, and the
@@ -470,8 +494,23 @@
470494
471495 (defn- send-draft! []
472496 (let [text (str @cells/draft)
473- room (str @cells/current)]
474- (when (and (seq text) (seq room) @conn)
497+ room (str @cells/current)
498+ edit @cells/editing]
499+ (cond
500+ ;; A rewrite replaces what was said. It carries no new msgid of its own
501+ ;; — the server files it under the original's id — so nothing is added
502+ ;; here and the echo folds it in where the line already is.
503+ (and edit (seq text) (seq room) @conn)
504+ (do (net/send-line!
505+ @conn
506+ (mutate/edit-line room (:id edit) text
507+ (reactions/peer-did @cells/channels room
508+ (str @cells/form-nick))))
509+ (reset! cells/editing nil)
510+ (reset! cells/draft ""))
511+
512+ (and (seq text) (seq room) @conn)
513+ (do
475514 (net/send-line! @conn (str "PRIVMSG " room " :" text))
476515 ;; Echoed locally only when the server will not echo it back. With
477516 ;; `echo-message` negotiated it does — that is what the cap is for, and
@@ -480,7 +519,7 @@
480519 (when-not (handshake/acked? @caps "echo-message")
481520 (swap! cells/channels update room
482521 #(update % :messages conj {:from @cells/form-nick :text text})))
483- (reset! cells/draft ""))))
522+ (reset! cells/draft "")))))
484523
485524 (defn- message-by-id [room id]
486525 (when id
@@ -585,7 +624,21 @@
585624 :unhover-reaction! (fn [_ _] nil)
586625 :wide? (fn [] false)
587626 :desktop? (fn [] false)
588- :mine? (fn [from] (= (str from) (str @cells/form-nick)))
627+ :mine? (fn [m] (rooms/mine? m (str @cells/form-nick)))
628+
629+ ;; Rewriting. The old text is the starting point rather than an empty
630+ ;; line: an edit is usually a word, and retyping the sentence around it
631+ ;; is not what was asked for.
632+ :start-edit! (fn [channel m]
633+ (when (and (:id m) (rooms/mine? m (str @cells/form-nick)))
634+ (reset! cells/replying-to nil)
635+ (reset! cells/editing {:channel channel :id (:id m)})
636+ (reset! cells/draft (or (:text m) ""))))
637+ ;; The box empties with it: what is in it is a copy of the line on
638+ ;; screen, and leaving that behind would look like a draft.
639+ :cancel-edit! (fn []
640+ (reset! cells/editing nil)
641+ (reset! cells/draft ""))
589642 :message-by-id (fn [room id]
590643 (->> (get-in @cells/channels [room :messages])
591644 (filter #(= id (:id %)))
@@ -41,6 +41,7 @@
41 [frq.rooms :as rooms]41 [frq.rooms :as rooms]
42 [frq.members :as members]42 [frq.members :as members]
43 [frq.reactions :as reactions]43 [frq.reactions :as reactions]
44+ [frq.edits :as edits]
44 [frq.irc.mutate :as mutate]45 [frq.irc.mutate :as mutate]
45 [frq.oauth.core :as oauth]46 [frq.oauth.core :as oauth]
46 [frq.oauth.dart :as oauth-dart]47 [frq.oauth.dart :as oauth-dart]
@@ -128,8 +129,25 @@
128 (let [tags (:tags m)129 (let [tags (:tags m)
129 target (first params)130 target (first params)
130 text (last params)131 text (last params)
131- name (if (rooms/dm? target) who target)]132+ name (if (rooms/dm? target) who target)
132- (swap! cells/channels update name133+ edit-of (or (irc/tag-value tags "+draft/edit")
134+ (irc/tag-value tags "+edit"))]
135+ (if edit-of
136+ ;; A revision is not a new line: it replaces the one it names, under
137+ ;; that line's own id and never the revision's own wire msgid, which
138+ ;; nothing else refers to. One older than the backlog we hold has
139+ ;; nothing here to replace, and is shown as itself rather than lost.
140+ (let [out (edits/apply-edit @cells/channels name edit-of who text)]
141+ (reset! cells/channels (:channels out))
142+ (when (= :absent (:result out))
143+ (swap! cells/channels update name
144+ #(-> (merge {:name name :messages [] :unread 0} %)
145+ (update :messages conj {:from who
146+ :text text
147+ :did (:account m)
148+ :id edit-of
149+ :edited? true})))))
150+ (swap! cells/channels update name
133 #(-> (merge {:name name :messages [] :unread 0} %)151 #(-> (merge {:name name :messages [] :unread 0} %)
134 (update :messages conj152 (update :messages conj
135 ;; The msgid is what everything after a message153 ;; The msgid is what everything after a message
@@ -147,9 +165,15 @@
147 (irc/tag-value tags "+draft/reply"))165 (irc/tag-value tags "+draft/reply"))
148 ;; What is already on it, so a reconnect does not166 ;; What is already on it, so a reconnect does not
149 ;; start every message empty.167 ;; start every message empty.
168+ ;; What the server says about a line it has
169+ ;; already collapsed: replay sends the current
170+ ;; text and no `+draft/edit` to hint that it is
171+ ;; not the original. This tag is the only trace.
172+ :edited? (= "1" (irc/tag-value tags
173+ "+freeq.at/edited"))
150 :reactions (reactions/parse-tally174 :reactions (reactions/parse-tally
151 (irc/tag-value tags175 (irc/tag-value tags
152- "+freeq.at/reactions"))}))))176+ "+freeq.at/reactions"))})))))
153 177
154 ;; A message that is only tags. A reaction is the one this reads:178 ;; A message that is only tags. A reaction is the one this reads:
155 ;; `+react` puts an emoji on the message `+reply` names, and the179 ;; `+react` puts an emoji on the message `+reply` names, and the
@@ -470,8 +494,23 @@
470 494
471 (defn- send-draft! []495 (defn- send-draft! []
472 (let [text (str @cells/draft)496 (let [text (str @cells/draft)
473- room (str @cells/current)]497+ room (str @cells/current)
474- (when (and (seq text) (seq room) @conn)498+ edit @cells/editing]
499+ (cond
500+ ;; A rewrite replaces what was said. It carries no new msgid of its own
501+ ;; — the server files it under the original's id — so nothing is added
502+ ;; here and the echo folds it in where the line already is.
503+ (and edit (seq text) (seq room) @conn)
504+ (do (net/send-line!
505+ @conn
506+ (mutate/edit-line room (:id edit) text
507+ (reactions/peer-did @cells/channels room
508+ (str @cells/form-nick))))
509+ (reset! cells/editing nil)
510+ (reset! cells/draft ""))
511+
512+ (and (seq text) (seq room) @conn)
513+ (do
475 (net/send-line! @conn (str "PRIVMSG " room " :" text))514 (net/send-line! @conn (str "PRIVMSG " room " :" text))
476 ;; Echoed locally only when the server will not echo it back. With515 ;; Echoed locally only when the server will not echo it back. With
477 ;; `echo-message` negotiated it does — that is what the cap is for, and516 ;; `echo-message` negotiated it does — that is what the cap is for, and
@@ -480,7 +519,7 @@
480 (when-not (handshake/acked? @caps "echo-message")519 (when-not (handshake/acked? @caps "echo-message")
481 (swap! cells/channels update room520 (swap! cells/channels update room
482 #(update % :messages conj {:from @cells/form-nick :text text})))521 #(update % :messages conj {:from @cells/form-nick :text text})))
483- (reset! cells/draft ""))))522+ (reset! cells/draft "")))))
484 523
485 (defn- message-by-id [room id]524 (defn- message-by-id [room id]
486 (when id525 (when id
@@ -585,7 +624,21 @@
585 :unhover-reaction! (fn [_ _] nil)624 :unhover-reaction! (fn [_ _] nil)
586 :wide? (fn [] false)625 :wide? (fn [] false)
587 :desktop? (fn [] false)626 :desktop? (fn [] false)
588- :mine? (fn [from] (= (str from) (str @cells/form-nick)))627+ :mine? (fn [m] (rooms/mine? m (str @cells/form-nick)))
628+
629+ ;; Rewriting. The old text is the starting point rather than an empty
630+ ;; line: an edit is usually a word, and retyping the sentence around it
631+ ;; is not what was asked for.
632+ :start-edit! (fn [channel m]
633+ (when (and (:id m) (rooms/mine? m (str @cells/form-nick)))
634+ (reset! cells/replying-to nil)
635+ (reset! cells/editing {:channel channel :id (:id m)})
636+ (reset! cells/draft (or (:text m) ""))))
637+ ;; The box empties with it: what is in it is a copy of the line on
638+ ;; screen, and leaving that behind would look like a draft.
639+ :cancel-edit! (fn []
640+ (reset! cells/editing nil)
641+ (reset! cells/draft ""))
589 :message-by-id (fn [room id]642 :message-by-id (fn [room id]
590 (->> (get-in @cells/channels [room :messages])643 (->> (get-in @cells/channels [room :messages])
591 (filter #(= id (:id %)))644 (filter #(= id (:id %)))
modified src/frq/irc.clj +3 -15
@@ -241,23 +241,11 @@
241241 "PRIVMSG " target " :" text))))
242242
243243 (defn edit!
244- "Rewrite something already said. The `+draft/edit` tag names the message
245- being replaced, and what follows is its new text — the server checks that
246- the message was ours, files the revision under the original's id, and sends
247- the new line on to the channel for every client to fold in.
248-
249- A PRIVMSG rather than a TAGMSG, because an edit carries a body — and signed,
250- like every other thing that changes a record already written: from an account
251- the server answers an unsigned one with
252- `FAIL EDIT SIGNATURE_REQUIRED` and the message stays as it was. `peer-did` is
253- who a DM is with, which is half of the name a DM signature is made under."
244+ "Rewrite something already said. The line is `frq.irc.mutate`'s; this writes
245+ it."
254246 ([conn target msgid text] (edit! conn target msgid text nil))
255247 ([conn target msgid text peer-did]
256- (let [tags (assoc (msgsig/edit-tags target msgid text nil peer-did)
257- "+draft/edit" msgid)
258- pairs (for [[k v] tags] (str k "=" (escape-tag-value v)))]
259- (send-line! conn (str "@" (str/join ";" pairs)
260- " PRIVMSG " target " :" text)))))
248+ (send-line! conn (mutate/edit-line target msgid text peer-did))))
261249
262250 (defn tagmsg!
263251 "A message that is only tags: how freeq carries a reaction, a typing hint or
@@ -241,23 +241,11 @@
241 "PRIVMSG " target " :" text))))241 "PRIVMSG " target " :" text))))
242 242
243 (defn edit!243 (defn edit!
244- "Rewrite something already said. The `+draft/edit` tag names the message244+ "Rewrite something already said. The line is `frq.irc.mutate`'s; this writes
245- being replaced, and what follows is its new text — the server checks that245+ it."
246- the message was ours, files the revision under the original's id, and sends
247- the new line on to the channel for every client to fold in.
248-
249- A PRIVMSG rather than a TAGMSG, because an edit carries a body — and signed,
250- like every other thing that changes a record already written: from an account
251- the server answers an unsigned one with
252- `FAIL EDIT SIGNATURE_REQUIRED` and the message stays as it was. `peer-did` is
253- who a DM is with, which is half of the name a DM signature is made under."
254 ([conn target msgid text] (edit! conn target msgid text nil))246 ([conn target msgid text] (edit! conn target msgid text nil))
255 ([conn target msgid text peer-did]247 ([conn target msgid text peer-did]
256- (let [tags (assoc (msgsig/edit-tags target msgid text nil peer-did)248+ (send-line! conn (mutate/edit-line target msgid text peer-did))))
257- "+draft/edit" msgid)
258- pairs (for [[k v] tags] (str k "=" (escape-tag-value v)))]
259- (send-line! conn (str "@" (str/join ";" pairs)
260- " PRIVMSG " target " :" text)))))
261 249
262 (defn tagmsg!250 (defn tagmsg!
263 "A message that is only tags: how freeq carries a reaction, a typing hint or251 "A message that is only tags: how freeq carries a reaction, a typing hint or
modified src/frq/state.clj +11 -54
@@ -8,6 +8,7 @@
88 [frq.rooms :as rooms]
99 [frq.members :as members]
1010 [frq.reactions :as reactions]
11+ [frq.edits :as edits]
1112 [glimmer.ratom :as r :refer [atom]]
1213 [frq.actions :as actions]
1314 [frq.cells :as cells]
@@ -564,57 +565,16 @@
564565 (swap! channels reactions/update-reaction channel msgid emoji nick on?))
565566
566567 (defn edit-message!
567- "Rewrite a message in place, and say so. `msgid` names the line as it was
568- first sent: a message keeps the id it was born with across every revision,
569- which is what keeps its reactions, replies and pins attached to it.
570-
571- Only the sender may rewrite their own line, so an edit whose nick is not the
572- one on the message is dropped — the server checks authorship too, and a
573- client that believed the wire alone would let a hostile relay put words in
574- somebody's mouth.
575-
576- Answers what became of it: `:applied`, `:refused` for one that was not the
577- sender's to make, or `:absent` when no line here has that id — an edit of
578- something older than the backlog we asked for, which is the one case the
579- caller shows as a line of its own rather than losing what it says."
568+ "Rewrite a message in place, and say so.
569+
570+ `frq.edits` is the fold and what it answers; the atom and the picture links
571+ are this half's. A message keeps the id it was born with across every
572+ revision, which is what keeps its reactions, replies and pins attached to it."
580573 [channel msgid from text]
581- (if-not (and channel msgid)
582- :absent
583- (let [found? (atom nil)]
584- (swap! channels
585- (fn [m]
586- (if-let [msgs (get-in m [channel :messages])]
587- (assoc-in m [channel :messages]
588- (mapv (fn [msg]
589- (if (= msgid (:id msg))
590- (if (= (str/lower-case (or (:from msg) ""))
591- (str/lower-case (or from "")))
592- (do (reset! found? :applied)
593- (assoc msg
594- :text text
595- :images (media/image-urls text)
596- :edited? true))
597- ;; Somebody else's line. The server
598- ;; refuses this too, so it is either a
599- ;; peer talking to us directly or one
600- ;; lying — and neither gets to put words
601- ;; under a name that is not theirs.
602- (do (reset! found? :refused) msg))
603- msg))
604- msgs))
605- m)))
606- ;; The revision may link a picture the original did not.
607- (when (= :applied @found?)
608- (doseq [url (media/image-urls text)]
609- (media/fetch! url #(swap! media-tick inc))))
610- (or @found? :absent))))
611-
612-;; --- who is in the room ------------------------------------------------------
613-;; All of it is `frq.members` now: a fold over the channels map, which is the
614-;; same fold under either compiler. What stays here is the atom it is folded
615-;; into and `ensure-channel`, because a room means more to this half than to
616-;; the phone — unread counts, read marks, a joining flag — and the shared
617-;; functions deliberately only ever touch `:users` and `:names-acc`.
574+ (let [out (edits/apply-edit @channels channel msgid from text
575+ #(assoc % :images (media/image-urls text)))]
576+ (reset! channels (:channels out))
577+ (:result out)))
618578
619579 (defn- names-line [channel names]
620580 (swap! channels #(members/with-names (ensure-channel % channel) channel names)))
@@ -1426,10 +1386,7 @@
14261386 server itself falls back to for an account with no DID and an edit it would
14271387 refuse is one not worth offering."
14281388 [m]
1429- (and (not (:system? m))
1430- (seq (or (:from m) ""))
1431- (= (str/lower-case (:from m))
1432- (str/lower-case (or @form-nick "")))))
1389+ (rooms/mine? m @form-nick))
14331390
14341391 (defn start-edit!
14351392 "Put a message back in the box to be rewritten.
@@ -8,6 +8,7 @@
8 [frq.rooms :as rooms]8 [frq.rooms :as rooms]
9 [frq.members :as members]9 [frq.members :as members]
10 [frq.reactions :as reactions]10 [frq.reactions :as reactions]
11+ [frq.edits :as edits]
11 [glimmer.ratom :as r :refer [atom]]12 [glimmer.ratom :as r :refer [atom]]
12 [frq.actions :as actions]13 [frq.actions :as actions]
13 [frq.cells :as cells]14 [frq.cells :as cells]
@@ -564,57 +565,16 @@
564 (swap! channels reactions/update-reaction channel msgid emoji nick on?))565 (swap! channels reactions/update-reaction channel msgid emoji nick on?))
565 566
566 (defn edit-message!567 (defn edit-message!
567- "Rewrite a message in place, and say so. `msgid` names the line as it was568+ "Rewrite a message in place, and say so.
568- first sent: a message keeps the id it was born with across every revision,569+
569- which is what keeps its reactions, replies and pins attached to it.570+ `frq.edits` is the fold and what it answers; the atom and the picture links
570-571+ are this half's. A message keeps the id it was born with across every
571- Only the sender may rewrite their own line, so an edit whose nick is not the572+ revision, which is what keeps its reactions, replies and pins attached to it."
572- one on the message is dropped — the server checks authorship too, and a
573- client that believed the wire alone would let a hostile relay put words in
574- somebody's mouth.
575-
576- Answers what became of it: `:applied`, `:refused` for one that was not the
577- sender's to make, or `:absent` when no line here has that id — an edit of
578- something older than the backlog we asked for, which is the one case the
579- caller shows as a line of its own rather than losing what it says."
580 [channel msgid from text]573 [channel msgid from text]
581- (if-not (and channel msgid)574+ (let [out (edits/apply-edit @channels channel msgid from text
582- :absent575+ #(assoc % :images (media/image-urls text)))]
583- (let [found? (atom nil)]576+ (reset! channels (:channels out))
584- (swap! channels577+ (:result out)))
585- (fn [m]
586- (if-let [msgs (get-in m [channel :messages])]
587- (assoc-in m [channel :messages]
588- (mapv (fn [msg]
589- (if (= msgid (:id msg))
590- (if (= (str/lower-case (or (:from msg) ""))
591- (str/lower-case (or from "")))
592- (do (reset! found? :applied)
593- (assoc msg
594- :text text
595- :images (media/image-urls text)
596- :edited? true))
597- ;; Somebody else's line. The server
598- ;; refuses this too, so it is either a
599- ;; peer talking to us directly or one
600- ;; lying — and neither gets to put words
601- ;; under a name that is not theirs.
602- (do (reset! found? :refused) msg))
603- msg))
604- msgs))
605- m)))
606- ;; The revision may link a picture the original did not.
607- (when (= :applied @found?)
608- (doseq [url (media/image-urls text)]
609- (media/fetch! url #(swap! media-tick inc))))
610- (or @found? :absent))))
611-
612-;; --- who is in the room ------------------------------------------------------
613-;; All of it is `frq.members` now: a fold over the channels map, which is the
614-;; same fold under either compiler. What stays here is the atom it is folded
615-;; into and `ensure-channel`, because a room means more to this half than to
616-;; the phone — unread counts, read marks, a joining flag — and the shared
617-;; functions deliberately only ever touch `:users` and `:names-acc`.
618 578
619 (defn- names-line [channel names]579 (defn- names-line [channel names]
620 (swap! channels #(members/with-names (ensure-channel % channel) channel names)))580 (swap! channels #(members/with-names (ensure-channel % channel) channel names)))
@@ -1426,10 +1386,7 @@
1426 server itself falls back to for an account with no DID and an edit it would1386 server itself falls back to for an account with no DID and an edit it would
1427 refuse is one not worth offering."1387 refuse is one not worth offering."
1428 [m]1388 [m]
1429- (and (not (:system? m))1389+ (rooms/mine? m @form-nick))
1430- (seq (or (:from m) ""))
1431- (= (str/lower-case (:from m))
1432- (str/lower-case (or @form-nick "")))))
1433 1390
1434 (defn start-edit!1391 (defn start-edit!
1435 "Put a message back in the box to be rewritten.1392 "Put a message back in the box to be rewritten.