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`
This commit is contained in:
Icaro Motta 2023-04-13 10:52:27 -03:00 committed by GitHub
parent 270f5ab258
commit d902fb10d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 1 deletions

View File

@ -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))))

View File

@ -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})

View File

@ -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])

View File

@ -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)

View File

@ -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]

View File

@ -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}]])

View File

@ -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}

View File

@ -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))