From d902fb10d1f53f6ecc32ef594336db1c10152b32 Mon Sep 17 00:00:00 2001 From: Icaro Motta Date: Thu, 13 Apr 2023 10:52:27 -0300 Subject: [PATCH] New component - URL Preview List (#15620) Implements the URL Preview List component. Fixes https://github.com/status-im/status-mobile/issues/15617 Notes ===== The quo component view implements the pattern recently described by @ulisesmac. See his great explanation here: https://github.com/status-im/status-mobile/issues/15552#issuecomment-1492590074. Steps to test: Go to `Quo2.0 Preview` > `links` > `url-preview-list` --- .../url_preview_list/component_spec.cljs | 49 +++++++++++ .../links/url_preview_list/style.cljs | 8 ++ .../links/url_preview_list/view.cljs | 86 +++++++++++++++++++ src/quo2/core.cljs | 2 + src/quo2/core_spec.cljs | 1 + .../quo_preview/links/url_preview_list.cljs | 47 ++++++++++ src/status_im2/contexts/quo_preview/main.cljs | 6 +- src/test_helpers/component.cljs | 6 ++ 8 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/quo2/components/links/url_preview_list/component_spec.cljs create mode 100644 src/quo2/components/links/url_preview_list/style.cljs create mode 100644 src/quo2/components/links/url_preview_list/view.cljs create mode 100644 src/status_im2/contexts/quo_preview/links/url_preview_list.cljs diff --git a/src/quo2/components/links/url_preview_list/component_spec.cljs b/src/quo2/components/links/url_preview_list/component_spec.cljs new file mode 100644 index 0000000000..0bb3176114 --- /dev/null +++ b/src/quo2/components/links/url_preview_list/component_spec.cljs @@ -0,0 +1,49 @@ +(ns quo2.components.links.url-preview-list.component-spec + (:require + [oops.core :as oops] + [quo2.components.links.url-preview-list.view :as view] + [test-helpers.component :as h])) + +(def previews + (->> (range 3) + (map inc) + (mapv (fn [index] + {:title (str "Title " index) + :body (str "status.im." index) + :loading? false + :url (str "status.im." index)})))) + +(h/describe "Links - URL Preview List" + (h/test "default render" + (h/render [view/view + {:data previews + :key-fn :url + :horizontal-spacing 10}]) + (-> (count (h/query-all-by-label-text :url-preview)) + (h/expect) + (.toEqual 3)) + + (-> (map #(oops/oget % "props.children") + (h/query-all-by-label-text :title)) + (clj->js) + (h/expect) + (.toStrictEqual #js ["Title 1" "Title 2" "Title 3"]))) + + (h/test "on-clear event is individually handled by each preview" + (let [on-clear (h/mock-fn)] + (h/render [view/view + {:data previews + :key-fn :url + :on-clear on-clear}]) + (h/fire-event :press (first (h/get-all-by-label-text :button-clear-preview))) + (h/fire-event :press (second (h/get-all-by-label-text :button-clear-preview))) + (h/was-called-times on-clear 2))) + + (h/test "previews have separate loading states" + (h/render [view/view + {:data (assoc-in previews [1 :loading?] true) + :key-fn :url}]) + (h/is-truthy (h/get-by-label-text :url-preview-loading)) + (-> (count (h/query-all-by-label-text :url-preview)) + (h/expect) + (.toEqual 2)))) diff --git a/src/quo2/components/links/url_preview_list/style.cljs b/src/quo2/components/links/url_preview_list/style.cljs new file mode 100644 index 0000000000..a99036f904 --- /dev/null +++ b/src/quo2/components/links/url_preview_list/style.cljs @@ -0,0 +1,8 @@ +(ns quo2.components.links.url-preview-list.style) + +(def url-preview-gap 12) + +(def url-preview-separator + {:width url-preview-gap + :padding-top 12 + :padding-bottom 8}) diff --git a/src/quo2/components/links/url_preview_list/view.cljs b/src/quo2/components/links/url_preview_list/view.cljs new file mode 100644 index 0000000000..f9d4ab589d --- /dev/null +++ b/src/quo2/components/links/url_preview_list/view.cljs @@ -0,0 +1,86 @@ +(ns quo2.components.links.url-preview-list.view + (:require + [oops.core :as oops] + [quo2.components.links.url-preview-list.style :as style] + [quo2.components.links.url-preview.view :as url-preview] + [react-native.core :as rn] + [reagent.core :as reagent])) + +(defn- use-scroll-to-last-item + [flat-list-ref item-count item-width] + (rn/use-effect + (fn [] + (when (and (pos? item-count) (pos? item-width)) + ;; We use a delay because calling `scrollToOffset` without a delay does + ;; nothing while the flatlist is still rendering its children. + ;; `scrollToEnd` doesn't work because it positions the item off-center + ;; and there's no argument to offset it. + (let [timer-id (js/setTimeout + (fn [] + (when (and @flat-list-ref (pos? item-count)) + (.scrollToOffset ^js @flat-list-ref + #js + {:animated true + :offset (* (+ item-width style/url-preview-gap) + (max 0 (dec item-count)))}))) + 25)] + (fn [] + (js/clearTimeout timer-id))))) + [item-count item-width])) + +(defn- separator + [] + [rn/view {:style style/url-preview-separator}]) + +(defn- item-component + [{:keys [title body loading? logo]} _ _ + {:keys [width on-clear loading-message container-style]}] + [url-preview/view + {:logo logo + :title title + :body body + :loading? loading? + :loading-message loading-message + :on-clear on-clear + :container-style (merge container-style {:width width})}]) + +(defn- calculate-width + [preview-width horizontal-spacing ^js e] + (reset! preview-width + (- (oops/oget e "nativeEvent.layout.width") + (* 2 horizontal-spacing)))) + +(defn- view-component + [] + (let [preview-width (reagent/atom 0) + flat-list-ref (atom nil)] + (fn [{:keys [data key-fn horizontal-spacing on-clear loading-message + container-style container-style-item]}] + (use-scroll-to-last-item flat-list-ref (count data) @preview-width) + ;; We need to use a wrapping view expanded to 100% instead of "flex 1", + ;; otherwise `on-layout` will be triggered multiple times as the flat list + ;; renders its children. + [rn/view + {:style (merge container-style {:width "100%"}) + :accessibility-label :url-preview-list} + [rn/flat-list + {:ref #(reset! flat-list-ref %) + :key-fn key-fn + :on-layout #(calculate-width preview-width horizontal-spacing %) + :horizontal true + :deceleration-rate :fast + :on-scroll-to-index-failed identity + :content-container-style {:padding-horizontal horizontal-spacing} + :separator [separator] + :snap-to-interval (+ @preview-width style/url-preview-gap) + :shows-horizontal-scroll-indicator false + :data data + :render-fn item-component + :render-data {:width @preview-width + :on-clear on-clear + :loading-message loading-message + :container-style container-style-item}}]]))) + +(defn view + [props] + [:f> view-component props]) diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index 8610823c8f..0c7a0db732 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -34,6 +34,7 @@ quo2.components.inputs.title-input.view quo2.components.inputs.profile-input.view quo2.components.links.url-preview.view + quo2.components.links.url-preview-list.view quo2.components.list-items.channel quo2.components.list-items.menu-item quo2.components.list-items.preview-list @@ -199,3 +200,4 @@ ;;;; LINKS (def url-preview quo2.components.links.url-preview.view/view) +(def url-preview-list quo2.components.links.url-preview-list.view/view) diff --git a/src/quo2/core_spec.cljs b/src/quo2/core_spec.cljs index 032264a147..334d519c40 100644 --- a/src/quo2/core_spec.cljs +++ b/src/quo2/core_spec.cljs @@ -13,6 +13,7 @@ [quo2.components.inputs.input.component-spec] [quo2.components.inputs.profile-input.component-spec] [quo2.components.inputs.title-input.component-spec] + [quo2.components.links.url-preview-list.component-spec] [quo2.components.links.url-preview.component-spec] [quo2.components.markdown.--tests--.text-component-spec] [quo2.components.onboarding.small-option-card.component-spec] diff --git a/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs b/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs new file mode 100644 index 0000000000..840307c34d --- /dev/null +++ b/src/status_im2/contexts/quo_preview/links/url_preview_list.cljs @@ -0,0 +1,47 @@ +(ns status-im2.contexts.quo-preview.links.url-preview-list + (:require + [quo2.core :as quo] + [quo2.foundations.colors :as colors] + [react-native.core :as rn] + [reagent.core :as reagent] + [status-im2.common.resources :as resources] + [status-im2.contexts.quo-preview.preview :as preview] + utils.number)) + +(def descriptor + [{:label "Number of previews" + :key :previews-length + :type :text}]) + +(defn cool-preview + [] + (let [state (reagent/atom {:previews-length "3"})] + (fn [] + (let [previews-length (min 6 (utils.number/parse-int (:previews-length @state)))] + [rn/view {:style {:padding-bottom 150}} + [preview/customizer state descriptor] + [rn/view + {:style {:align-items :center + :margin-top 50}} + [quo/url-preview-list + {:horizontal-spacing 20 + :on-clear #(js/alert "Clear button pressed") + :key-fn :url + :data (for [index (range previews-length) + :let [index (inc index)]] + {:title (str "Title " index) + :body (str "status.im." index) + :logo (resources/get-mock-image :status-logo) + :loading? false + :url (str "status.im." index)})}]]])))) + +(defn preview + [] + [rn/view + {:style {:background-color (colors/theme-colors colors/white colors/neutral-95) + :flex 1}} + [rn/flat-list + {:flex 1 + :keyboard-should-persist-taps :always + :header [cool-preview] + :key-fn str}]]) diff --git a/src/status_im2/contexts/quo_preview/main.cljs b/src/status_im2/contexts/quo_preview/main.cljs index 9ad7d2c91b..c1c76dbbb3 100644 --- a/src/status_im2/contexts/quo_preview/main.cljs +++ b/src/status_im2/contexts/quo_preview/main.cljs @@ -40,6 +40,7 @@ [status-im2.contexts.quo-preview.inputs.profile-input :as profile-input] [status-im2.contexts.quo-preview.inputs.title-input :as title-input] [status-im2.contexts.quo-preview.links.url-preview :as url-preview] + [status-im2.contexts.quo-preview.links.url-preview-list :as url-preview-list] [status-im2.contexts.quo-preview.list-items.channel :as channel] [status-im2.contexts.quo-preview.list-items.preview-lists :as preview-lists] [status-im2.contexts.quo-preview.markdown.text :as text] @@ -183,7 +184,10 @@ :component title-input/preview-title-input}] :links [{:name :url-preview :options {:insets {:top? true}} - :component url-preview/preview}] + :component url-preview/preview} + {:name :url-preview-list + :options {:insets {:top? true}} + :component url-preview-list/preview}] :list-items [{:name :channel :insets {:top false} :component channel/preview-channel} diff --git a/src/test_helpers/component.cljs b/src/test_helpers/component.cljs index 9777b6140d..a48d3d2392 100644 --- a/src/test_helpers/component.cljs +++ b/src/test_helpers/component.cljs @@ -61,6 +61,8 @@ "Pretty-print to STDOUT the current component tree." (with-node-or-screen :debug)) +(def within rtl/within) + (defn fire-event ([event-name node] (fire-event event-name node nil)) @@ -188,3 +190,7 @@ (defn was-called [mock] (.toHaveBeenCalled (js/expect mock))) + +(defn was-called-times + [^js mock number-of-times] + (.toHaveBeenCalledTimes (js/expect mock) number-of-times))