| @@ -157,6 +157,46 @@ |
| 157 | [] | 157 | [] |
| 158 | (and @terminal? @terminal-graphics?)) | 158 | (and @terminal? @terminal-graphics?)) |
| 159 | | 159 | |
| | 160 | +(defonce ^:private derived-cells |
| | 161 | + ;; One cell per question, kept for the session: a cell made afresh on every |
| | 162 | + ;; render would add a watch to its source each time and never take it off. |
| | 163 | + (clojure.core/atom {})) |
| | 164 | + |
| | 165 | +(defn- derived |
| | 166 | + "A reactive cell for one row's answer to a question about shared state, made |
| | 167 | + once per `k` and kept. |
| | 168 | + |
| | 169 | + A message row that read `s/highlight` itself was re-rendered whenever the |
| | 170 | + highlight moved anywhere — every row in the backlog, for one jump — and the |
| | 171 | + same for a hover, an open picker, or any face or picture finishing a fetch. |
| | 172 | + A reaction is recomputed on each such change, which is a comparison, but it |
| | 173 | + wakes the rows that read it only when its answer changes: the two rows a |
| | 174 | + jump moves between, the one face under the pointer. |
| | 175 | + |
| | 176 | + Kept rather than collected: glimmer's reactions have no way to unsubscribe, |
| | 177 | + so a cell per message lasts as long as the session does." |
| | 178 | + [k f] |
| | 179 | + (or (get @derived-cells k) |
| | 180 | + (let [cell (r/reaction (f))] |
| | 181 | + (swap! derived-cells assoc k cell) |
| | 182 | + cell))) |
| | 183 | + |
| | 184 | +(defn- avatar-path |
| | 185 | + "The cell answering where `actor`'s face is on disk, once it is." |
| | 186 | + [actor] |
| | 187 | + (derived [:avatar actor] #(do @s/media-tick (avatars/path-when-ready actor)))) |
| | 188 | + |
| | 189 | +(defn- image-path |
| | 190 | + "The cell answering where the picture behind `url` is on disk, once it is." |
| | 191 | + [url] |
| | 192 | + (derived [:image url] #(do @s/media-tick (media/path-when-ready url)))) |
| | 193 | + |
| | 194 | +(defonce ^:private hovered-face |
| | 195 | + ;; Which message's face the pointer is on. `profile/hovering` says whose, and |
| | 196 | + ;; the same person's face is on every message they sent: asked by name alone, |
| | 197 | + ;; one hover woke every one of those rows and hung a card off each face. |
| | 198 | + (atom nil)) |
| | 199 | + |
| 160 | ;; How big a face is on a message, in points — the size the window has always | 200 | ;; How big a face is on a message, in points — the size the window has always |
| 161 | ;; drawn one at. A terminal's cell is eight points across and sixteen down, so | 201 | ;; drawn one at. A terminal's cell is eight points across and sixteen down, so |
| 162 | ;; the same number is four columns by two rows there: a cached 128-pixel | 202 | ;; the same number is four columns by two rows there: a cached 128-pixel |
| @@ -562,14 +602,19 @@ |
| 562 | than a button with the emoji as its label — the chip draws the glyph from the | 602 | than a button with the emoji as its label — the chip draws the glyph from the |
| 563 | Twemoji pack, in colour, where a label gets whatever the text font has." | 603 | Twemoji pack, in colour, where a label gets whatever the text font has." |
| 564 | [channel m] | 604 | [channel m] |
| 565 | - (let [reactions (:reactions m)] | 605 | + (let [reactions (:reactions m) |
| | 606 | + ;; Which of this message's pills the pointer is on, if any. Asked once |
| | 607 | + ;; per message, so moving between pills wakes the rows involved and not |
| | 608 | + ;; every row that has a reaction on it. |
| | 609 | + hovered @(derived [:pill-hover (:id m)] |
| | 610 | + #(let [h @s/reaction-hover] |
| | 611 | + (when (= (:id m) (:id h)) (:emoji h))))] |
| 566 | ;; `into` and not a lazy `for` inside the vector. The pills read | 612 | ;; `into` and not a lazy `for` inside the vector. The pills read |
| 567 | - ;; `hovering-reaction?` — a ratom — and a ratom read while a lazy seq is | 613 | + ;; `my-reaction?` — a ratom — and a ratom read while a lazy seq is being |
| 568 | - ;; being realised somewhere other than the render is a read the component | 614 | + ;; realised somewhere other than the render is a read the component never |
| 569 | - ;; never records, so the row went on showing what it showed before the | 615 | + ;; records, so the row went on showing what it showed before. The pictures |
| 570 | - ;; pointer arrived. It is the one place in this file whose ratom read is | 616 | + ;; in `message-body` are the other place a ratom is read inside a `for`, |
| 571 | - ;; inside the `for` body rather than above it, which is why it is the one | 617 | + ;; and are made eager for the same reason. |
| 572 | - ;; place that needs this. | | |
| 573 | (into | 618 | (into |
| 574 | [:hbox {:key :pills :spacing (chip-gap)}] | 619 | [:hbox {:key :pills :spacing (chip-gap)}] |
| 575 | (for [emoji (sort (keys reactions))] | 620 | (for [emoji (sort (keys reactions))] |
| @@ -588,7 +633,7 @@ |
| 588 | ;; Only the hovered pill carries a card: the panel is painted from | 633 | ;; Only the hovered pill carries a card: the panel is painted from |
| 589 | ;; whatever children the node has, and a channel's worth of unseen | 634 | ;; whatever children the node has, and a channel's worth of unseen |
| 590 | ;; lists is a tree nobody looks at. | 635 | ;; lists is a tree nobody looks at. |
| 591 | - (when (and (platform/desktop?) (s/hovering-reaction? (:id m) emoji)) | 636 | + (when (and (platform/desktop?) (= emoji hovered)) |
| 592 | [reactor-card emoji (get reactions emoji)])])))) | 637 | [reactor-card emoji (get reactions emoji)])])))) |
| 593 | | 638 | |
| 594 | (def ^:private picker-columns | 639 | (def ^:private picker-columns |
| @@ -766,7 +811,7 @@ |
| 766 | ;; Always an avatar, picture or not: the initial stands in until the | 811 | ;; Always an avatar, picture or not: the initial stands in until the |
| 767 | ;; fetch lands, and for the guests who have no profile at all, which | 812 | ;; fetch lands, and for the guests who have no profile at all, which |
| 768 | ;; is what keeps the column of faces straight down the left. | 813 | ;; is what keeps the column of faces straight down the left. |
| 769 | - ;; Reading the tick subscribes this row to a fetch finishing. | 814 | + ;; `avatar-path` is what wakes this row when that fetch lands. |
| 770 | ;; The face is also the way to the person behind it: Vidya's plain | 815 | ;; The face is also the way to the person behind it: Vidya's plain |
| 771 | ;; label does not answer the pointer, so the tap sleek puts on the | 816 | ;; label does not answer the pointer, so the tap sleek puts on the |
| 772 | ;; nick lives here, on the one thing in the row that does. | 817 | ;; nick lives here, on the one thing in the row that does. |
| @@ -779,18 +824,21 @@ |
| 779 | ;; terminal can draw a picture there is a face after all, hung beside | 824 | ;; terminal can draw a picture there is a face after all, hung beside |
| 780 | ;; the whole message rather than off its heading — `message-row` has it. | 825 | ;; the whole message rather than off its heading — `message-row` has it. |
| 781 | (when-not @terminal? | 826 | (when-not @terminal? |
| 782 | - (let [_ @s/media-tick] | 827 | + (let [src @(avatar-path (:actor m))] |
| 783 | [:avatar (cond-> {:label (:from m) | 828 | [:avatar (cond-> {:label (:from m) |
| 784 | - :src (or (avatars/path-when-ready (:actor m)) "") | 829 | + :src (or src "") |
| 785 | :size face-size | 830 | :size face-size |
| 786 | :on-click #(profile/open! (:from m) (:actor m))} | 831 | :on-click #(profile/open! (:from m) (:actor m))} |
| 787 | (platform/desktop?) | 832 | (platform/desktop?) |
| 788 | - (assoc :on-hover #(profile/hover! (:from m) (:actor m)) | 833 | + (assoc :on-hover #(do (reset! hovered-face (:id m)) |
| | 834 | + (profile/hover! (:from m) (:actor m))) |
| 789 | :on-unhover #(profile/unhover! (:from m)))) | 835 | :on-unhover #(profile/unhover! (:from m)))) |
| 790 | ;; Only the hovered face carries one: a card is painted when its | 836 | ;; Only the hovered face carries one: a card is painted when its |
| 791 | ;; node has children, and the pointer is on one face at a time. | 837 | ;; node has children, and the pointer is on one face at a time. |
| 792 | (when (and (platform/desktop?) | 838 | (when (and (platform/desktop?) |
| 793 | - (= (:from m) (:nick @profile/hovering))) | 839 | + @(derived [:hovering (:id m) (:from m)] |
| | 840 | + #(and (= (:id m) @hovered-face) |
| | 841 | + (= (:from m) (:nick @profile/hovering))))) |
| 794 | [hover-card (:from m) (:actor m)])])) | 842 | [hover-card (:from m) (:actor m)])])) |
| 795 | ;; The name carries the row, so it is set at body size in the plain | 843 | ;; The name carries the row, so it is set at body size in the plain |
| 796 | ;; text colour: dimmed caption made the one thing you scan a column | 844 | ;; text colour: dimmed caption made the one thing you scan a column |
| @@ -837,7 +885,7 @@ |
| 837 | ;; has no msgid, and neither does a closed picker — so `nil = nil` was | 885 | ;; has no msgid, and neither does a closed picker — so `nil = nil` was |
| 838 | ;; every one of those messages opening a picker of its own at startup. | 886 | ;; every one of those messages opening a picker of its own at startup. |
| 839 | [:vbox {:key :picker :margin-bottom 4} | 887 | [:vbox {:key :picker :margin-bottom 4} |
| 840 | - (when (and (:id m) (= (:id m) (:id @s/reacting))) | 888 | + (when (and (:id m) @(derived [:reacting (:id m)] #(= (:id m) (:id @s/reacting)))) |
| 841 | [emoji-picker])] | 889 | [emoji-picker])] |
| 842 | ;; Pictures under the line that linked them. The link stays: it is what a | 890 | ;; Pictures under the line that linked them. The link stays: it is what a |
| 843 | ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | 891 | ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. |
| @@ -845,15 +893,17 @@ |
| 845 | :images | 893 | :images |
| 846 | [:vbox {:key :images :spacing 4} | 894 | [:vbox {:key :images :spacing 4} |
| 847 | (when (seq (:images m)) | 895 | (when (seq (:images m)) |
| 848 | - ;; Reading the tick is what subscribes this row to a fetch finishing. | 896 | + ;; Eager, so each read happens during the render and is recorded — see |
| 849 | - (let [_ @s/media-tick] | 897 | + ;; `reaction-row`. Each picture is read through `image-path`, which |
| 850 | - (for [url (:images m)] | 898 | + ;; wakes this row for its own pictures landing and not for everyone's. |
| 851 | - (when-let [path (media/path-when-ready url)] | 899 | + (doall |
| 852 | - [:image {:key url | 900 | + (for [url (:images m)] |
| 853 | - :src path | 901 | + (when-let [path @(image-path url)] |
| 854 | - :max-height (preview-height) | 902 | + [:image {:key url |
| 855 | - :max-width (preview-width) | 903 | + :src path |
| 856 | - :on-click #(reset! s/lightbox {:path path :url url})}]))))]) | 904 | + :max-height (preview-height) |
| | 905 | + :max-width (preview-width) |
| | 906 | + :on-click #(reset! s/lightbox {:path path :url url})}]))))]) |
| 857 | ;; Reactions go last, under whatever the message turned out to be: a | 907 | ;; Reactions go last, under whatever the message turned out to be: a |
| 858 | ;; line with a picture on it is the picture, and pills between the words | 908 | ;; line with a picture on it is the picture, and pills between the words |
| 859 | ;; and the image they introduce read as reactions to the words alone. | 909 | ;; and the image they introduce read as reactions to the words alone. |
| @@ -880,7 +930,12 @@ |
| 880 | (let [;; What a jump landed on wears a surface of its own for a moment, so | 930 | (let [;; What a jump landed on wears a surface of its own for a moment, so |
| 881 | ;; the answer to "which one was I sent to" is on the screen rather | 931 | ;; the answer to "which one was I sent to" is on the screen rather |
| 882 | ;; than in the reader's count of rows. | 932 | ;; than in the reader's count of rows. |
| 883 | - highlit? (boolean (and (:id m) (= (:id m) @s/highlight)))] | 933 | + ;; Through `derived`, as is everything below that asks about shared |
| | 934 | + ;; state: a row that read the highlight itself re-rendered for every |
| | 935 | + ;; jump anywhere in the backlog. |
| | 936 | + highlit? (boolean (and (:id m) |
| | 937 | + @(derived [:highlit (:id m)] |
| | 938 | + #(= (:id m) @s/highlight))))] |
| 884 | [:vbox {:key i :spacing 2 :margin 0 | 939 | [:vbox {:key i :spacing 2 :margin 0 |
| 885 | ;; Clear of the right edge: the actions ride that edge, and the | 940 | ;; Clear of the right edge: the actions ride that edge, and the |
| 886 | ;; list's scrollbar rides it too — without this the ↩ is what the | 941 | ;; list's scrollbar rides it too — without this the ↩ is what the |
| @@ -891,7 +946,9 @@ |
| 891 | ;; closed-up line to belong to. | 946 | ;; closed-up line to belong to. |
| 892 | :margin-top 10 | 947 | :margin-top 10 |
| 893 | ;; The jump target is what a "go to message" click scrolls to. | 948 | ;; The jump target is what a "go to message" click scrolls to. |
| 894 | - :scroll-here (boolean (and (:id m) (= (:id m) @s/jump-to)))} | 949 | + :scroll-here (boolean (and (:id m) |
| | 950 | + @(derived [:jump (:id m)] |
| | 951 | + #(= (:id m) @s/jump-to))))} |
| 895 | ;; The gap above, where the margin cannot be one. `:margin-top 10` is | 952 | ;; The gap above, where the margin cannot be one. `:margin-top 10` is |
| 896 | ;; most of a row in a window and nothing at all in a terminal — ten points | 953 | ;; most of a row in a window and nothing at all in a terminal — ten points |
| 897 | ;; against a row of sixteen, rounded down, because a gap that thin is what | 954 | ;; against a row of sixteen, rounded down, because a gap that thin is what |
| @@ -907,12 +964,11 @@ |
| 907 | (if (terminal-face?) | 964 | (if (terminal-face?) |
| 908 | [:hbox {:key :faced :spacing 8} | 965 | [:hbox {:key :faced :spacing 8} |
| 909 | [:vbox {:key :face :width-request face-size} | 966 | [:vbox {:key :face :width-request face-size} |
| 910 | - (let [_ @s/media-tick] | 967 | + (when-let [path @(avatar-path (:actor m))] |
| 911 | - (when-let [path (avatars/path-when-ready (:actor m))] | 968 | + [:image {:key :picture |
| 912 | - [:image {:key :picture | 969 | + :src path |
| 913 | - :src path | 970 | + :max-width face-size |
| 914 | - :max-width face-size | 971 | + :max-height face-size}])] |
| 915 | - :max-height face-size}]))] | | |
| 916 | [message-body m highlit?]] | 972 | [message-body m highlit?]] |
| 917 | [message-body m highlit?])])) | 973 | [message-body m highlit?])])) |
| 918 | | 974 | |