Set an emoji in a message as a character, not as a reaction
The emoji came back from tofu wearing a pill: `:reaction` was the only node that drew from the Twemoji pack, so a glyph in a sentence arrived with a fill, a border and a hover card, and a line that ended in one read as a line somebody had reacted to. Two 🤔 in a message and a reader counts two reactions. jolt-native grew `:emoji` for this — the same picture with none of the tally around it — so ask for that instead. The row keeps a 2px spacing it did not need before: runs alternate text and picture, so the gap only ever falls either side of an emoji, and a bare glyph has no inner margin of its own the way the pill did. The pin moves with the call site, because a lock that named the old jolt-native would make `:emoji` an unknown tag and drop the emoji from the line altogether — worse than the pill it replaces. It also catches frq up on the terminal backend, which landed while the lock sat still. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
00b5f93 parent: 9afdfa4 modified
flake.lock +5 -5 | @@ -110,16 +110,16 @@ | ||
| 110 | 110 | "jolt-native": { |
| 111 | 111 | "flake": false, |
| 112 | 112 | "locked": { |
| 113 | - "lastModified": 1788324929, | |
| 114 | - "narHash": "sha256-EqvQn/ZUxrWJPXVlMvJkCUeHWemElG8wzPveGyRmaX8=", | |
| 113 | + "lastModified": 1788399042, | |
| 114 | + "narHash": "sha256-8fqp7OGifbyaS8cKMlBou/NtH8/o5laGomF9Pmptxck=", | |
| 115 | 115 | "ref": "refs/heads/main", |
| 116 | - "rev": "a9f3673367a8f0d3dd7d6ab1d380484d1df795c1", | |
| 117 | - "revCount": 30, | |
| 116 | + "rev": "00d32dd6bf0cde9cd73efa265c3057741d82e524", | |
| 117 | + "revCount": 32, | |
| 118 | 118 | "type": "git", |
| 119 | 119 | "url": "https://gitlab.com/nandithebull/jolt-native" |
| 120 | 120 | }, |
| 121 | 121 | "original": { |
| 122 | - "rev": "a9f3673367a8f0d3dd7d6ab1d380484d1df795c1", | |
| 122 | + "rev": "00d32dd6bf0cde9cd73efa265c3057741d82e524", | |
| 123 | 123 | "type": "git", |
| 124 | 124 | "url": "https://gitlab.com/nandithebull/jolt-native" |
| 125 | 125 | } |
| @@ -110,16 +110,16 @@ | |||
| 110 | "jolt-native": { | 110 | "jolt-native": { |
| 111 | "flake": false, | 111 | "flake": false, |
| 112 | "locked": { | 112 | "locked": { |
| 113 | - "lastModified": 1788324929, | 113 | + "lastModified": 1788399042, |
| 114 | - "narHash": "sha256-EqvQn/ZUxrWJPXVlMvJkCUeHWemElG8wzPveGyRmaX8=", | 114 | + "narHash": "sha256-8fqp7OGifbyaS8cKMlBou/NtH8/o5laGomF9Pmptxck=", |
| 115 | "ref": "refs/heads/main", | 115 | "ref": "refs/heads/main", |
| 116 | - "rev": "a9f3673367a8f0d3dd7d6ab1d380484d1df795c1", | 116 | + "rev": "00d32dd6bf0cde9cd73efa265c3057741d82e524", |
| 117 | - "revCount": 30, | 117 | + "revCount": 32, |
| 118 | "type": "git", | 118 | "type": "git", |
| 119 | "url": "https://gitlab.com/nandithebull/jolt-native" | 119 | "url": "https://gitlab.com/nandithebull/jolt-native" |
| 120 | }, | 120 | }, |
| 121 | "original": { | 121 | "original": { |
| 122 | - "rev": "a9f3673367a8f0d3dd7d6ab1d380484d1df795c1", | 122 | + "rev": "00d32dd6bf0cde9cd73efa265c3057741d82e524", |
| 123 | "type": "git", | 123 | "type": "git", |
| 124 | "url": "https://gitlab.com/nandithebull/jolt-native" | 124 | "url": "https://gitlab.com/nandithebull/jolt-native" |
| 125 | } | 125 | } |
modified
flake.nix +1 -1 | @@ -41,7 +41,7 @@ | ||
| 41 | 41 | # drift cost was silence: the Jolt half sent a reaction pill's hover card |
| 42 | 42 | # to a libvidya with no handler for one, and the pill said nothing. |
| 43 | 43 | jolt-native = { |
| 44 | - url = "git+https://gitlab.com/nandithebull/jolt-native?rev=a9f3673367a8f0d3dd7d6ab1d380484d1df795c1"; | |
| 44 | + url = "git+https://gitlab.com/nandithebull/jolt-native?rev=00d32dd6bf0cde9cd73efa265c3057741d82e524"; | |
| 45 | 45 | flake = false; |
| 46 | 46 | }; |
| 47 | 47 | |
| @@ -41,7 +41,7 @@ | |||
| 41 | # drift cost was silence: the Jolt half sent a reaction pill's hover card | 41 | # drift cost was silence: the Jolt half sent a reaction pill's hover card |
| 42 | # to a libvidya with no handler for one, and the pill said nothing. | 42 | # to a libvidya with no handler for one, and the pill said nothing. |
| 43 | jolt-native = { | 43 | jolt-native = { |
| 44 | - url = "git+https://gitlab.com/nandithebull/jolt-native?rev=a9f3673367a8f0d3dd7d6ab1d380484d1df795c1"; | 44 | + url = "git+https://gitlab.com/nandithebull/jolt-native?rev=00d32dd6bf0cde9cd73efa265c3057741d82e524"; |
| 45 | flake = false; | 45 | flake = false; |
| 46 | }; | 46 | }; |
| 47 | 47 | ||
modified
src/frq/app.jolt +16 -8 | @@ -293,10 +293,15 @@ | ||
| 293 | 293 | |
| 294 | 294 | Except for the emoji in it. A label is set in the text font, and that font — |
| 295 | 295 | Ubuntu with a DejaVu subset behind it — has no emoji in it at all, so every |
| 296 | - one anybody typed was drawn as tofu. A `:reaction` is the one node that | |
| 297 | - draws from the Twemoji pack, which is why a reaction pill has always shown | |
| 298 | - the same glyph a message could not: so the emoji come out of the text and go | |
| 299 | - in as chips, and the words either side stay text. | |
| 296 | + one anybody typed was drawn as tofu. The Twemoji pack has the picture, so | |
| 297 | + the emoji come out of the text and are drawn from the pack, and the words | |
| 298 | + either side stay text. | |
| 299 | + | |
| 300 | + `:emoji` and not `:reaction`, though both draw the same picture. A reaction | |
| 301 | + is a tally: it wears a pill, it answers the pointer, and it names the people | |
| 302 | + in it on hover. An emoji in a sentence is a character — so it is drawn bare, | |
| 303 | + and a message that ends in one no longer reads as a message someone has | |
| 304 | + reacted to. | |
| 300 | 305 | |
| 301 | 306 | The pieces go in a wrapping row rather than the column the runs themselves |
| 302 | 307 | are stacked in: a row is what puts an emoji *in* a sentence instead of |
| @@ -308,12 +313,15 @@ | ||
| 308 | 313 | [:link {:key j :label value :on-click #(platform/open-url! value)}] |
| 309 | 314 | (let [pieces (glyphs/runs (str/trim value))] |
| 310 | 315 | (if (glyphs/emoji? pieces) |
| 311 | - ;; No spacing of its own: the gaps between the words are the spaces | |
| 312 | - ;; the message was typed with, and a chip carries its own margin. | |
| 313 | - [:hbox {:key j :spacing 0} | |
| 316 | + ;; Runs alternate text and picture, so this gap only ever falls either | |
| 317 | + ;; side of an emoji — never between two words, whose spacing is the | |
| 318 | + ;; spaces the message was typed with. A bare glyph carries no margin of | |
| 319 | + ;; its own the way the pill did, and a picture set flush against a word | |
| 320 | + ;; reads as part of it. | |
| 321 | + [:hbox {:key j :spacing 2} | |
| 314 | 322 | (map-indexed (fn [k [pkind pvalue]] |
| 315 | 323 | (if (= :emoji pkind) |
| 316 | - [:reaction {:key k :emoji pvalue :size text-emoji-size}] | |
| 324 | + [:emoji {:key k :emoji pvalue :size text-emoji-size}] | |
| 317 | 325 | (word-node k pvalue system?))) |
| 318 | 326 | pieces)] |
| 319 | 327 | (word-node j (str/trim value) system?))))) |
| @@ -293,10 +293,15 @@ | |||
| 293 | 293 | ||
| 294 | Except for the emoji in it. A label is set in the text font, and that font — | 294 | Except for the emoji in it. A label is set in the text font, and that font — |
| 295 | Ubuntu with a DejaVu subset behind it — has no emoji in it at all, so every | 295 | Ubuntu with a DejaVu subset behind it — has no emoji in it at all, so every |
| 296 | - one anybody typed was drawn as tofu. A `:reaction` is the one node that | 296 | + one anybody typed was drawn as tofu. The Twemoji pack has the picture, so |
| 297 | - draws from the Twemoji pack, which is why a reaction pill has always shown | 297 | + the emoji come out of the text and are drawn from the pack, and the words |
| 298 | - the same glyph a message could not: so the emoji come out of the text and go | 298 | + either side stay text. |
| 299 | - in as chips, and the words either side stay text. | 299 | + |
| 300 | + `:emoji` and not `:reaction`, though both draw the same picture. A reaction | ||
| 301 | + is a tally: it wears a pill, it answers the pointer, and it names the people | ||
| 302 | + in it on hover. An emoji in a sentence is a character — so it is drawn bare, | ||
| 303 | + and a message that ends in one no longer reads as a message someone has | ||
| 304 | + reacted to. | ||
| 300 | 305 | ||
| 301 | The pieces go in a wrapping row rather than the column the runs themselves | 306 | The pieces go in a wrapping row rather than the column the runs themselves |
| 302 | are stacked in: a row is what puts an emoji *in* a sentence instead of | 307 | are stacked in: a row is what puts an emoji *in* a sentence instead of |
| @@ -308,12 +313,15 @@ | |||
| 308 | [:link {:key j :label value :on-click #(platform/open-url! value)}] | 313 | [:link {:key j :label value :on-click #(platform/open-url! value)}] |
| 309 | (let [pieces (glyphs/runs (str/trim value))] | 314 | (let [pieces (glyphs/runs (str/trim value))] |
| 310 | (if (glyphs/emoji? pieces) | 315 | (if (glyphs/emoji? pieces) |
| 311 | - ;; No spacing of its own: the gaps between the words are the spaces | 316 | + ;; Runs alternate text and picture, so this gap only ever falls either |
| 312 | - ;; the message was typed with, and a chip carries its own margin. | 317 | + ;; side of an emoji — never between two words, whose spacing is the |
| 313 | - [:hbox {:key j :spacing 0} | 318 | + ;; spaces the message was typed with. A bare glyph carries no margin of |
| 319 | + ;; its own the way the pill did, and a picture set flush against a word | ||
| 320 | + ;; reads as part of it. | ||
| 321 | + [:hbox {:key j :spacing 2} | ||
| 314 | (map-indexed (fn [k [pkind pvalue]] | 322 | (map-indexed (fn [k [pkind pvalue]] |
| 315 | (if (= :emoji pkind) | 323 | (if (= :emoji pkind) |
| 316 | - [:reaction {:key k :emoji pvalue :size text-emoji-size}] | 324 | + [:emoji {:key k :emoji pvalue :size text-emoji-size}] |
| 317 | (word-node k pvalue system?))) | 325 | (word-node k pvalue system?))) |
| 318 | pieces)] | 326 | pieces)] |
| 319 | (word-node j (str/trim value) system?))))) | 327 | (word-node j (str/trim value) system?))))) |
modified
src/frq/glyphs.jolt +4 -3 | @@ -5,8 +5,9 @@ | ||
| 5 | 5 | UI punctuation, maths-in-prose, box drawing — and no emoji at all. So an |
| 6 | 6 | emoji anybody typed into a message was drawn as tofu: the hollow box a font |
| 7 | 7 | puts up for a codepoint it has no glyph for. The same emoji in a reaction |
| 8 | - pill drew properly all along, because `:reaction` is the one node that looks | |
| 9 | - a glyph up in the Twemoji pack instead of setting it as text. | |
| 8 | + pill drew properly all along, because a pill looks its glyph up in the | |
| 9 | + Twemoji pack instead of setting it as text — which is what `:emoji` does | |
| 10 | + too, without the pill. | |
| 10 | 11 | |
| 11 | 12 | The split therefore has to happen before the text reaches a label, and this |
| 12 | 13 | is where: the message cut into the parts a font can draw and the parts only |
| @@ -63,7 +64,7 @@ | ||
| 63 | 64 | (defn runs |
| 64 | 65 | "`text` as alternating `[:text s]` and `[:emoji s]` runs, in order. |
| 65 | 66 | |
| 66 | - An `:emoji` run is one glyph: a chip draws one picture, so two emoji side by | |
| 67 | + An `:emoji` run is one glyph: a node draws one picture, so two emoji side by | |
| 67 | 68 | side are two runs and not one." |
| 68 | 69 | [text] |
| 69 | 70 | (let [cps (vec (seq (or text ""))) |
| @@ -5,8 +5,9 @@ | |||
| 5 | UI punctuation, maths-in-prose, box drawing — and no emoji at all. So an | 5 | UI punctuation, maths-in-prose, box drawing — and no emoji at all. So an |
| 6 | emoji anybody typed into a message was drawn as tofu: the hollow box a font | 6 | emoji anybody typed into a message was drawn as tofu: the hollow box a font |
| 7 | puts up for a codepoint it has no glyph for. The same emoji in a reaction | 7 | puts up for a codepoint it has no glyph for. The same emoji in a reaction |
| 8 | - pill drew properly all along, because `:reaction` is the one node that looks | 8 | + pill drew properly all along, because a pill looks its glyph up in the |
| 9 | - a glyph up in the Twemoji pack instead of setting it as text. | 9 | + Twemoji pack instead of setting it as text — which is what `:emoji` does |
| 10 | + too, without the pill. | ||
| 10 | 11 | ||
| 11 | The split therefore has to happen before the text reaches a label, and this | 12 | The split therefore has to happen before the text reaches a label, and this |
| 12 | is where: the message cut into the parts a font can draw and the parts only | 13 | is where: the message cut into the parts a font can draw and the parts only |
| @@ -63,7 +64,7 @@ | |||
| 63 | (defn runs | 64 | (defn runs |
| 64 | "`text` as alternating `[:text s]` and `[:emoji s]` runs, in order. | 65 | "`text` as alternating `[:text s]` and `[:emoji s]` runs, in order. |
| 65 | 66 | ||
| 66 | - An `:emoji` run is one glyph: a chip draws one picture, so two emoji side by | 67 | + An `:emoji` run is one glyph: a node draws one picture, so two emoji side by |
| 67 | side are two runs and not one." | 68 | side are two runs and not one." |
| 68 | [text] | 69 | [text] |
| 69 | (let [cps (vec (seq (or text ""))) | 70 | (let [cps (vec (seq (or text ""))) |