Tag the Flutter line that answers as a reply
The composer's banner was the only place the reply existed: send built its PRIVMSG by hand and never read `replying-to`, so the `+draft/reply` tag every other freeq client threads on went out missing, and the banner stayed up to aim the next line at the same message. The line is `frq.irc.mutate`'s now — say-line, beside edit-line, unsigned because a new message is not a change to one already said — so both halves tag a reply the same way. jolt's `privmsg!` writes it rather than its own string, which also gets the tag value escaped the way IRCv3 asks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
879ae18 parent: f218f2a modified
common/frq/irc/mutate.cljc +13 -0 | @@ -65,3 +65,16 @@ | ||
| 65 | 65 | (tag-line (assoc (msgsig/edit-tags target msgid text nil peer-did) |
| 66 | 66 | "+draft/edit" msgid) |
| 67 | 67 | (str "PRIVMSG " target " :" text)))) |
| 68 | + | |
| 69 | +(defn say-line | |
| 70 | + "Say something. With `reply-to`, say it as an answer to that message. | |
| 71 | + | |
| 72 | + The `+draft/reply` tag is what every other freeq client reads to thread it, | |
| 73 | + and what both of ours draw their chips from. Unsigned, unlike the mutations | |
| 74 | + above: a new message is not a change to one already said, and the server | |
| 75 | + asks for no proof of authorship beyond the account it came in on." | |
| 76 | + ([target text] (say-line target text nil)) | |
| 77 | + ([target text reply-to] | |
| 78 | + (if (seq reply-to) | |
| 79 | + (tag-line {"+draft/reply" reply-to} (str "PRIVMSG " target " :" text)) | |
| 80 | + (str "PRIVMSG " target " :" text)))) | |
| @@ -65,3 +65,16 @@ | |||
| 65 | (tag-line (assoc (msgsig/edit-tags target msgid text nil peer-did) | 65 | (tag-line (assoc (msgsig/edit-tags target msgid text nil peer-did) |
| 66 | "+draft/edit" msgid) | 66 | "+draft/edit" msgid) |
| 67 | (str "PRIVMSG " target " :" text)))) | 67 | (str "PRIVMSG " target " :" text)))) |
| 68 | + | ||
| 69 | +(defn say-line | ||
| 70 | + "Say something. With `reply-to`, say it as an answer to that message. | ||
| 71 | + | ||
| 72 | + The `+draft/reply` tag is what every other freeq client reads to thread it, | ||
| 73 | + and what both of ours draw their chips from. Unsigned, unlike the mutations | ||
| 74 | + above: a new message is not a change to one already said, and the server | ||
| 75 | + asks for no proof of authorship beyond the account it came in on." | ||
| 76 | + ([target text] (say-line target text nil)) | ||
| 77 | + ([target text reply-to] | ||
| 78 | + (if (seq reply-to) | ||
| 79 | + (tag-line {"+draft/reply" reply-to} (str "PRIVMSG " target " :" text)) | ||
| 80 | + (str "PRIVMSG " target " :" text)))) | ||
modified
flutter/src/frq/main.cljd +12 -3 | @@ -921,7 +921,8 @@ | ||
| 921 | 921 | (defn- send-draft! [] |
| 922 | 922 | (let [text (str @cells/draft) |
| 923 | 923 | room (str @cells/current) |
| 924 | - edit @cells/editing] | |
| 924 | + edit @cells/editing | |
| 925 | + reply-to @cells/replying-to] | |
| 925 | 926 | (cond |
| 926 | 927 | (empty? room) nil |
| 927 | 928 | |
| @@ -980,18 +981,26 @@ | ||
| 980 | 981 | ;; show. A line that is only a picture is only the link. |
| 981 | 982 | text (.trim (str text (when url (str " " url))))] |
| 982 | 983 | (do |
| 983 | - (net/send-line! @conn (str "PRIVMSG " room " :" text)) | |
| 984 | + ;; The `+draft/reply` tag is what makes this an answer rather than a | |
| 985 | + ;; line that happens to follow: without it the banner over the box is | |
| 986 | + ;; the only place the reply ever existed, and it goes out untagged. | |
| 987 | + (net/send-line! @conn (mutate/say-line room text (:id reply-to))) | |
| 984 | 988 | ;; Echoed locally only when the server will not echo it back. With |
| 985 | 989 | ;; `echo-message` negotiated it does — that is what the cap is for, and |
| 986 | 990 | ;; it is how a client learns the msgid of its own line — so adding one |
| 987 | 991 | ;; here as well put every sent message in the room twice. |
| 988 | 992 | (when-not (handshake/acked? @caps "echo-message") |
| 989 | 993 | (swap! cells/channels update room |
| 990 | - #(update % :messages conj {:from @cells/form-nick :text text}))) | |
| 994 | + #(update % :messages conj {:from @cells/form-nick :text text | |
| 995 | + :reply-to (:id reply-to)}))) | |
| 991 | 996 | ;; The attachment has done its job the moment the link is on the wire. |
| 992 | 997 | (when-let [a @cells/attachment] |
| 993 | 998 | (reset! cells/attachment nil) |
| 994 | 999 | (discard! (:path a))) |
| 1000 | + ;; The banner comes down with the send: it is the question this line | |
| 1001 | + ;; just answered, and leaving it up would aim the next line at the same | |
| 1002 | + ;; message. | |
| 1003 | + (reset! cells/replying-to nil) | |
| 995 | 1004 | (reset! cells/draft "")))))) |
| 996 | 1005 | |
| 997 | 1006 | (defn- message-by-id [room id] |
| @@ -921,7 +921,8 @@ | |||
| 921 | (defn- send-draft! [] | 921 | (defn- send-draft! [] |
| 922 | (let [text (str @cells/draft) | 922 | (let [text (str @cells/draft) |
| 923 | room (str @cells/current) | 923 | room (str @cells/current) |
| 924 | - edit @cells/editing] | 924 | + edit @cells/editing |
| 925 | + reply-to @cells/replying-to] | ||
| 925 | (cond | 926 | (cond |
| 926 | (empty? room) nil | 927 | (empty? room) nil |
| 927 | 928 | ||
| @@ -980,18 +981,26 @@ | |||
| 980 | ;; show. A line that is only a picture is only the link. | 981 | ;; show. A line that is only a picture is only the link. |
| 981 | text (.trim (str text (when url (str " " url))))] | 982 | text (.trim (str text (when url (str " " url))))] |
| 982 | (do | 983 | (do |
| 983 | - (net/send-line! @conn (str "PRIVMSG " room " :" text)) | 984 | + ;; The `+draft/reply` tag is what makes this an answer rather than a |
| 985 | + ;; line that happens to follow: without it the banner over the box is | ||
| 986 | + ;; the only place the reply ever existed, and it goes out untagged. | ||
| 987 | + (net/send-line! @conn (mutate/say-line room text (:id reply-to))) | ||
| 984 | ;; Echoed locally only when the server will not echo it back. With | 988 | ;; Echoed locally only when the server will not echo it back. With |
| 985 | ;; `echo-message` negotiated it does — that is what the cap is for, and | 989 | ;; `echo-message` negotiated it does — that is what the cap is for, and |
| 986 | ;; it is how a client learns the msgid of its own line — so adding one | 990 | ;; it is how a client learns the msgid of its own line — so adding one |
| 987 | ;; here as well put every sent message in the room twice. | 991 | ;; here as well put every sent message in the room twice. |
| 988 | (when-not (handshake/acked? @caps "echo-message") | 992 | (when-not (handshake/acked? @caps "echo-message") |
| 989 | (swap! cells/channels update room | 993 | (swap! cells/channels update room |
| 990 | - #(update % :messages conj {:from @cells/form-nick :text text}))) | 994 | + #(update % :messages conj {:from @cells/form-nick :text text |
| 995 | + :reply-to (:id reply-to)}))) | ||
| 991 | ;; The attachment has done its job the moment the link is on the wire. | 996 | ;; The attachment has done its job the moment the link is on the wire. |
| 992 | (when-let [a @cells/attachment] | 997 | (when-let [a @cells/attachment] |
| 993 | (reset! cells/attachment nil) | 998 | (reset! cells/attachment nil) |
| 994 | (discard! (:path a))) | 999 | (discard! (:path a))) |
| 1000 | + ;; The banner comes down with the send: it is the question this line | ||
| 1001 | + ;; just answered, and leaving it up would aim the next line at the same | ||
| 1002 | + ;; message. | ||
| 1003 | + (reset! cells/replying-to nil) | ||
| 995 | (reset! cells/draft "")))))) | 1004 | (reset! cells/draft "")))))) |
| 996 | 1005 | ||
| 997 | (defn- message-by-id [room id] | 1006 | (defn- message-by-id [room id] |
modified
src/frq/irc.clj +3 -3 | @@ -234,11 +234,11 @@ | ||
| 234 | 234 | (defn privmsg! |
| 235 | 235 | "Say something. With `reply-to`, say it as an answer to that message: the |
| 236 | 236 | `+draft/reply` tag is what every other freeq client reads to thread it, and |
| 237 | - what this one draws its chips from." | |
| 237 | + what this one draws its chips from. The line is `frq.irc.mutate`'s, so both | |
| 238 | + halves tag a reply the same way; this writes it." | |
| 238 | 239 | ([conn target text] (privmsg! conn target text nil)) |
| 239 | 240 | ([conn target text reply-to] |
| 240 | - (send-line! conn (str (when (seq reply-to) (str "@+draft/reply=" reply-to " ")) | |
| 241 | - "PRIVMSG " target " :" text)))) | |
| 241 | + (send-line! conn (mutate/say-line target text reply-to)))) | |
| 242 | 242 | |
| 243 | 243 | (defn edit! |
| 244 | 244 | "Rewrite something already said. The line is `frq.irc.mutate`'s; this writes |
| @@ -234,11 +234,11 @@ | |||
| 234 | (defn privmsg! | 234 | (defn privmsg! |
| 235 | "Say something. With `reply-to`, say it as an answer to that message: the | 235 | "Say something. With `reply-to`, say it as an answer to that message: the |
| 236 | `+draft/reply` tag is what every other freeq client reads to thread it, and | 236 | `+draft/reply` tag is what every other freeq client reads to thread it, and |
| 237 | - what this one draws its chips from." | 237 | + what this one draws its chips from. The line is `frq.irc.mutate`'s, so both |
| 238 | + halves tag a reply the same way; this writes it." | ||
| 238 | ([conn target text] (privmsg! conn target text nil)) | 239 | ([conn target text] (privmsg! conn target text nil)) |
| 239 | ([conn target text reply-to] | 240 | ([conn target text reply-to] |
| 240 | - (send-line! conn (str (when (seq reply-to) (str "@+draft/reply=" reply-to " ")) | 241 | + (send-line! conn (mutate/say-line target text reply-to)))) |
| 241 | - "PRIVMSG " target " :" text)))) | ||
| 242 | 242 | ||
| 243 | (defn edit! | 243 | (defn edit! |
| 244 | "Rewrite something already said. The line is `frq.irc.mutate`'s; this writes | 244 | "Rewrite something already said. The line is `frq.irc.mutate`'s; this writes |