From 28eb3655c38c6993fce9589d4ad58bb32961254e Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 09:12:55 +0100 Subject: [PATCH 1/6] Fix lazy image loading --- src/computersandblues/lodestone/app.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index 7128011..b498ee1 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -357,7 +357,7 @@ "image" [:img {:src preview-url :srcset (str preview-url ", " remote-url) :alt (:description attachment) - :lazy "lazy"}] + :loading "lazy"}] "video" [:video {:controls true} [:source {:type (str "video/" ext) :src remote-url}] [:a {:href (:remote_url attachment)} (str "Original video at " (:remote_url attachment))]] From 5eb75a3ac29c642d1ec1e2100f4f8e3f9ceedf01 Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 13:07:28 +0100 Subject: [PATCH 2/6] Fix typo --- src/computersandblues/lodestone/app.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index b498ee1..079e47f 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -507,7 +507,7 @@ [:li "If you want to search verbatim for a phrase, \"quote it like this\""] [:li "Lodestone tries to turn words into regular expressions. " [:code "fossi.*ergy"] " will match \"fossile energy\"." [:code "bo?ar"] " will match both boar and bar."]]]) -;; the component tying it all toger +;; the component tying it all together (defn app [] (let [section (:section @state) From 54067c210375d0e6a7a20d545338b1b422258a10 Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 13:15:00 +0100 Subject: [PATCH 3/6] Shout less in database wrapper --- src/computersandblues/lodestone/app.cljs | 38 ++++++++++++++----- src/computersandblues/lodestone/database.cljs | 27 ++++++------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index 079e47f..6e096ff 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -160,7 +160,7 @@ (declare fetch-posts!) (defn- fetch-application-settings [] - (->> (db/open-cursor! ::db/application ::db/all) + (->> (db/open-cursor ::db/application ::db/all) (db/first-result (map #(js->clj % :keywordize-keys true))))) (defn setup-application! @@ -198,7 +198,7 @@ (.then (fn [application] (when application (swap! state assoc :section :posts) - (promise-all [application (db/count! ::db/posts)])))) + (promise-all [application (db/count ::db/posts)])))) (.then (fn [[application post-count]] (when post-count (if (zero? post-count) @@ -295,10 +295,10 @@ (map #(js->clj % :keywordize-keys true))) refresh-id (now)] (swap! state update-in [:section/posts :loading] conj refresh-id) - (. (promise-all [(db/count! ::db/posts) - (->> (db/open-cursor! ::db/posts ::db/all - {:index ::db/post-created-at - :direction :desc}) + (. (promise-all [(db/count ::db/posts) + (->> (db/open-cursor ::db/posts ::db/all + {:index ::db/post-created-at + :direction :desc}) (db/transduce-cursor xform))]) (then (fn [[total displayed-posts]] (swap! state update :section/posts #(-> (assoc % :total total) @@ -370,6 +370,24 @@ [:div [:strong "Unsupported attachment"] [debug attachment]]))) + +(comment + ; query current results + (-> @state :section/posts :displayed-posts) + + ; run and time a query on the database + (do + (js/console.log :start (.toISOString (js/Date.))) + (. (->> (db/open-cursor ::db/posts ::db/all) + (db/transduce-cursor (comp (mapcat (j/get :tags)) + (map (j/get :name)) + (filter #(= "typescript" %)) + (take 50)) conj (sorted-set))) + (then (fn [result] + (js/console.log :end (.toISOString (js/Date.))) + (js/console.log :accts result))))) + ) + (defn post [{:keys [post]}] ; TODO: handle (:sensitive post) [:article.post @@ -432,11 +450,11 @@ "Returns a promise which resolves to the smallest or largest internal post id. This is useful to continue interrupted paginated requests." [max-or-min] - (->> (db/open-cursor! ::db/posts ::db/all {:index ::db/post-internal-id - :direction (if (= max-or-min :min) :asc :desc)}) + (->> (db/open-cursor ::db/posts ::db/all {:index ::db/post-internal-id + :direction (if (= max-or-min :min) :asc :desc)}) (db/first-result (keep (j/get :internal_id))))) -(defn fetch-more-posts! [e] +(defn- fetch-more-posts! [e] (.preventDefault e) (. (promise-all [(fetch-application-settings) (internal-post-id :min) (internal-post-id :max)]) (then (fn [[application min-id max-id]] @@ -535,7 +553,7 @@ (defn- convert-internal-ids! [] ; TODO figure out when we can remove this again - (. (->> (db/open-cursor! ::db/posts ::db/all) + (. (->> (db/open-cursor ::db/posts ::db/all) (db/transduce-cursor (filter (comp string? (j/get :internal_id))))) (then (fn [rows] (doseq [row rows] diff --git a/src/computersandblues/lodestone/database.cljs b/src/computersandblues/lodestone/database.cljs index 05fcc60..406c82a 100644 --- a/src/computersandblues/lodestone/database.cljs +++ b/src/computersandblues/lodestone/database.cljs @@ -1,4 +1,5 @@ -(ns computersandblues.lodestone.database) +(ns computersandblues.lodestone.database + (:refer-clojure :exclude [count get])) (defonce +db+ (atom nil)) @@ -72,15 +73,15 @@ request (.put store (clj->js object))] (promisify request)))) -(defn get! - ([store-id k] (get! @+db+ store-id k)) +(defn get + ([store-id k] (get @+db+ store-id k)) ([db store-id k] (let [store (open-store db store-id "readonly") request (.get store k)] (promisify request)))) -(defn get-all! - ([store-id key-range] (get-all! @+db+ store-id key-range)) +(defn get-all + ([store-id key-range] (get-all @+db+ store-id key-range)) ([db store-id key-range] (let [store (open-store db store-id "readonly") request (.getAll store key-range)] @@ -94,23 +95,23 @@ (promisify request)))) (defn delete! - ([store-id k] (get! @+db+ store-id k)) + ([store-id k] (get @+db+ store-id k)) ([db store-id k] (let [store (open-store db store-id "readwrite") request (.delete store k)] (promisify request)))) -(defn count! - ([store-id] (count! @+db+ store-id)) +(defn count + ([store-id] (count @+db+ store-id)) ([db store-id] (let [store (open-store db store-id "readonly") request (.count store)] (promisify request)))) -(defn open-cursor! - ([store-id key-range] (open-cursor! @+db+ store-id key-range {:direction :asc})) - ([store-id key-range opts] (open-cursor! @+db+ store-id key-range opts)) +(defn open-cursor + ([store-id key-range] (open-cursor @+db+ store-id key-range {:direction :asc})) + ([store-id key-range opts] (open-cursor @+db+ store-id key-range opts)) ([db store-id key-range {:keys [direction index]}] (let [store (open-store db store-id "readonly") key-range (if (= key-range ::all) @@ -161,13 +162,13 @@ (resolve @result))))))))) (defn first-result [xform cursor-req] - (transduce-cursor (comp xform (take 1)) (fn [_ x] x) cursor-req)) + (transduce-cursor (comp xform (take 1)) (fn [_ x] x) nil cursor-req)) (comment (let [re (js/RegExp. "user" "i")] (do (print "starting…" (js/Date.)) - (-> (open-cursor! ::posts ::all) + (-> (open-cursor ::posts ::all) (transduce-cursor (comp (filter #(re-find re (.-content %))) (take 50) (map #(js->clj % :keywordize-keys true)))) From a02c33c620dc350f5f98776ea9aff99f86e8ae92 Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 13:39:26 +0100 Subject: [PATCH 4/6] Clean up database calls --- src/computersandblues/lodestone/app.cljs | 57 ++++++++++--------- src/computersandblues/lodestone/database.cljs | 11 +++- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index 6e096ff..385331f 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -160,8 +160,9 @@ (declare fetch-posts!) (defn- fetch-application-settings [] - (->> (db/open-cursor ::db/application ::db/all) - (db/first-result (map #(js->clj % :keywordize-keys true))))) + (let [cursor (db/open-cursor ::db/application ::db/all)] + (. (db/first-result cursor) + (then #(js->clj % :keywordize-keys true))))) (defn setup-application! "Handles Mastodon application setup on the client side" @@ -287,29 +288,32 @@ (defn- refresh-displayed-posts! [{:keys [per-page query]}] - (let [; this `xform` is responsible for filtering and building the final list - ; of results by iterating through the posts in the database. + (let [; this `xform` is responsible for filtering and building the list of + ; results that is displayed to the user. it iterates through all stored + ; posts in the database and returns a result that will be rendered + ; by the `post` component below. xform (comp (filter (query->matching-fn query)) (take per-page) (map #(js->clj % :keywordize-keys true))) + posts-cursor (db/open-cursor ::db/posts + ::db/all + {:index ::db/post-created-at + :direction :desc}) refresh-id (now)] (swap! state update-in [:section/posts :loading] conj refresh-id) (. (promise-all [(db/count ::db/posts) - (->> (db/open-cursor ::db/posts ::db/all - {:index ::db/post-created-at - :direction :desc}) - (db/transduce-cursor xform))]) + (db/transduce-cursor xform posts-cursor)]) (then (fn [[total displayed-posts]] (swap! state update :section/posts #(-> (assoc % :total total) (assoc :displayed-posts displayed-posts) (update :loading disj refresh-id)))))))) +(def debounced-refresh! (debounce 48 refresh-displayed-posts!)) + ; we use reagent's machinery below to define a callback that runs whenever the ; values change that serve as input to the current search reults. -(def debounced-refresh! (debounce 48 refresh-displayed-posts!)) - (def search-result-inputs (r/reaction (select-keys (:section/posts @state) [:query :per-page :last-update]))) (defonce update-search-results (r/track! (fn [] @@ -378,14 +382,15 @@ ; run and time a query on the database (do (js/console.log :start (.toISOString (js/Date.))) - (. (->> (db/open-cursor ::db/posts ::db/all) - (db/transduce-cursor (comp (mapcat (j/get :tags)) - (map (j/get :name)) - (filter #(= "typescript" %)) - (take 50)) conj (sorted-set))) - (then (fn [result] - (js/console.log :end (.toISOString (js/Date.))) - (js/console.log :accts result))))) + (let [xform (comp (mapcat (j/get :tags)) + #_(filter #(= "typescript" (j/get % :name))) + (take 10)) + posts-cursor (db/open-cursor ::db/posts ::db/all)] + (.. (db/transduce-cursor xform conj! (transient #{}) posts-cursor) + (then persistent!) + (then (fn [result] + (js/console.log :end (.toISOString (js/Date.))) + (js/console.log :accts result)))))) ) (defn post [{:keys [post]}] @@ -450,9 +455,9 @@ "Returns a promise which resolves to the smallest or largest internal post id. This is useful to continue interrupted paginated requests." [max-or-min] - (->> (db/open-cursor ::db/posts ::db/all {:index ::db/post-internal-id - :direction (if (= max-or-min :min) :asc :desc)}) - (db/first-result (keep (j/get :internal_id))))) + (let [posts-cursor (db/open-cursor ::db/posts ::db/all {:index ::db/post-internal-id + :direction (if (= max-or-min :min) :asc :desc)})] + (db/first-result (keep (j/get :internal_id)) posts-cursor))) (defn- fetch-more-posts! [e] (.preventDefault e) @@ -553,11 +558,11 @@ (defn- convert-internal-ids! [] ; TODO figure out when we can remove this again - (. (->> (db/open-cursor ::db/posts ::db/all) - (db/transduce-cursor (filter (comp string? (j/get :internal_id))))) - (then (fn [rows] - (doseq [row rows] - (db/put! ::db/posts (j/update! row :internal_id parse-long))))))) + (let [cursor (db/open-cursor ::db/posts ::db/all) + result (db/transduce-cursor (filter (comp string? (j/get :internal_id))) cursor)] + (.then result (fn [rows] + (doseq [row rows] + (db/put! ::db/posts (j/update! row :internal_id parse-long))))))) ;; go go go diff --git a/src/computersandblues/lodestone/database.cljs b/src/computersandblues/lodestone/database.cljs index 406c82a..7f2beba 100644 --- a/src/computersandblues/lodestone/database.cljs +++ b/src/computersandblues/lodestone/database.cljs @@ -161,8 +161,15 @@ (.continue cursor)))) (resolve @result))))))))) -(defn first-result [xform cursor-req] - (transduce-cursor (comp xform (take 1)) (fn [_ x] x) nil cursor-req)) +(defn first-result + "Given a cursor, will return a promise that resolves to the first result. + + Optionally takes an `xform` that can be used to filter or transform values + returned by the cursor." + ([cursor-req] + (first-result (map identity) cursor-req)) + ([xform cursor-req] + (transduce-cursor (comp xform (take 1)) (fn [_ x] x) nil cursor-req))) (comment From bddb3b97fa827a67bf1708f38199b3562deb1edd Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 13:47:19 +0100 Subject: [PATCH 5/6] Handle attachments with height < 320px --- public/index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 5742287..b07109c 100644 --- a/public/index.html +++ b/public/index.html @@ -228,13 +228,18 @@ section.posts .post .attachments { margin-top: 16px; - height: 320px; + max-height: 320px; display: flex; align-items: flex-start; max-width: 100%; overflow: auto; } + section.posts .post .attachments img, + section.posts .post .attachments video { + max-height: 320px; + } + section.posts .post .controls { margin: 24px 0 0; padding: 0; From 58d709f7be1272d9d9bb545a13b5d45834275fd6 Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 13:59:09 +0100 Subject: [PATCH 6/6] Fix attachment reusal bug The way that the atom was sometimes dereferenced and sometimes not caused weird re-rendering artifacts, where sometimes attachment would show up below posts that they actually did not belong to. Refactoring all of the different cases to be handled by separate functions seems to solve this. --- src/computersandblues/lodestone/app.cljs | 37 +++++++++++++++--------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index 385331f..3561957 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -353,24 +353,33 @@ (interleave (repeat ", ")) (drop 1))]) +(defn img-attachment [{:keys [attachment preview-url]}] + [:img {:src preview-url + :alt (:description attachment) + :loading "lazy"}]) + +(defn video-attachment [{:keys [attachment remote-url ext]}] + [:video {:controls true} + [:source {:type (str "video/" ext) :src remote-url}] + [:a {:href (:remote_url attachment)} (str "Original video at " (:remote_url attachment))]]) + +(defn gifv-attachment [{:keys [attachment remote-url ext]}] + (let [autoplay (r/atom false) + toggle-autoplay #(swap! autoplay not)] + (fn [] + [:video {:loop true :autoplay @autoplay :muted true :on-pointer-enter toggle-autoplay} + [:source {:type (str "video/" ext) :src remote-url}] + [:a {:href (:remote_url attachment)} (str "Original video at " (:remote_url attachment))]]))) + (defn attachment [{:keys [attachment]}] (let [preview-url (or (:preview_remote_url attachment) (:preview_url attachment)) remote-url (or (:remote_url attachment) (:url attachment)) - ext (last (str/split remote-url #"\."))] + ext (last (str/split remote-url #"\.")) + props {:attachment attachment :preview-url preview-url :remote-url remote-url :ext ext}] (case (:type attachment) - "image" [:img {:src preview-url - :srcset (str preview-url ", " remote-url) - :alt (:description attachment) - :loading "lazy"}] - "video" [:video {:controls true} - [:source {:type (str "video/" ext) :src remote-url}] - [:a {:href (:remote_url attachment)} (str "Original video at " (:remote_url attachment))]] - "gifv" (let [autoplay (r/atom false) - toggle-autoplay #(swap! autoplay not)] - (fn [] - [:video {:loop true :autoplay @autoplay :muted true :on-pointer-enter toggle-autoplay} - [:source {:type (str "video/" ext) :src remote-url}] - [:a {:href (:remote_url attachment)} (str "Original video at " (:remote_url attachment))]])) + "image" [img-attachment props] + "video" [video-attachment props] + "gifv" [gifv-attachment props] [:div [:strong "Unsupported attachment"] [debug attachment]])))