diff --git a/resources/images/icons/close20@2x.png b/resources/images/icons/close20@2x.png new file mode 100644 index 0000000000..139f5cfd3a Binary files /dev/null and b/resources/images/icons/close20@2x.png differ diff --git a/resources/images/icons/close20@3x.png b/resources/images/icons/close20@3x.png new file mode 100644 index 0000000000..44e11166d8 Binary files /dev/null and b/resources/images/icons/close20@3x.png differ diff --git a/resources/images/icons/unread20@2x.png b/resources/images/icons/unread20@2x.png new file mode 100644 index 0000000000..84527f2da4 Binary files /dev/null and b/resources/images/icons/unread20@2x.png differ diff --git a/resources/images/icons/unread20@3x.png b/resources/images/icons/unread20@3x.png new file mode 100644 index 0000000000..118d80db4c Binary files /dev/null and b/resources/images/icons/unread20@3x.png differ diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index 6971a2102c..edf7c5c98c 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -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 diff --git a/src/quo2/components/tabs/tabs.cljs b/src/quo2/components/tabs/tabs.cljs index 8131793ef4..e0c63c3c4a 100644 --- a/src/quo2/components/tabs/tabs.cljs +++ b/src/quo2/components/tabs/tabs.cljs @@ -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]])])))) \ No newline at end of file + {: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]])})]))))) diff --git a/src/status_im/subs/activity_center.cljs b/src/status_im/subs/activity_center.cljs index 53dbc1a77b..5b7e3a5559 100644 --- a/src/status_im/subs/activity_center.cljs +++ b/src/status_im/subs/activity_center.cljs @@ -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))))) \ No newline at end of file + supported-notifications))))) diff --git a/src/status_im/ui/components/react.cljs b/src/status_im/ui/components/react.cljs index 75fcbf580b..2414c65934 100644 --- a/src/status_im/ui/components/react.cljs +++ b/src/status_im/ui/components/react.cljs @@ -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] diff --git a/src/status_im/ui/screens/activity_center/views.cljs b/src/status_im/ui/screens/activity_center/views.cljs index c286f62919..cf9d9657ca 100644 --- a/src/status_im/ui/screens/activity_center/views.cljs +++ b/src/status_im/ui/screens/activity_center/views.cljs @@ -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 [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 (evt [:activity-center.notifications/fetch-next-page]) :render-fn render-notification}])) +(defn filter-selector-read [] + (let [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]]]))})) diff --git a/src/status_im/utils/number.cljs b/src/status_im/utils/number.cljs new file mode 100644 index 0000000000..4b268fca7e --- /dev/null +++ b/src/status_im/utils/number.cljs @@ -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))) diff --git a/translations/en.json b/translations/en.json index fc8082c6ce..5fd23aea3e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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" }