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

Let a tall picture scroll in the lightbox

A picture is fitted to the window's width, which leaves a tall one taller than
the window and, until now, cut off at the bottom with no way down. It sits in
a scroll area now.

No `:reserve` on that area: nothing follows it, so the rest of the window is
the picture's to use — reserving a row for a compose bar that is not there
just left a dead band under the image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T11:06:24-07:00 Browse files
e84d24d parent: 563ada3
modified src/frq/app.jolt +55 -6
@@ -4,10 +4,12 @@
44 The screens follow sleek's: connect, chats, chat, discover, settings, under a
55 tab bar. Where sleek draws them in Rust against egui directly, here each is a
66 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]]
89 [glimmer.core :as ui]
910 [glimmer-vidya.core :as vidya]
1011 [frq.media :as media]
12+ [frq.platform :as platform]
1113 [frq.state :as s]))
1214
1315 ;; ---------------------------------------------------------------- pieces
@@ -171,6 +173,49 @@
171173
172174 ;; ---------------------------------------------------------------- chat
173175
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+
174219 (defn message-row
175220 "One message. `prev` is the message above it, which decides whether this one
176221 repeats the sender.
@@ -189,9 +234,9 @@
189234 [:vbox {:key :who}
190235 (when-not (or same-sender? (:system? m))
191236 [:dim-label {:label (:from m)}])]
192- (if (:system? m)
193- [:dim-label {:label (:text m)}]
194- [:label {:label (:text m)}])
237+ [:vbox {:key :text :spacing 2}
238+ (map-indexed (fn [j run] (run-node j run (:system? m)))
239+ (text-runs (:text m)))]
195240 ;; Pictures under the line that linked them. The link stays: it is what a
196241 ;; failed fetch, an unsupported format, or a phone with no TLS leaves you.
197242 [:vbox {:key :images :spacing 4}
@@ -220,8 +265,12 @@
220265 [:hbox {:spacing 8}
221266 [:button {:label "← Back" :on-click #(reset! s/lightbox nil)}]
222267 [:dim-label {:label url}]]
223- ;; Clicking the picture closes it too the same gesture that opened it.
224- [:image {:src path :max-height 2000 :on-click #(reset! s/lightbox nil)}]]))
268+ ;; A picture fits the window's width, and a tall one is then taller than
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)}]]]))
225274
226275 (defn chat-screen []
227276 (let [name @s/current
@@ -4,10 +4,12 @@
4 The screens follow sleek's: connect, chats, chat, discover, settings, under a4 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 a5 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 ;; ---------------------------------------------------------------- pieces15 ;; ---------------------------------------------------------------- pieces
@@ -171,6 +173,49 @@
171 173
172 ;; ---------------------------------------------------------------- chat174 ;; ---------------------------------------------------------------- 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-row219 (defn message-row
175 "One message. `prev` is the message above it, which decides whether this one220 "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 a240 ;; 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/current276 (let [name @s/current