parent
390ac858dc
commit
73983b2abd
Binary file not shown.
After Width: | Height: | Size: 454 B |
Binary file not shown.
After Width: | Height: | Size: 673 B |
Binary file not shown.
After Width: | Height: | Size: 249 B |
Binary file not shown.
After Width: | Height: | Size: 282 B |
|
@ -248,6 +248,8 @@
|
|||
|
||||
(def react-native-gradien #js {:default #js {}})
|
||||
|
||||
(def masked-view #js {:default #js {}})
|
||||
|
||||
(def react-native-permissions #js {:default #js {}})
|
||||
|
||||
(def push-notification-ios #js {:default #js {:abandonPermissions identity}})
|
||||
|
@ -308,6 +310,7 @@
|
|||
"react-native-device-info" react-native-device-info
|
||||
"react-native-push-notification" react-native-push-notification
|
||||
"react-native-linear-gradient" react-native-gradien
|
||||
"@react-native-community/masked-view" masked-view
|
||||
"react-native-blob-util" react-native-blob-util
|
||||
"react-native-navigation" react-native-navigation
|
||||
"@react-native-community/push-notification-ios" push-notification-ios
|
||||
|
|
|
@ -1,21 +1,139 @@
|
|||
(ns quo2.components.tabs.tabs
|
||||
(:require [reagent.core :as reagent]
|
||||
(:require [oops.core :refer [oget]]
|
||||
[quo.react-native :as rn]
|
||||
[quo2.components.tabs.tab :as tab]))
|
||||
[quo2.components.tabs.tab :as tab]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.ui.components.react :as react]
|
||||
[status-im.utils.number :as number-utils]))
|
||||
|
||||
(def default-tab-size 32)
|
||||
|
||||
(defn tabs [{:keys [default-active on-change]}]
|
||||
(let [active-tab-id (reagent/atom default-active)]
|
||||
(fn [{:keys [data size] :or {size 32}}]
|
||||
(fn [{:keys [data size] :or {size default-tab-size}}]
|
||||
(let [active-id @active-tab-id]
|
||||
[rn/view {:flex-direction :row}
|
||||
(for [{:keys [label id]} data]
|
||||
^{:key id}
|
||||
[rn/view {:margin-right (if (= size 32) 12 8)}
|
||||
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)}}
|
||||
[tab/tab
|
||||
{:id id
|
||||
:size size
|
||||
:active (= id active-id)
|
||||
:on-press #(do (reset! active-tab-id %)
|
||||
(when on-change
|
||||
(on-change %)))}
|
||||
label]])]))))
|
||||
{:id id
|
||||
:size size
|
||||
:active (= id active-id)
|
||||
:on-press (fn [^js press-event id]
|
||||
(reset! active-tab-id id)
|
||||
(when on-change
|
||||
(on-change press-event id)))}
|
||||
label]])]))))
|
||||
|
||||
(defn- calculate-fade-end-percentage
|
||||
[{:keys [offset-x content-width layout-width max-fade-percentage]}]
|
||||
(let [fade-percentage (max max-fade-percentage
|
||||
(/ (+ layout-width offset-x)
|
||||
content-width))]
|
||||
;; Truncate to avoid unnecessary rendering.
|
||||
(if (> fade-percentage 0.99)
|
||||
0.99
|
||||
(number-utils/naive-round fade-percentage 2))))
|
||||
|
||||
(defn scrollable-tabs
|
||||
"Just like the component `tabs`, displays horizontally scrollable tabs with
|
||||
extra options to control if/how the end of the scroll view fades.
|
||||
|
||||
Tabs are rendered using ReactNative's FlatList, which offers the convenient
|
||||
`scrollToIndex` method. FlatList accepts VirtualizedList and ScrollView props,
|
||||
and so does this component.
|
||||
|
||||
Usage:
|
||||
[tabs/scrollable-tabs
|
||||
{:scroll-on-press? true
|
||||
:fade-end? true
|
||||
:on-change #(...)
|
||||
:default-active :tab-a
|
||||
:data [{:id :tab-a :label \"Tab A\"}
|
||||
{:id :tab-b :label \"Tab B\"}]}]]
|
||||
|
||||
Opts:
|
||||
- `size` number
|
||||
- `scroll-on-press?` When non-nil, clicking on a tab centers it the middle
|
||||
(with animation enabled).
|
||||
- `fade-end?` When non-nil, causes the end of the scrollable view to fade out.
|
||||
- `fade-end-percentage` Percentage where fading starts relative to the total
|
||||
layout width of the `flat-list` data."
|
||||
[{:keys [default-active fade-end-percentage]
|
||||
:or {fade-end-percentage 0.8}}]
|
||||
(let [active-tab-id (reagent/atom default-active)
|
||||
fading (reagent/atom {:fade-end-percentage fade-end-percentage})
|
||||
flat-list-ref (atom nil)]
|
||||
(fn [{:keys [data
|
||||
fade-end-percentage
|
||||
fade-end?
|
||||
on-change
|
||||
on-scroll
|
||||
scroll-event-throttle
|
||||
scroll-on-press?
|
||||
size]
|
||||
:or {fade-end-percentage fade-end-percentage
|
||||
fade-end? false
|
||||
scroll-event-throttle 64
|
||||
scroll-on-press? false
|
||||
size default-tab-size}
|
||||
:as props}]
|
||||
(let [maybe-mask-wrapper (if fade-end?
|
||||
[react/masked-view
|
||||
{:mask-element (reagent/as-element
|
||||
[react/linear-gradient {:colors ["black" "transparent"]
|
||||
:locations [(@fading :fade-end-percentage) 1]
|
||||
:start {:x 0 :y 0}
|
||||
:end {:x 1 :y 0}
|
||||
:pointer-events :none
|
||||
:style {:width "100%"
|
||||
:height "100%"}}])}]
|
||||
[:<>])]
|
||||
(conj maybe-mask-wrapper
|
||||
[rn/flat-list
|
||||
(merge (dissoc props
|
||||
:default-active
|
||||
:fade-end-percentage
|
||||
:fade-end?
|
||||
:on-change
|
||||
:scroll-on-press?
|
||||
:size)
|
||||
{:ref (partial reset! flat-list-ref)
|
||||
:extra-data (str @active-tab-id)
|
||||
:horizontal true
|
||||
:scroll-event-throttle scroll-event-throttle
|
||||
:shows-horizontal-scroll-indicator false
|
||||
:data data
|
||||
:key-fn (comp str :id)
|
||||
:on-scroll (fn [^js e]
|
||||
(when fade-end?
|
||||
(let [offset-x (oget e "nativeEvent.contentOffset.x")
|
||||
content-width (oget e "nativeEvent.contentSize.width")
|
||||
layout-width (oget e "nativeEvent.layoutMeasurement.width")
|
||||
new-percentage (calculate-fade-end-percentage {:offset-x offset-x
|
||||
:content-width content-width
|
||||
:layout-width layout-width
|
||||
:max-fade-percentage fade-end-percentage})]
|
||||
;; Avoid unnecessary re-rendering.
|
||||
(when (not= new-percentage (@fading :fade-end-percentage))
|
||||
(swap! fading assoc :fade-end-percentage new-percentage))))
|
||||
(when on-scroll
|
||||
(on-scroll e)))
|
||||
:render-fn (fn [{:keys [id label]} index]
|
||||
[rn/view {:style {:margin-right (if (= size default-tab-size) 12 8)
|
||||
:padding-right (when (= index (dec (count data)))
|
||||
(get-in props [:style :padding-left]))}}
|
||||
[tab/tab {:id id
|
||||
:size size
|
||||
:active (= id @active-tab-id)
|
||||
:on-press (fn [id]
|
||||
(reset! active-tab-id id)
|
||||
(when scroll-on-press?
|
||||
(.scrollToIndex @flat-list-ref
|
||||
#js {:animated true
|
||||
:index index
|
||||
:viewPosition 0.5}))
|
||||
(when on-change
|
||||
(on-change id)))}
|
||||
label]])})])))))
|
||||
|
|
|
@ -20,13 +20,19 @@
|
|||
(fn [db]
|
||||
(get-in db [:activity-center :current-status-filter])))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:activity-center/status-filter-unread-enabled?
|
||||
:<- [:activity-center/current-status-filter]
|
||||
(fn [current-status-filter]
|
||||
(= :unread current-status-filter)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
:activity-center/notifications-per-read-status
|
||||
:<- [:activity-center/notifications-read]
|
||||
:<- [:activity-center/notifications-unread]
|
||||
:<- [:activity-center/current-status-filter]
|
||||
(fn [[notifications-read notifications-unread status-filter]]
|
||||
(if (= status-filter :unread)
|
||||
:<- [:activity-center/status-filter-unread-enabled?]
|
||||
(fn [[notifications-read notifications-unread unread-filter-enabled?]]
|
||||
(if unread-filter-enabled?
|
||||
notifications-unread
|
||||
notifications-read)))
|
||||
|
||||
|
@ -64,4 +70,4 @@
|
|||
(map #(assoc %
|
||||
:timestamp (or (:timestamp %) (:timestamp (or (:message %) (:last-message %))))
|
||||
:contact (multiaccounts/contact-by-identity contacts (get-in % [:message :from])))
|
||||
supported-notifications)))))
|
||||
supported-notifications)))))
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:refer (SafeAreaProvider SafeAreaInsetsContext)]
|
||||
["@react-native-community/clipboard" :default Clipboard]
|
||||
["react-native-linear-gradient" :default LinearGradient]
|
||||
["@react-native-community/masked-view" :default MaskedView]
|
||||
["react-native-navigation" :refer (Navigation)]
|
||||
["react-native-fast-image" :as FastImage]
|
||||
["@react-native-community/blur" :as blur])
|
||||
|
@ -40,6 +41,8 @@
|
|||
|
||||
(def linear-gradient (reagent/adapt-react-class LinearGradient))
|
||||
|
||||
(def masked-view (reagent/adapt-react-class MaskedView))
|
||||
|
||||
(def blur-view (reagent/adapt-react-class (.-BlurView blur)))
|
||||
|
||||
(defn valid-source? [source]
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
(:require [quo.components.animated.pressable :as animation]
|
||||
[quo.react-native :as rn]
|
||||
[quo2.components.buttons.button :as button]
|
||||
[quo2.components.markdown.text :as text]
|
||||
[quo2.components.notifications.activity-logs :as activity-logs]
|
||||
[quo2.components.tabs.tabs :as tabs]
|
||||
[quo2.components.tags.context-tags :as context-tags]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[reagent.core :as reagent]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.i18n.i18n :as i18n]
|
||||
[status-im.multiaccounts.core :as multiaccounts]
|
||||
[status-im.ui.components.topbar :as topbar]
|
||||
[status-im.utils.datetime :as datetime]
|
||||
[status-im.utils.handlers :refer [<sub >evt]]))
|
||||
|
||||
(defonce selected-activity-type
|
||||
(reagent/atom :activity-type/all))
|
||||
|
||||
(defn activity-title
|
||||
[{:keys [type]}]
|
||||
(case type
|
||||
|
@ -107,24 +111,64 @@
|
|||
(defn notifications-list
|
||||
[]
|
||||
(let [notifications (<sub [:activity-center/notifications-per-read-status])]
|
||||
[rn/flat-list {:style {:padding-horizontal 8}
|
||||
:data notifications
|
||||
[rn/flat-list {:data notifications
|
||||
:key-fn :id
|
||||
:on-end-reached #(>evt [:activity-center.notifications/fetch-next-page])
|
||||
:render-fn render-notification}]))
|
||||
|
||||
(defn filter-selector-read []
|
||||
(let [unread-filter-enabled? (<sub [:activity-center/status-filter-unread-enabled?])]
|
||||
;; TODO: Replace the button by a Filter Selector component once available for use.
|
||||
[button/button {:icon true
|
||||
:type (if unread-filter-enabled? :primary :outline)
|
||||
:size 32
|
||||
:on-press #(if unread-filter-enabled?
|
||||
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}])
|
||||
(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}]))}
|
||||
:unread]))
|
||||
|
||||
(defn activity-center []
|
||||
(reagent/create-class
|
||||
{:component-did-mount #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}])
|
||||
:reagent-render
|
||||
(fn []
|
||||
[:<>
|
||||
[topbar/topbar {:navigation {:on-press #(>evt [:navigate-back])}
|
||||
:title (i18n/label :t/activity)}]
|
||||
;; TODO(ilmotta): Temporary solution to switch between read/unread
|
||||
;; notifications while the Design team works on the mockups.
|
||||
[button/button {:on-press #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :unread}])}
|
||||
"Unread"]
|
||||
[button/button {:on-press #(>evt [:activity-center.notifications/fetch-first-page {:status-filter :read}])}
|
||||
"Read"]
|
||||
[notifications-list]])}))
|
||||
(let [screen-padding 20]
|
||||
[:<>
|
||||
[button/button {:icon true
|
||||
:type :grey
|
||||
:size 32
|
||||
:style {:margin-vertical 12
|
||||
:margin-left screen-padding}
|
||||
:on-press #(>evt [:navigate-back])}
|
||||
:close]
|
||||
[text/text {:size :heading-1
|
||||
:weight :semi-bold
|
||||
:style {:padding-horizontal screen-padding
|
||||
:padding-vertical 12}}
|
||||
(i18n/label :t/notifications)]
|
||||
[rn/view {:flex-direction :row
|
||||
:padding-vertical 12}
|
||||
[rn/view {:flex 1
|
||||
:align-self :stretch}
|
||||
[tabs/scrollable-tabs {:size 32
|
||||
:style {:padding-left screen-padding}
|
||||
:fade-end-percentage 0.79
|
||||
:scroll-on-press? true
|
||||
:fade-end? true
|
||||
:on-change (partial reset! selected-activity-type)
|
||||
:default-active :activity-type/all
|
||||
:data [{:id :activity-type/all :label (i18n/label :t/all)}
|
||||
{:id :activity-type/admin :label (i18n/label :t/admin)}
|
||||
{:id :activity-type/mention :label (i18n/label :t/mentions)}
|
||||
{:id :activity-type/reply :label (i18n/label :t/replies)}
|
||||
{:id :activity-type/contact-request :label (i18n/label :t/contact-requests)}
|
||||
{:id :activity-type/identity-verification :label (i18n/label :t/identity-verification)}
|
||||
{:id :activity-type/transaction :label (i18n/label :t/transactions)}
|
||||
{:id :activity-type/membership :label (i18n/label :t/membership)}
|
||||
{:id :activity-type/system :label (i18n/label :t/system)}]}]]
|
||||
[rn/view {:flex-grow 0
|
||||
:margin-left 16
|
||||
:padding-right screen-padding}
|
||||
[filter-selector-read]]]
|
||||
[rn/view {:padding-horizontal screen-padding}
|
||||
[notifications-list]]]))}))
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
(ns status-im.utils.number)
|
||||
|
||||
(defn naive-round
|
||||
"Quickly and naively round number `n` up to `decimal-places`.
|
||||
|
||||
Example usage: use it to avoid re-renders caused by floating-point number
|
||||
changes in Reagent atoms. Such numbers can be rounded up to a certain number
|
||||
of `decimal-places` in order to avoid re-rendering due to tiny fractional
|
||||
changes.
|
||||
|
||||
Don't use this function for arbitrary-precision arithmetic."
|
||||
[n decimal-places]
|
||||
(let [scale (Math/pow 10 decimal-places)]
|
||||
(/ (Math/round (* n scale))
|
||||
scale)))
|
|
@ -1762,6 +1762,7 @@
|
|||
"new-ui": "New UI",
|
||||
"send-contact-request-message": "To start a chat you need to become contacts",
|
||||
"contact-request": "Contact request",
|
||||
"contact-requests": "Contact Requests",
|
||||
"say-hi": "Say hi",
|
||||
"opened" : "Opened",
|
||||
"accepted": "Accepted",
|
||||
|
@ -1798,5 +1799,10 @@
|
|||
"edit-message": "Edit message",
|
||||
"save-image-library": "Save image to library",
|
||||
"share-image": "Share image",
|
||||
"see-sticker-set": "See the full sticker set"
|
||||
"see-sticker-set": "See the full sticker set",
|
||||
"mentions": "Mentions",
|
||||
"admin": "Admin",
|
||||
"replies": "Replies",
|
||||
"identity-verification": "Identity verification",
|
||||
"membership": "Membership"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue