| @@ -4,10 +4,12 @@ |
| 4 | The screens follow sleek's: connect, chats, chat, discover, settings, under a | 4 | The screens follow sleek's: connect, chats, chat, discover, settings, under a |
| 5 | tab bar. Where sleek draws them in Rust against egui directly, here each is a | 5 | tab bar. Where sleek draws them in Rust against egui directly, here each is a |
| 6 | hiccup component over the same widgets." | 6 | hiccup component over the same widgets." |
| 7 | - (:require [glimmer.ratom :as r :refer [atom]] | 7 | + (:require [clojure.string :as str] |
| | 8 | + [glimmer.ratom :as r :refer [atom]] |
| 8 | [glimmer.core :as ui] | 9 | [glimmer.core :as ui] |
| 9 | [glimmer-vidya.core :as vidya] | 10 | [glimmer-vidya.core :as vidya] |
| 10 | [frq.media :as media] | 11 | [frq.media :as media] |
| | 12 | + [frq.platform :as platform] |
| 11 | [frq.state :as s])) | 13 | [frq.state :as s])) |
| 12 | | 14 | |
| 13 | ;; ---------------------------------------------------------------- pieces | 15 | ;; ---------------------------------------------------------------- pieces |
| @@ -171,6 +173,49 @@ |
| 171 | | 173 | |
| 172 | ;; ---------------------------------------------------------------- chat | 174 | ;; ---------------------------------------------------------------- chat |
| 173 | | 175 | |
| | 176 | +(def ^:private url-pattern #"https?://[^\s<>\"]+") |
| | 177 | + |
| | 178 | +(defn- trim-trailing-punctuation |
| | 179 | + "A URL at the end of a sentence would otherwise keep the sentence's |
| | 180 | + punctuation. A closing bracket only counts as trailing when the URL does not |
| | 181 | + open one itself, which is what keeps a wikipedia-style path intact." |
| | 182 | + [url] |
| | 183 | + (loop [u url] |
| | 184 | + (let [c (last u)] |
| | 185 | + (cond |
| | 186 | + (nil? c) u |
| | 187 | + (contains? #{\. \, \; \: \! \?} c) (recur (subs u 0 (dec (count u)))) |
| | 188 | + (and (= \) c) (not (str/includes? u "("))) (recur (subs u 0 (dec (count u)))) |
| | 189 | + :else u)))) |
| | 190 | + |
| | 191 | +(defn text-runs |
| | 192 | + "Message text as alternating [:text s] and [:link url] runs. |
| | 193 | + |
| | 194 | + Runs because a link has to be its own widget to be clickable, and stacked |
| | 195 | + rather than laid out in a row because a wrapping label inside a horizontal |
| | 196 | + row lays out against the row's width, not the column's — which is what drags |
| | 197 | + long URLs off the left edge." |
| | 198 | + [text] |
| | 199 | + (let [text (or text "")] |
| | 200 | + (loop [pos 0 acc []] |
| | 201 | + (if-let [raw (re-find url-pattern (subs text pos))] |
| | 202 | + (let [url (trim-trailing-punctuation raw) |
| | 203 | + at (+ pos (str/index-of (subs text pos) raw)) |
| | 204 | + before (subs text pos at) |
| | 205 | + acc (cond-> acc (seq before) (conj [:text before]))] |
| | 206 | + (recur (+ at (count url)) (conj acc [:link url]))) |
| | 207 | + (let [tail (subs text pos)] |
| | 208 | + (cond-> acc (seq tail) (conj [:text tail]))))))) |
| | 209 | + |
| | 210 | +(defn- run-node |
| | 211 | + "One run as a widget. A link is accent-coloured and opens on click; the rest |
| | 212 | + is body text, or dim text for the lines the client writes itself." |
| | 213 | + [j [kind value] system?] |
| | 214 | + (cond |
| | 215 | + (= :link kind) [:link {:key j :label value :on-click #(platform/open-url! value)}] |
| | 216 | + system? [:dim-label {:key j :label (str/trim value)}] |
| | 217 | + :else [:label {:key j :label (str/trim value)}])) |
| | 218 | + |
| 174 | (defn message-row | 219 | (defn message-row |
| 175 | "One message. `prev` is the message above it, which decides whether this one | 220 | "One message. `prev` is the message above it, which decides whether this one |
| 176 | repeats the sender. | 221 | repeats the sender. |
| @@ -189,9 +234,9 @@ |
| 189 | [:vbox {:key :who} | 234 | [:vbox {:key :who} |
| 190 | (when-not (or same-sender? (:system? m)) | 235 | (when-not (or same-sender? (:system? m)) |
| 191 | [:dim-label {:label (:from m)}])] | 236 | [:dim-label {:label (:from m)}])] |
| 192 | - (if (:system? m) | 237 | + [:vbox {:key :text :spacing 2} |
| 193 | - [:dim-label {:label (:text m)}] | 238 | + (map-indexed (fn [j run] (run-node j run (:system? m))) |
| 194 | - [:label {:label (:text m)}]) | 239 | + (text-runs (:text m)))] |
| 195 | ;; Pictures under the line that linked them. The link stays: it is what a | 240 | ;; Pictures under the line that linked them. The link stays: it is what a |
| 196 | ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | 241 | ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. |
| 197 | [:vbox {:key :images :spacing 4} | 242 | [:vbox {:key :images :spacing 4} |
| @@ -220,8 +265,12 @@ |
| 220 | [:hbox {:spacing 8} | 265 | [:hbox {:spacing 8} |
| 221 | [:button {:label "← Back" :on-click #(reset! s/lightbox nil)}] | 266 | [:button {:label "← Back" :on-click #(reset! s/lightbox nil)}] |
| 222 | [:dim-label {:label url}]] | 267 | [:dim-label {:label url}]] |
| 223 | - ;; Clicking the picture closes it too — the same gesture that opened it. | 268 | + ;; A picture fits the window's width, and a tall one is then taller than |
| 224 | - [:image {:src path :max-height 2000 :on-click #(reset! s/lightbox nil)}]])) | 269 | + ;; the window — so it scrolls. No `:reserve`: nothing follows it here, so |
| | 270 | + ;; the rest of the window is the picture's to use. |
| | 271 | + [:scroll {:orientation :vertical} |
| | 272 | + ;; Clicking the picture closes it too — the same gesture that opened it. |
| | 273 | + [:image {:src path :max-height 20000 :on-click #(reset! s/lightbox nil)}]]])) |
| 225 | | 274 | |
| 226 | (defn chat-screen [] | 275 | (defn chat-screen [] |
| 227 | (let [name @s/current | 276 | (let [name @s/current |