From a9e4d2aa3852fd59a50f4210e7cc2e7907dd9b90 Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 00:20:02 +0100 Subject: [PATCH 1/3] Simplify and gather reactive refresh logic in one place --- src/computersandblues/lodestone/app.cljs | 87 +++++++++++++----------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/src/computersandblues/lodestone/app.cljs b/src/computersandblues/lodestone/app.cljs index 98b6a9d..b91fc8c 100644 --- a/src/computersandblues/lodestone/app.cljs +++ b/src/computersandblues/lodestone/app.cljs @@ -9,6 +9,7 @@ (def posts-init-state {:query nil + :last-update -1 ; TODO: pagination ; :page 0 ; :max-displayed-id nil @@ -42,6 +43,9 @@ (defn- promise-reject [val] (js/Promise.reject val)) +(defn- now [] + (js/Date.now)) + (defn- link-header "Given a JS `Response` object, will parse the `link` header and find a link of a given `link-type` if present. Useful for paginating API requests." @@ -152,7 +156,6 @@ (obtain-oauth-authorization-code! application)))))) (declare fetch-posts!) -(declare debounced-refresh!) (defn- fetch-application-settings [] (->> (db/open-cursor! ::db/application ::db/all) @@ -199,7 +202,7 @@ (if (zero? post-count) (fetch-posts! {:instance-url (:instance_url application) :bearer-token (:bearer_token application)}) - (debounced-refresh! (:section/posts @state)))))))) + (swap! state assoc-in [:section/posts :last-update] (now)))))))) ;;; views @@ -247,7 +250,7 @@ on-response on-response on-error on-error}}] ((fn paginate! [url] - (let [req-id (js/Date.now)] + (let [req-id (now)] (js/console.log :paginate! url :max-id max-id :min-id min-id) (swap! state update-in [:section/posts :loading] conj req-id) (-> (mastodon-request! {:url url :bearer-token bearer-token}) @@ -271,23 +274,50 @@ always prefer to call the most recent call to `f` as close to the delay as possible." [ms f] - (let [prev (volatile! (js/Date.now)) - scheduled (volatile! (js/setTimeout (fn dummy []) ms))] + (let [prev (volatile! (now))] (fn debounced-fn [& args] - (let [now (js/Date.now)] - (js/clearTimeout @scheduled) - (vreset! scheduled (js/setTimeout (fn [] - (vreset! prev (js/Date.now)) - (apply f args)) - (max 0 (- ms (- now @prev))))))))) + (when (< ms (- (now) @prev)) + (js/requestAnimationFrame #(apply f args))) + (vreset! prev (now))))) -(defn search [] +(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. + xform (comp + (filter (query->matching-fn query)) + (take per-page) + (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}) + (db/transduce-cursor xform))]) + (then (fn [[total displayed-posts]] + (swap! state update :section/posts #(-> (assoc % :total total) + (assoc :displayed-posts displayed-posts) + (update :loading disj refresh-id)))))))) + +; 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 [] + (let [inputs @search-result-inputs] + (debounced-refresh! inputs))))) + +(defn search [{:keys [query]}] [:input {:placeholder "Start typing to search…" :type "search" + :initial-value query :on-change (fn [e] (let [query (.. e -target -value)] - (swap! state assoc-in [:section/posts :query] (if (str/blank? query) nil query)) - (debounced-refresh! (:section/posts @state))))}]) + (swap! state assoc-in [:section/posts :query] (if (str/blank? query) nil query))))}]) (defn- debug "Implements a lazy pretty-printer for whatever is passed in as `obj`. The @@ -369,29 +399,6 @@ (js/console.log :logging args) (apply f args))))) -(defn- refresh-displayed-posts! - [posts-section] - (let [{:keys [per-page query]} posts-section - ; this `xform` is responsible for filtering and building the final list - ; of results by iterating through the posts in the database. - xform (comp - (filter (query->matching-fn query)) - (take per-page) - (map #(js->clj % :keywordize-keys true))) - refresh-id (js/Date.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))]) - (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 40 refresh-displayed-posts!)) - (defn- fetch-posts! [opts] (let [defaults {:max-id nil :on-response (fn [response] @@ -410,7 +417,7 @@ (.get url-params "min_id") (assoc :internal_id (parse-long (.get url-params "min_id"))))))) - (debounced-refresh! (:section/posts @state)))}] + (swap! state assoc-in [:section/posts :last-update] (now)))}] (paginate-posts! (merge defaults opts)))) (defn- internal-post-id @@ -452,7 +459,7 @@ [:path.arc {:d "M50,0 A50,50 180 0,1 100,50"}]])) (defn posts-section [{:keys [posts]}] - (let [{:keys [per-page query total displayed-posts loading]} posts + (let [{:keys [per-page query total displayed-posts loading query]} posts n-displayed (count displayed-posts)] [:section.posts [:h2 "Favorites"] @@ -465,7 +472,7 @@ " match" " matches")))))] [:section.search-form - [search] + [search {:query query}] [loading-indicator {:loading loading}] #_(cond (= api-state :loading) " …" (= api-state :error) " API Error!")] From 09407c3231365fa95e009b24531644700a322c7a Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 00:30:52 +0100 Subject: [PATCH 2/3] Place control buttons next to search input on large enough screens --- public/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/index.html b/public/index.html index 61db30f..e87f052 100644 --- a/public/index.html +++ b/public/index.html @@ -163,6 +163,12 @@ } @media screen and (min-width: 640px) { + section.posts .controls { + grid-template: + "a a" + "b c"; + } + section.posts .controls .post-info { grid-area: a; } From de2cf7f9c2f0033e79790bbbdc9eb974e7e4815f Mon Sep 17 00:00:00 2001 From: arne Date: Fri, 21 Nov 2025 00:33:12 +0100 Subject: [PATCH 3/3] Place `rsync` example into README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index da8b769..1766e45 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ After that npm run build ``` +You can upload these files to any static file server, for example using `rsync`: + +``` bash +rsync -rsvP --delete-after --exclude js/cljs-runtime/ public/ user@server:some/directory +``` + ## Development Just as when building a release, you need to ensure that JS dependencies are installed: