From d40f70dca4999a3914e100ea177e6bc57627adfc Mon Sep 17 00:00:00 2001 From: Parvesh Monu Date: Tue, 19 Dec 2023 16:01:13 +0530 Subject: [PATCH] Optimize chat screen navigation view (#18055) --- src/js/worklets/{ => chat}/lightbox.js | 0 src/js/worklets/chat/messages.js | 65 +++ src/mocks/js_dependencies.cljs | 3 +- src/quo/components/banners/banner/style.cljs | 2 - src/react_native/reanimated.cljs | 2 + .../contexts/chat/composer/utils.cljs | 24 +- .../contexts/chat/composer/view.cljs | 53 +- .../contexts/chat/lightbox/utils.cljs | 2 +- .../contexts/chat/messages/constants.cljs | 16 + .../contact_requests/bottom_drawer/style.cljs | 7 + .../view.cljs} | 5 +- .../contexts/chat/messages/list/style.cljs | 66 +-- .../contexts/chat/messages/list/view.cljs | 510 +++++++----------- .../chat/messages/navigation/style.cljs | 110 ++-- .../chat/messages/navigation/view.cljs | 243 ++++----- .../chat/messages/pin/banner/style.cljs | 42 +- .../chat/messages/pin/banner/view.cljs | 36 +- .../contexts/chat/messages/view.cljs | 121 ++--- .../contexts/chat/placeholder/style.cljs | 16 + .../contexts/chat/placeholder/view.cljs | 23 + src/utils/worklets/chat/lightbox.cljs | 7 + src/utils/worklets/chat/messages.cljs | 37 ++ src/utils/worklets/lightbox.cljs | 7 - 23 files changed, 658 insertions(+), 739 deletions(-) rename src/js/worklets/{ => chat}/lightbox.js (100%) create mode 100644 src/js/worklets/chat/messages.js create mode 100644 src/status_im2/contexts/chat/messages/constants.cljs create mode 100644 src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/style.cljs rename src/status_im2/contexts/chat/messages/contact_requests/{bottom_drawer.cljs => bottom_drawer/view.cljs} (92%) create mode 100644 src/status_im2/contexts/chat/placeholder/style.cljs create mode 100644 src/status_im2/contexts/chat/placeholder/view.cljs create mode 100644 src/utils/worklets/chat/lightbox.cljs create mode 100644 src/utils/worklets/chat/messages.cljs delete mode 100644 src/utils/worklets/lightbox.cljs diff --git a/src/js/worklets/lightbox.js b/src/js/worklets/chat/lightbox.js similarity index 100% rename from src/js/worklets/lightbox.js rename to src/js/worklets/chat/lightbox.js diff --git a/src/js/worklets/chat/messages.js b/src/js/worklets/chat/messages.js new file mode 100644 index 0000000000..16923bf551 --- /dev/null +++ b/src/js/worklets/chat/messages.js @@ -0,0 +1,65 @@ +import { useDerivedValue, withTiming, interpolate, useAnimatedScrollHandler, runOnJS } from 'react-native-reanimated'; + +export function navigationHeaderOpacity(distanceFromListTop, isAllLoaded, isCalculationsComplete, startPosition) { + return useDerivedValue(function () { + 'worklet'; + const isCalculationsCompleteValue = isCalculationsComplete.value; + if (distanceFromListTop.value < startPosition && isAllLoaded.value) { + return isCalculationsCompleteValue ? withTiming(0) : 0; + } else { + return isCalculationsCompleteValue ? withTiming(1) : 1; + } + }); +} + +export function navigationHeaderPosition(distanceFromListTop, isAllLoaded, topBarHeight, startPosition) { + return useDerivedValue(function () { + 'worklet'; + return distanceFromListTop.value < startPosition && isAllLoaded.value ? withTiming(topBarHeight) : withTiming(0); + }); +} + +export function interpolateNavigationViewOpacity(props) { + return useDerivedValue(function () { + 'worklet'; + const { + 'all-loaded?': isAllLoaded, + 'end-position': endPosition, + 'start-position': startPosition, + 'distance-from-list-top': distanceFromListTop, + } = props; + if (isAllLoaded.value) { + return interpolate(distanceFromListTop.value, [startPosition, endPosition], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + } else { + return 1; + } + }); +} + +export function messagesListOnScroll(distanceFromListTop, callback) { + return function (event) { + 'worklet'; + const currentY = event.contentOffset.y; + const layoutHeight = event.layoutMeasurement.height; + const contentSizeY = event.contentSize.height - layoutHeight; + distanceFromListTop.value = contentSizeY - currentY; + runOnJS(callback)(currentY, layoutHeight); + }; +} + +export function placeholderOpacity(isCalculationsComplete) { + return useDerivedValue(function () { + 'worklet'; + return isCalculationsComplete.value ? 0 : 1; + }); +} + +export function placeholderZIndex(isCalculationsComplete) { + return useDerivedValue(function () { + 'worklet'; + return isCalculationsComplete.value ? 0 : 2; + }); +} diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index 1246e23c05..3bce29b0cc 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -432,7 +432,8 @@ "../src/js/worklets/record_audio.js" #js {} "../src/js/worklets/scroll_view.js" #js {} "../src/js/worklets/onboarding_carousel.js" #js {} - "../src/js/worklets/lightbox.js" #js {} + "../src/js/worklets/chat/lightbox.js" #js {} + "../src/js/worklets/chat/messages.js" #js {} "../src/js/worklets/parallax.js" #js {} "../src/js/worklets/identifiers_highlighting.js" #js {} "./fleets.js" default-fleets diff --git a/src/quo/components/banners/banner/style.cljs b/src/quo/components/banners/banner/style.cljs index 273bcab2ab..de19d9f064 100644 --- a/src/quo/components/banners/banner/style.cljs +++ b/src/quo/components/banners/banner/style.cljs @@ -2,7 +2,6 @@ (:require [quo.foundations.colors :as colors])) - (def container {:height 40 :background-color (colors/custom-color :blue 50 20) @@ -25,4 +24,3 @@ [hide-pin?] {:flex (if hide-pin? 16 15) :margin-right 10}) - diff --git a/src/react_native/reanimated.cljs b/src/react_native/reanimated.cljs index 0c05fdc4cd..af732d86e0 100644 --- a/src/react_native/reanimated.cljs +++ b/src/react_native/reanimated.cljs @@ -19,6 +19,7 @@ SlideOutUp LinearTransition enableLayoutAnimations + useAnimatedScrollHandler runOnJS)] ["react-native-redash" :refer (withPause)] [react-native.flat-list :as rn-flat-list] @@ -55,6 +56,7 @@ ;; Hooks (def use-shared-value useSharedValue) (def use-animated-style useAnimatedStyle) +(def use-animated-scroll-handler useAnimatedScrollHandler) ;; Animations (def with-timing withTiming) diff --git a/src/status_im2/contexts/chat/composer/utils.cljs b/src/status_im2/contexts/chat/composer/utils.cljs index f8c7e892c5..5a4275cda0 100644 --- a/src/status_im2/contexts/chat/composer/utils.cljs +++ b/src/status_im2/contexts/chat/composer/utils.cljs @@ -197,21 +197,15 @@ (defn init-subs [] - (let [chat-screen-loaded? (rf/sub [:shell/chat-screen-loaded?])] - (merge - {:chat-screen-loaded? chat-screen-loaded? - :link-previews? false} - (when chat-screen-loaded? - (let [chat-input (rf/sub [:chats/current-chat-input])] - {:images (seq (rf/sub [:chats/sending-image])) - :link-previews? (rf/sub [:chats/link-previews?]) - :audio (rf/sub [:chats/sending-audio]) - :reply (rf/sub [:chats/reply-message]) - :edit (rf/sub [:chats/edit-message]) - :input-with-mentions (rf/sub [:chat/input-with-mentions]) - :input-text (:input-text chat-input) - :input-content-height (:input-content-height chat-input) - :chat-screen-loaded? chat-screen-loaded?}))))) + (let [chat-input (rf/sub [:chats/current-chat-input])] + {:images (seq (rf/sub [:chats/sending-image])) + :link-previews? (rf/sub [:chats/link-previews?]) + :audio (rf/sub [:chats/sending-audio]) + :reply (rf/sub [:chats/reply-message]) + :edit (rf/sub [:chats/edit-message]) + :input-with-mentions (rf/sub [:chat/input-with-mentions]) + :input-text (:input-text chat-input) + :input-content-height (:input-content-height chat-input)})) (defn init-animations [{:keys [input-text images link-previews? reply audio]} diff --git a/src/status_im2/contexts/chat/composer/view.cljs b/src/status_im2/contexts/chat/composer/view.cljs index 9e7a8d7f86..902f95635e 100644 --- a/src/status_im2/contexts/chat/composer/view.cljs +++ b/src/status_im2/contexts/chat/composer/view.cljs @@ -23,7 +23,10 @@ [status-im2.contexts.chat.composer.style :as style] [status-im2.contexts.chat.composer.sub-view :as sub-view] [status-im2.contexts.chat.composer.utils :as utils] - [utils.i18n :as i18n])) + [status-im2.contexts.chat.messages.contact-requests.bottom-drawer.view :as + contact-requests.bottom-drawer] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) (defn sheet-component [{:keys [insets @@ -35,8 +38,7 @@ background-y theme messages-list-on-layout-finished?]} props state] - (let [{:keys [chat-screen-loaded?] - :as subscriptions} (utils/init-subs) + (let [subscriptions (utils/init-subs) content-height (reagent/atom (or (:input-content-height ; Actual text height subscriptions) constants/input-height)) @@ -80,12 +82,11 @@ (effects/link-previews props state animations subscriptions) (effects/use-images props state animations subscriptions) [:<> - (when chat-screen-loaded? - [mentions/view props state animations max-height cursor-pos - (:images subscriptions) - (:link-previews? subscriptions) - (:reply subscriptions) - (:edit subscriptions)]) + [mentions/view props state animations max-height cursor-pos + (:images subscriptions) + (:link-previews? subscriptions) + (:reply subscriptions) + (:edit subscriptions)] [rn/view {:style style/composer-sheet-and-jump-to-container} [sub-view/shell-button state scroll-to-bottom-fn show-floating-scroll-down-button?] @@ -96,12 +97,11 @@ {:style (style/sheet-container insets state animations theme) :on-layout #(handler/layout % state blur-height)} [sub-view/bar] - (when chat-screen-loaded? - [:<> - [reply/view state (:input-ref props)] - [edit/view - {:text-value (:text-value state) - :input-ref (:input-ref props)}]]) + [:<> + [reply/view state (:input-ref props)] + [edit/view + {:text-value (:text-value state) + :input-ref (:input-ref props)}]] [reanimated/touchable-opacity {:active-opacity 1 :on-press (fn [] @@ -140,15 +140,14 @@ :theme theme}) :max-length constants/max-text-size :accessibility-label :chat-message-input}]]] - (when chat-screen-loaded? - [:<> - [gradients/view props state animations show-bottom-gradient?] - [link-preview/view] - [images/images-list]]) + [:<> + [gradients/view props state animations show-bottom-gradient?] + [link-preview/view] + [images/images-list]] [:f> actions/view props state animations window-height insets scroll-to-bottom-fn subscriptions]]]]])) -(defn composer +(defn f-composer [{:keys [insets scroll-to-bottom-fn show-floating-scroll-down-button? messages-list-on-layout-finished?]}] (let [window-height (:height (rn/get-window)) @@ -177,3 +176,15 @@ :focused? (:focused? state) :theme theme}] [:f> sheet-component extra-params props state]])) + +(defn composer + [props] + (let [{:keys [chat-id + contact-request-state + group-chat + able-to-send-message?] + :as chat} (rf/sub [:chats/current-chat-chat-view])] + (when (seq chat) + (if able-to-send-message? + [:f> f-composer props] + [contact-requests.bottom-drawer/view chat-id contact-request-state group-chat])))) diff --git a/src/status_im2/contexts/chat/lightbox/utils.cljs b/src/status_im2/contexts/chat/lightbox/utils.cljs index 4ed12a9e22..60b95af16f 100644 --- a/src/status_im2/contexts/chat/lightbox/utils.cljs +++ b/src/status_im2/contexts/chat/lightbox/utils.cljs @@ -13,7 +13,7 @@ [status-im2.contexts.chat.lightbox.constants :as constants] [status-im2.contexts.chat.lightbox.top-view :as top-view] [utils.re-frame :as rf] - [utils.worklets.lightbox :as worklet])) + [utils.worklets.chat.lightbox :as worklet])) (defn clear-timers [timers] diff --git a/src/status_im2/contexts/chat/messages/constants.cljs b/src/status_im2/contexts/chat/messages/constants.cljs new file mode 100644 index 0000000000..ded9d7d6b4 --- /dev/null +++ b/src/status_im2/contexts/chat/messages/constants.cljs @@ -0,0 +1,16 @@ +(ns status-im2.contexts.chat.messages.constants) + +;;;; Navigation +(def ^:const top-bar-height 56) +(def ^:const pinned-banner-height 40) +(def ^:const header-container-top-margin 48) +(def ^:const header-container-radius 20) +(def ^:const header-animation-distance 20) +(def ^:const content-animation-start-position 110) +;; Note - We should also consider height of bio for banner animation starting position +;; Todo - Should be updated once New-profile implemation is complete +(def ^:const pinned-banner-animation-start-position 148) + +(def ^:const default-extrapolation-option + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"}) diff --git a/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/style.cljs b/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/style.cljs new file mode 100644 index 0000000000..d7c5d7693b --- /dev/null +++ b/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/style.cljs @@ -0,0 +1,7 @@ +(ns status-im2.contexts.chat.messages.contact-requests.bottom-drawer.style) + +(def container + {:position :absolute + :bottom 0 + :left 0 + :right 0}) diff --git a/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer.cljs b/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/view.cljs similarity index 92% rename from src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer.cljs rename to src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/view.cljs index 767db345d4..14121b15e3 100644 --- a/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer.cljs +++ b/src/status_im2/contexts/chat/messages/contact_requests/bottom_drawer/view.cljs @@ -1,8 +1,9 @@ -(ns status-im2.contexts.chat.messages.contact-requests.bottom-drawer +(ns status-im2.contexts.chat.messages.contact-requests.bottom-drawer.view (:require [quo.core :as quo] [react-native.core :as rn] [status-im2.constants :as constants] + [status-im2.contexts.chat.messages.contact-requests.bottom-drawer.style :as style] [status-im2.contexts.shell.jump-to.constants :as jump-to.constants] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -11,7 +12,7 @@ [contact-id contact-request-state group-chat] (let [customization-color (rf/sub [:profile/customization-color]) [primary-name _] (rf/sub [:contacts/contact-two-names-by-identity contact-id])] - [rn/view + [rn/view {:style style/container} [quo/permission-context [quo/button {:type :ghost diff --git a/src/status_im2/contexts/chat/messages/list/style.cljs b/src/status_im2/contexts/chat/messages/list/style.cljs index b3141836ff..d32a6b6a96 100644 --- a/src/status_im2/contexts/chat/messages/list/style.cljs +++ b/src/status_im2/contexts/chat/messages/list/style.cljs @@ -1,56 +1,46 @@ (ns status-im2.contexts.chat.messages.list.style (:require [quo.foundations.colors :as colors] - [react-native.reanimated :as reanimated])) - -(defonce ^:const cover-height 168) -(defonce ^:const overscroll-cover-height 2000) -(defonce ^:const header-avatar-top-offset -36) + [react-native.reanimated :as reanimated] + [status-im2.contexts.chat.messages.constants :as messages.constants])) (def keyboard-avoiding-container - {:flex 1}) + {:flex 1 + :z-index 2}) (def list-container {:padding-vertical 16}) -(defn header-container - [show? theme] - {:display (if show? :flex :none) - :background-color (colors/theme-colors colors/white colors/neutral-100 theme) - :top (- overscroll-cover-height) - :margin-bottom (- overscroll-cover-height)}) - -(defn header-cover - [cover-bg-color theme] - {:flex 1 - :height (+ overscroll-cover-height cover-height) - :background-color (colors/theme-colors - (colors/custom-color cover-bg-color 50 20) - (colors/custom-color cover-bg-color 50 40) - theme)}) +(defn background-container + [background-color background-opacity top-margin] + (reanimated/apply-animations-to-style + {:opacity background-opacity} + {:background-color background-color + :position :absolute + :top 0 + :left 0 + :right 0 + :height (+ top-margin messages.constants/header-container-radius)})) (defn header-bottom-part - [animation theme] + [border-radius theme top-margin] (reanimated/apply-animations-to-style - {:border-top-right-radius animation - :border-top-left-radius animation} - {:top -16 - :margin-bottom -16 - :padding-bottom 24 - :background-color (colors/theme-colors colors/white colors/neutral-95 theme)})) - -(def header-avatar - {:top header-avatar-top-offset - :margin-horizontal 20 - :margin-bottom header-avatar-top-offset}) + {:border-top-left-radius border-radius + :border-top-right-radius border-radius} + {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) + :padding-horizontal 20 + :margin-top top-margin})) (defn header-image - [scale-animation side-animation bottom-animation] + [scale top left theme] (reanimated/apply-animations-to-style - {:transform [{:scale scale-animation} - {:translate-x side-animation} - {:translate-y bottom-animation}]} - {:align-items :flex-start})) + {:transform [{:scale scale}] + :top top + :left left} + {:position :absolute + :border-width 4 + :border-radius 50 + :border-color (colors/theme-colors colors/white colors/neutral-95 theme)})) (def bio {:margin-top 8}) diff --git a/src/status_im2/contexts/chat/messages/list/view.cljs b/src/status_im2/contexts/chat/messages/list/view.cljs index 7ec21e5ff6..b8206d754e 100644 --- a/src/status_im2/contexts/chat/messages/list/view.cljs +++ b/src/status_im2/contexts/chat/messages/list/view.cljs @@ -6,31 +6,27 @@ [quo.theme :as quo.theme] [react-native.background-timer :as background-timer] [react-native.core :as rn] + [react-native.hooks :as hooks] [react-native.platform :as platform] - [react-native.react-native-intersection-observer :as rnio] [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] [status-im.ui.screens.chat.group :as chat.group] [status-im2.common.home.actions.view :as home.actions] [status-im2.constants :as constants] [status-im2.contexts.chat.composer.constants :as composer.constants] + [status-im2.contexts.chat.messages.constants :as messages.constants] [status-im2.contexts.chat.messages.content.view :as message] [status-im2.contexts.chat.messages.list.state :as state] [status-im2.contexts.chat.messages.list.style :as style] - [status-im2.contexts.chat.messages.navigation.style :as navigation.style] [status-im2.contexts.shell.jump-to.constants :as jump-to.constants] [utils.i18n :as i18n] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [utils.worklets.chat.messages :as worklets])) -(defonce ^:const card-border-radius 16) (defonce ^:const threshold-percentage-to-show-floating-scroll-down-button 75) (defonce ^:const loading-indicator-extra-spacing 250) (defonce ^:const loading-indicator-page-loading-height 100) -(defonce ^:const scroll-animation-input-range [0 50]) (defonce ^:const min-message-height 32) -(defonce ^:const topbar-visible-scroll-y-value 50) -(defonce ^:const topbar-invisible-scroll-y-value 135) -(defonce ^:const minimum-scroll-y-topbar-overlaying-avatar 400) -(def root-margin-for-big-name-visibility-detector {:bottom -35}) (defonce messages-list-ref (atom nil)) (defn list-key-fn [{:keys [message-id value]}] (or message-id value)) @@ -42,16 +38,15 @@ (.scrollToOffset #js {:animated true}))) -(defn on-scroll - [evt show-floating-scroll-down-button?] - (let [y (oops/oget evt "nativeEvent.contentOffset.y") - layout-height (oops/oget evt "nativeEvent.layoutMeasurement.height") - threshold-height (* (/ layout-height 100) - threshold-percentage-to-show-floating-scroll-down-button) - reached-threshold? (> y threshold-height)] - (when (not= reached-threshold? @show-floating-scroll-down-button?) - (rn/configure-next (:ease-in-ease-out rn/layout-animation-presets)) - (reset! show-floating-scroll-down-button? reached-threshold?)))) +(defn on-scroll-fn + [show-floating-scroll-down-button?] + (fn [y layout-height] + (let [threshold-height (* (/ layout-height 100) + threshold-percentage-to-show-floating-scroll-down-button) + reached-threshold? (> y threshold-height)] + (when (not= reached-threshold? @show-floating-scroll-down-button?) + (rn/configure-next (:ease-in-ease-out rn/layout-animation-presets)) + (reset! show-floating-scroll-down-button? reached-threshold?))))) (defn on-viewable-items-changed [e] @@ -69,14 +64,13 @@ first-not-visible)))))) (defn list-on-end-reached - [scroll-y on-end-reached?] - ;; FIXME: that's a bit of a hack but we need to update `scroll-y` once the new messages + [distance-from-list-top] + ;; FIXME: that's a bit of a hack but we need to update `distance-from-list-top` once the new messages ;; are fetched in order for the header to work properly (let [on-loaded (fn [n] - (reanimated/set-shared-value scroll-y - (+ (reanimated/get-shared-value scroll-y) + (reanimated/set-shared-value distance-from-list-top + (+ (reanimated/get-shared-value distance-from-list-top) (* n 200))))] - (reset! on-end-reached? true) (if @state/scrolling (rf/dispatch [:chat.ui/load-more-messages-for-current-chat on-loaded]) (background-timer/set-timeout #(rf/dispatch [:chat.ui/load-more-messages-for-current-chat @@ -103,10 +97,6 @@ :size 20 :color (colors/theme-colors colors/primary-50 colors/primary-60 theme)}]))])) -(def header-extrapolation-option - {:extrapolateLeft "clamp" - :extrapolateRight "clamp"}) - (defn- skeleton-list-props [content parent-height animated?] {:content content @@ -114,67 +104,53 @@ :animated? animated?}) (defn loading-view - [chat-id messages-view-height messages-view-header-height] - (let [loading-messages? (rf/sub [:chats/loading-messages? chat-id]) - all-loaded? (rf/sub [:chats/all-loaded? chat-id]) - messages (rf/sub [:chats/raw-chat-messages-stream chat-id]) + [chat-id {:keys [window-height]}] + (let [messages (rf/sub [:chats/raw-chat-messages-stream chat-id]) loading-first-page? (= (count messages) 0) - top-spacing (if loading-first-page? 0 navigation.style/navigation-bar-height) + top-spacing (if loading-first-page? + 0 + (+ messages.constants/top-bar-height (safe-area/get-top))) parent-height (if loading-first-page? - (- @messages-view-height - @messages-view-header-height - composer.constants/composer-default-height - loading-indicator-extra-spacing) + window-height loading-indicator-page-loading-height)] - (when (or loading-messages? (not all-loaded?)) - [rn/view {:padding-top top-spacing} - ;; Only use animated loading skeleton for ios - ;; https://github.com/status-im/status-mobile/issues/17426 - [quo/skeleton-list (skeleton-list-props :messages parent-height platform/ios?)]]))) - -(defn calc-list-header-height - [insets able-to-send-message? images] - (if able-to-send-message? - (+ composer.constants/composer-default-height - jump-to.constants/floating-shell-button-height - (if (seq images) composer.constants/images-container-height 0) - (:bottom insets)) - (- 70 (:bottom insets)))) + [rn/view {:padding-top top-spacing} + ;; Only use animated loading skeleton for ios + ;; https://github.com/status-im/status-mobile/issues/17426 + [quo/skeleton-list (skeleton-list-props :messages parent-height platform/ios?)]])) (defn list-header - [insets able-to-send-message? images theme] - (let [height (calc-list-header-height insets able-to-send-message? images)] - [rn/view - {:background-color (colors/theme-colors colors/white colors/neutral-95 theme) - :height height}])) + [insets able-to-send-message?] + (let [images (rf/sub [:chats/sending-image]) + height (if able-to-send-message? + (+ composer.constants/composer-default-height + jump-to.constants/floating-shell-button-height + (if (seq images) composer.constants/images-container-height 0) + (:bottom insets)) + (- 70 (:bottom insets)))] + [rn/view {:style {:height height}}])) (defn f-list-footer-avatar - [{:keys [scroll-y display-name online? profile-picture]}] - (let [image-scale-animation (reanimated/interpolate scroll-y - scroll-animation-input-range - [1 0.5] - header-extrapolation-option) - image-bottom-animation (reanimated/interpolate scroll-y - scroll-animation-input-range - [0 56] - header-extrapolation-option) - image-side-animation (reanimated/interpolate scroll-y - scroll-animation-input-range - [0 -40] - header-extrapolation-option)] + [{:keys [distance-from-list-top display-name online? profile-picture theme]}] + (let [scale (reanimated/interpolate distance-from-list-top + [0 messages.constants/header-container-top-margin] + [1 0.4] + messages.constants/default-extrapolation-option) + top (reanimated/interpolate distance-from-list-top + [0 messages.constants/header-container-top-margin] + [-40 -8] + messages.constants/default-extrapolation-option) + left (reanimated/interpolate distance-from-list-top + [0 messages.constants/header-container-top-margin] + [20 -4] + messages.constants/default-extrapolation-option)] [reanimated/view - {:style (style/header-image image-scale-animation image-side-animation image-bottom-animation)} + {:style (style/header-image scale top left theme)} [quo/user-avatar {:full-name display-name :online? online? :profile-picture profile-picture :size :big}]])) -(defn list-footer-avatar - [props] - [:f> f-list-footer-avatar props]) - - (defn actions [chat-id cover-bg-color] (let [latest-pin-text (rf/sub [:chats/last-pinned-message-text chat-id]) @@ -210,11 +186,9 @@ muted?)))}]}])) (defn f-list-footer - [{:keys [chat scroll-y cover-bg-color on-layout theme messages-view-height - messages-view-header-height big-name-visible?]}] + [{:keys [chat distance-from-list-top cover-bg-color theme]}] (let [{:keys [chat-id chat-name emoji chat-type group-chat]} chat - all-loaded? (rf/sub [:chats/all-loaded? chat-id]) display-name (cond (= chat-type constants/one-to-one-chat-type) (first (rf/sub [:contacts/contact-two-names-by-identity chat-id])) @@ -226,250 +200,168 @@ contact (when-not group-chat (rf/sub [:contacts/contact-by-address chat-id])) photo-path (rf/sub [:chats/photo-path chat-id]) - border-animation (reanimated/interpolate scroll-y - [30 50] - [card-border-radius 0] - header-extrapolation-option)] - [rn/view {:flex 1} - [rn/view - {:style (style/header-container all-loaded? theme) - :on-layout on-layout} - [rn/view {:style (style/header-cover cover-bg-color theme)}] - [reanimated/view {:style (style/header-bottom-part border-animation theme)} - [rn/view {:style style/header-avatar} - [rn/view {:style {:align-items :flex-start}} - (when-not group-chat - [list-footer-avatar - {:scroll-y scroll-y - :display-name display-name - :online? online? - :profile-picture photo-path}])] - [rnio/view - {:on-change (fn [view-visible?] - (reset! big-name-visible? view-visible?)) - :style {:flex-direction :row - :margin-top (if group-chat 54 12)}} - [quo/text - {:weight :semi-bold - :size :heading-1 - :style {:flex-shrink 1} - :number-of-lines 1} - display-name] - [contact-icon contact theme]] - (when bio - [quo/text {:style style/bio} - bio]) - [actions chat-id cover-bg-color]]]] - [loading-view chat-id messages-view-height messages-view-header-height]])) + top-margin (+ (safe-area/get-top) + messages.constants/top-bar-height + messages.constants/header-container-top-margin) + background-color (colors/theme-colors + (colors/custom-color cover-bg-color 50 20) + (colors/custom-color cover-bg-color 50 40) + theme) + border-radius (reanimated/interpolate + distance-from-list-top + [0 messages.constants/header-container-top-margin] + [20 0] + messages.constants/default-extrapolation-option) + background-opacity (reanimated/interpolate + distance-from-list-top + [messages.constants/header-container-top-margin + (+ messages.constants/header-animation-distance + messages.constants/header-container-top-margin)] + [1 0] + messages.constants/default-extrapolation-option)] + [:<> + [reanimated/view + {:style (style/background-container background-color background-opacity top-margin)}] + [reanimated/view {:style (style/header-bottom-part border-radius theme top-margin)} + (when-not group-chat + [:f> f-list-footer-avatar + {:distance-from-list-top distance-from-list-top + :display-name display-name + :online? online? + :theme theme + :profile-picture photo-path}]) + [rn/view + {:style {:flex-direction :row + :margin-top (if group-chat 94 52)}} + [quo/text + {:weight :semi-bold + :size :heading-1 + :style {:flex-shrink 1} + :number-of-lines 1} + display-name] + [contact-icon contact theme]] + (when bio + [quo/text {:style style/bio} + bio]) + [actions chat-id cover-bg-color]]])) (defn list-footer [props] - [:f> f-list-footer props]) + (let [chat-id (get-in props [:chat :chat-id]) + loading-messages? (rf/sub [:chats/loading-messages? chat-id]) + all-loaded? (rf/sub [:chats/all-loaded? chat-id])] + [:<> + (if (or loading-messages? (not all-loaded?)) + [loading-view chat-id props] + [:f> f-list-footer props])])) (defn list-group-chat-header [{:keys [chat-id invitation-admin]}] [rn/view [chat.group/group-chat-footer chat-id invitation-admin]]) -(defn footer-on-layout - [e messages-view-header-height] - (let [height (oops/oget e "nativeEvent.layout.height") - y (oops/oget e "nativeEvent.layout.y")] - (reset! messages-view-header-height (+ height y)))) (defn render-fn - [{:keys [type value content-type theme] :as message-data} _ _ + [{:keys [type value content-type] :as message-data} _ _ {:keys [context keyboard-shown?]}] (when (not= content-type constants/content-type-contact-request) - [rn/view - {:background-color (colors/theme-colors colors/white colors/neutral-95 theme)} - (cond - (= type :datemark) - [quo/divider-date value] + (cond + (= type :datemark) + [quo/divider-date value] - :else - [message/message message-data context keyboard-shown?])])) + :else + [message/message message-data context keyboard-shown?]))) -(defn use-scroll-handler - [{:keys [scroll-y-animated-value - content-size-animated-value - animate-topbar-opacity? - on-end-reached? - animate-topbar-name?]}] - (fn [new-scroll-y new-content-size] - (when (and @on-end-reached? (pos? new-scroll-y)) - (reset! on-end-reached? false)) - (if (< topbar-visible-scroll-y-value new-scroll-y) - (reset! animate-topbar-opacity? true) - (reset! animate-topbar-opacity? false)) - (if (< topbar-invisible-scroll-y-value new-scroll-y) - (reset! animate-topbar-name? true) - (reset! animate-topbar-name? false)) - (reanimated/set-shared-value content-size-animated-value new-content-size) - (reanimated/set-shared-value scroll-y-animated-value (max new-scroll-y 0)))) +(defn on-content-size-change + [{:keys [distance-from-list-top window-height content-height calculations-complete?]}] + (fn [_ content-height-new] + (let [change (- content-height-new @content-height) + distance (if (reanimated/get-shared-value calculations-complete?) + (+ (reanimated/get-shared-value distance-from-list-top) change) + (- content-height-new window-height))] + (reanimated/set-shared-value distance-from-list-top distance) + (reset! content-height content-height-new)) + (when-not (reanimated/get-shared-value calculations-complete?) + (js/setTimeout #(reanimated/set-shared-value calculations-complete? true) 10)))) + +(defn keyboard-offset + [distance-from-list-top keyboard-shown keyboard-height keyboard-offset?] + ;; Note - keyboard fires multiple events, we are making sure we only offset once + (when (> keyboard-height 0) + (let [current-distance-from-top (reanimated/get-shared-value distance-from-list-top)] + (when (and keyboard-shown (not @keyboard-offset?)) + (reanimated/set-shared-value distance-from-list-top + (+ current-distance-from-top keyboard-height)) + (reset! keyboard-offset? true)) + (when (and (not keyboard-shown) @keyboard-offset?) + (reanimated/set-shared-value distance-from-list-top + (- current-distance-from-top keyboard-height)) + (reset! keyboard-offset? false))))) (defn f-messages-list-content - [{:keys [chat insets scroll-y content-height cover-bg-color keyboard-shown? inner-state-atoms - big-name-visible? animate-topbar-opacity? composer-active? messages-list-on-layout-finished? - on-end-reached? animate-topbar-name?]}] - (rn/use-effect (fn [] - (if (and (not @on-end-reached?) - (< topbar-visible-scroll-y-value (reanimated/get-shared-value scroll-y))) - (reset! animate-topbar-opacity? true) - (reset! animate-topbar-opacity? false))) - [composer-active? @on-end-reached? @animate-topbar-opacity?]) - (let [theme (quo.theme/use-theme-value) - {window-height :height} (rn/get-window) - context (rf/sub [:chats/current-chat-message-list-view-context]) - messages (rf/sub [:chats/raw-chat-messages-stream (:chat-id chat)]) - images (rf/sub [:chats/sending-image]) - recording? (rf/sub [:chats/recording?]) - {:keys [show-floating-scroll-down-button? - messages-scroll-y-value-initialized? - messages-view-height - messages-view-header-height]} inner-state-atoms - scroll-handler (use-scroll-handler - {:animate-topbar-name? animate-topbar-name? - :animate-topbar-opacity? animate-topbar-opacity? - :on-end-reached? on-end-reached? - :scroll-y-animated-value scroll-y - :content-size-animated-value - content-height})] - [rn/view {:style {:flex 1}} - [rnio/flat-list - {:root-margin root-margin-for-big-name-visibility-detector - :key-fn list-key-fn - :ref list-ref - :bounces false - :header [:<> - [list-header insets (:able-to-send-message? context) images theme] - (when (= (:chat-type chat) constants/private-group-chat-type) - [list-group-chat-header chat])] - :footer [list-footer - {:theme theme - :chat chat - :scroll-y scroll-y - :cover-bg-color cover-bg-color - :on-layout #(footer-on-layout - % - messages-view-header-height) - :messages-view-header-height messages-view-header-height - :messages-view-height messages-view-height - :big-name-visible? big-name-visible?}] - :data messages - :render-data {:theme theme - :context context - :keyboard-shown? keyboard-shown? - :insets insets} - :render-fn render-fn - :on-viewable-items-changed on-viewable-items-changed - :on-content-size-change - (fn [_ y] - (if (or - (< minimum-scroll-y-topbar-overlaying-avatar - (reanimated/get-shared-value scroll-y)) - (< topbar-visible-scroll-y-value - (reanimated/get-shared-value scroll-y))) - (reset! animate-topbar-opacity? true) - (reset! animate-topbar-opacity? false)) - (when-not (or - (not @big-name-visible?) - (= :initial-render @big-name-visible?) - (pos? (reanimated/get-shared-value - scroll-y))) - (reset! on-end-reached? false)) - ;; NOTE(alwx): here we set the initial value of `scroll-y` which is needed because by - ;; default the chat is scrolled to the bottom and no initial `on-scroll` event is getting - ;; triggered. Also makes sure changes in the content size are reflected in `scroll-y` e.g. - ;; incoming messages scrolling the content up, which should affect the header - ;; animations/interpolations. - (let [content-size-shared (reanimated/get-shared-value - content-height) - scroll-y-shared (if - @messages-scroll-y-value-initialized? - (reanimated/get-shared-value - scroll-y) - y) - ;; NOTE: when the keyboard is shown and the message list - ;; doesn't fill the screen, we use a different value for - ;; scroll-y, which would make sure the avatar is minimized - ;; appropriately and the header is shown. - keyboard-shown-with-unfilled-list? (< y window-height) - keyboard-space (- window-height y) - keyboard-scroll-value (- y keyboard-space keyboard-space) - ;; NOTE: doing the calculations as we would in `on-scroll`, so the animations - ;; and interpolations are triggered properly. - content-size (- y - window-height - ;; NOTE: for some reason android insets are - ;; included in the content size `y`, while iOS - ;; are not. - (when platform/android? (:top insets))) - current-y (max (- content-size-shared - scroll-y-shared) - 0) - new-scroll-value (if keyboard-shown-with-unfilled-list? - keyboard-scroll-value - (- content-size - current-y))] - (when (and (>= new-scroll-value 0) - (or (= scroll-y-shared 0) - (> (Math/abs (- content-size-shared y)) - min-message-height))) - (reset! messages-scroll-y-value-initialized? true) - (scroll-handler new-scroll-value content-size)))) - :on-end-reached #(list-on-end-reached scroll-y on-end-reached?) - :on-scroll-to-index-failed identity - :scroll-indicator-insets {:top (if (:able-to-send-message? context) - (- composer.constants/composer-default-height 16) - 0)} - :keyboard-dismiss-mode :interactive - :keyboard-should-persist-taps :always - :on-scroll-begin-drag #(do - (rf/dispatch [:chat.ui/set-input-focused false]) - (rn/dismiss-keyboard!)) - :on-momentum-scroll-begin state/start-scrolling - :on-momentum-scroll-end state/stop-scrolling - :scroll-event-throttle 16 - :on-scroll (fn [event] - (let [content-size-y - (- (oops/oget event "nativeEvent.contentSize.height") - (oops/oget event - "nativeEvent.layoutMeasurement.height")) - current-y (oops/oget event - "nativeEvent.contentOffset.y") - scroll-distance (- content-size-y current-y)] - (scroll-handler scroll-distance content-size-y)) - (on-scroll event show-floating-scroll-down-button?)) - :content-container-style {:justify-content :flex-end - :min-height @messages-view-height} - :style {:background-color (colors/theme-colors - colors/white - colors/neutral-95 - theme)} - :inverted true - :on-layout (fn [e] - ;; FIXME: the 1s timeout is to assure all effects with - ;; timeouts that depend on the value are considered. - ;; Hacky, but we're heavily relying on timeouts in the - ;; composer and need to react to differently (e.g. - ;; inside effects/use-edit) when the chat has just - ;; opened and the subsequent times. - (js/setTimeout #(reset! messages-list-on-layout-finished? - true) - 1000) - (let [layout-height (oops/oget e - "nativeEvent.layout.height")] - (reset! messages-view-height layout-height))) - :scroll-enabled (not recording?) - :content-inset-adjustment-behavior :never}]])) - -(defn message-list-content-view - [props] - (let [chat-screen-loaded? (rf/sub [:shell/chat-screen-loaded?]) - window-height (:height (rn/get-window)) - content-height (- window-height composer.constants/composer-default-height) - top-spacing (when (not chat-screen-loaded?) navigation.style/navigation-bar-height)] - (if chat-screen-loaded? - [:f> f-messages-list-content props] - [rn/view {:style {:padding-top top-spacing :flex 1}} - [quo/skeleton-list (skeleton-list-props :messages content-height false)]]))) + [{:keys [insets distance-from-list-top keyboard-offset? content-height cover-bg-color + show-floating-scroll-down-button? calculations-complete? + messages-list-on-layout-finished?]}] + (let [theme (quo.theme/use-theme-value) + chat (rf/sub [:chats/current-chat-chat-view]) + {:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard) + {window-height :height} (rn/get-window) + context (rf/sub [:chats/current-chat-message-list-view-context]) + messages (rf/sub [:chats/raw-chat-messages-stream + (:chat-id chat)]) + recording? (rf/sub [:chats/recording?])] + (keyboard-offset distance-from-list-top keyboard-shown keyboard-height keyboard-offset?) + [rn/view {:style {:flex 3}} ;; Pushes composer to bottom + [rn/view {:style {:flex-shrink 1}} ;; Keeps flat list on top + [reanimated/flat-list + {:key-fn list-key-fn + :ref list-ref + :bounces false + :header [:<> + [list-header insets (:able-to-send-message? context)] + (when (= (:chat-type chat) constants/private-group-chat-type) + [list-group-chat-header chat])] + :footer [list-footer + {:theme theme + :chat chat + :window-height window-height + :distance-from-list-top distance-from-list-top + :cover-bg-color cover-bg-color}] + :data messages + :render-data {:theme theme + :context context + :keyboard-shown? keyboard-shown + :insets insets} + :render-fn render-fn + :on-viewable-items-changed on-viewable-items-changed + :on-content-size-change (on-content-size-change + {:distance-from-list-top distance-from-list-top + :window-height window-height + :content-height content-height + :calculations-complete? calculations-complete?}) + :on-end-reached #(list-on-end-reached distance-from-list-top) + :on-scroll-to-index-failed identity + :scroll-indicator-insets {:top (if (:able-to-send-message? context) + (- composer.constants/composer-default-height 16) + 0)} + :keyboard-dismiss-mode :interactive + :keyboard-should-persist-taps :always + :on-scroll-begin-drag #(do + (rf/dispatch [:chat.ui/set-input-focused false]) + (rn/dismiss-keyboard!)) + :on-momentum-scroll-begin state/start-scrolling + :on-momentum-scroll-end state/stop-scrolling + :scroll-event-throttle 16 + :on-scroll (reanimated/use-animated-scroll-handler + (worklets/messages-list-on-scroll + distance-from-list-top + (on-scroll-fn show-floating-scroll-down-button?))) + :style {:background-color (colors/theme-colors colors/white + colors/neutral-95 + theme)} + :inverted true + :on-layout (fn [_] + (js/setTimeout #(reset! messages-list-on-layout-finished? + true) + 1000)) + :scroll-enabled (not recording?) + :content-inset-adjustment-behavior :never}]]])) diff --git a/src/status_im2/contexts/chat/messages/navigation/style.cljs b/src/status_im2/contexts/chat/messages/navigation/style.cljs index c5a3e26919..ab5a2767ca 100644 --- a/src/status_im2/contexts/chat/messages/navigation/style.cljs +++ b/src/status_im2/contexts/chat/messages/navigation/style.cljs @@ -3,90 +3,50 @@ [quo.foundations.colors :as colors] [react-native.reanimated :as reanimated])) -(defonce ^:const navigation-bar-height 100) -(defonce ^:const header-offset 56) - -(defn background-view - [theme] - {:position :absolute - :top 0 - :left 0 - :right 0 - :height navigation-bar-height - :background-color (colors/theme-colors colors/white-opa-70 colors/neutral-100-opa-70 theme) - :display :flex - :flex-direction :row - :overflow :hidden}) +(defn navigation-view + [navigation-view-height pinned-banner-height] + {:top 0 + :left 0 + :right 0 + :position :absolute + :height (+ navigation-view-height pinned-banner-height) + :z-index 1}) (defn animated-background-view - [enabled? animation theme] + [background-opacity navigation-view-height] (reanimated/apply-animations-to-style - (when enabled? - {:opacity animation}) - (background-view theme))) + {:opacity background-opacity} + {:height navigation-view-height + :top 0 + :left 0 + :right 0 + :overflow :hidden + :position :absolute})) -(def blur-view - {:position :absolute - :top 0 - :left 0 - :right 0 - :height navigation-bar-height - :display :flex - :flex-direction :row - :overflow :hidden}) - -(defn animated-blur-view - [enabled? animation] - (reanimated/apply-animations-to-style - (when enabled? - {:opacity animation}) - blur-view)) - -(defn navigation-view - [loaded?] - {:z-index 1 - :top 0 - :right 0 - :left 0 - :position :absolute - :opacity (if loaded? 1 0)}) - -(def header-container - {:position :absolute - :top header-offset - :left 0 - :right 0 - :padding-bottom 8 - :padding-horizontal 20 - :display :flex +(defn header-container + [top-insets top-bar-height] + {:margin-top top-insets :flex-direction :row - :overflow :hidden}) + :padding-horizontal 20 + :overflow :hidden + :height top-bar-height + :align-items :center}) -(def header - {:flex 1}) +;;;; Content -(defn animated-header - [enabled? y-animation opacity-animation] +(defn header-content-container + [header-opacity header-position] (reanimated/apply-animations-to-style - ;; here using `top` won't work on Android, so we are using `translateY` - (when enabled? - {:transform [{:translateY y-animation}] - :opacity opacity-animation}) - header)) - -(def header-content-container - {:flex-direction :row - :align-items :center - :margin-left 12 - :margin-right 8 - :margin-top -4 - :height 40}) - -(def header-avatar-container - {:margin-right 8}) + {:transform [{:translate-y header-position}] + :opacity header-opacity} + {:flex-direction :row + :align-items :center + :flex 1 + :margin-horizontal 12 + :height 40})) (def header-text-container - {:flex 1}) + {:margin-left 8}) (defn header-display-name [] @@ -94,4 +54,4 @@ (defn header-status [] - {:color (colors/theme-colors colors/neutral-80-opa-50 colors/white-opa-50)}) + {:color (colors/theme-colors colors/neutral-80-opa-50 colors/white-opa-40)}) diff --git a/src/status_im2/contexts/chat/messages/navigation/view.cljs b/src/status_im2/contexts/chat/messages/navigation/view.cljs index cea51cf396..12108ca00d 100644 --- a/src/status_im2/contexts/chat/messages/navigation/view.cljs +++ b/src/status_im2/contexts/chat/messages/navigation/view.cljs @@ -2,112 +2,120 @@ (:require [quo.core :as quo] [quo.foundations.colors :as colors] - [quo.theme :as quo.theme] [re-frame.db] [react-native.blur :as blur] [react-native.core :as rn] [react-native.platform :as platform] [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] [status-im2.common.home.actions.view :as actions] - [status-im2.contexts.chat.messages.list.view :refer [topbar-invisible-scroll-y-value]] + [status-im2.constants :as constants] + [status-im2.contexts.chat.messages.constants :as messages.constants] [status-im2.contexts.chat.messages.navigation.style :as style] [status-im2.contexts.chat.messages.pin.banner.view :as pin.banner] [utils.i18n :as i18n] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [utils.worklets.chat.messages :as worklets])) -(defonce ^:const title-opacity-interpolation-start 50) -;; This has two possibilities, One when sending messages and one when opening chat. -(defonce ^:const minimum-scroll-y-topbar-overlaying-avatar 80) -(defonce ^:const minimum-scroll-y-topbar-overlaying-avatar-2 350) -(defonce ^:const minimum-scroll-y-topbar-overlaying-avatar-composer-active 85) +(defn f-header-content-container + [{:keys [chat distance-from-list-top all-loaded? calculations-complete?]}] + (let [{:keys [chat-id group-chat chat-type chat-name + emoji]} chat + display-name (cond + (= chat-type constants/one-to-one-chat-type) + (first (rf/sub + [:contacts/contact-two-names-by-identity + chat-id])) + (= chat-type constants/community-chat-type) + (str (when emoji (str emoji " ")) "# " chat-name) + :else (str emoji chat-name)) + online? (when-not group-chat (rf/sub [:visibility-status-updates/online? chat-id])) + photo-path (when-not group-chat (rf/sub [:chats/photo-path chat-id])) + header-opacity (worklets/navigation-header-opacity + distance-from-list-top + all-loaded? + calculations-complete? + messages.constants/content-animation-start-position) + header-position (worklets/navigation-header-position + distance-from-list-top + all-loaded? + messages.constants/top-bar-height + messages.constants/content-animation-start-position)] + [reanimated/view + {:style (style/header-content-container header-opacity header-position)} + (when-not group-chat + [quo/user-avatar + {:full-name display-name + :online? online? + :profile-picture photo-path + :size :small}]) + [rn/view {:style style/header-text-container} + [quo/text + {:weight :semi-bold + :size :paragraph-1 + :number-of-lines 1 + :style (style/header-display-name)} + display-name] + (when-not group-chat + [quo/text + {:number-of-lines 1 + :weight :medium + :size :paragraph-2 + :style (style/header-status)} + (i18n/label + (if online? :t/online :t/offline))])]])) + +(defn f-animated-background-and-pinned-banner + [{:keys [chat-id navigation-view-height distance-from-list-top all-loaded?]}] + (let [animation-distance messages.constants/header-animation-distance + props {:distance-from-list-top distance-from-list-top + :all-loaded? all-loaded?} + background-opacity (worklets/interpolate-navigation-view-opacity + (assoc props + :start-position + messages.constants/header-container-top-margin + :end-position + (+ animation-distance + messages.constants/header-container-top-margin))) + banner-opacity (worklets/interpolate-navigation-view-opacity + (assoc props + :start-position + (+ navigation-view-height + messages.constants/pinned-banner-animation-start-position) + :end-position + (+ animation-distance + navigation-view-height + messages.constants/pinned-banner-animation-start-position)))] + [:<> + [reanimated/view {:style (style/animated-background-view background-opacity navigation-view-height)} + [blur/view + {:style {:flex 1} + :blur-amount 20 + :blur-type :transparent + :overlay-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur) + :blur-radius (if platform/ios? 20 10)}]] + [pin.banner/banner + {:chat-id chat-id + :banner-opacity banner-opacity + :top-offset navigation-view-height}]])) (defn f-view - [{:keys [theme scroll-y chat chat-screen-loaded? all-loaded? display-name online? photo-path - back-icon animate-topbar-name? composer-active? big-name-visible? animate-topbar-opacity? - on-end-reached?]}] - (let [{:keys [group-chat chat-id]} chat - opacity-animation (reanimated/use-shared-value 0) - banner-opacity-animation (reanimated/interpolate - scroll-y - [(+ style/navigation-bar-height 150) - (+ style/navigation-bar-height 200)] - [0 1] - {:extrapolateLeft "clamp" - :extrapolateRight "clamp"}) - translate-animation (reanimated/use-shared-value - title-opacity-interpolation-start) - title-opacity-animation (reanimated/use-shared-value 0) - messages (rf/sub [:chats/raw-chat-messages-stream - (:chat-id chat)]) - more-than-two-messages? (<= 2 (count messages)) - more-than-four-messages? (<= 4 (count messages)) - more-than-eight-messages? (<= 8 (count messages)) - scroll-y-sending-eight-messages-threshold 469] - (rn/use-effect - (fn [] - (if - (or - (and (not composer-active?) - more-than-eight-messages? - (= :initial-render @big-name-visible?)) - (and - (< minimum-scroll-y-topbar-overlaying-avatar (reanimated/get-shared-value scroll-y)) - (not @on-end-reached?)) - (and (if platform/ios? more-than-two-messages? more-than-four-messages?) - composer-active?) - (and - (not @on-end-reached?) - @animate-topbar-opacity?) - - (or - (< minimum-scroll-y-topbar-overlaying-avatar-2 (reanimated/get-shared-value scroll-y)) - (and (pos? (count messages)) - composer-active? - (< minimum-scroll-y-topbar-overlaying-avatar-composer-active - (reanimated/get-shared-value scroll-y))))) - (reanimated/animate opacity-animation 1) - (reanimated/animate opacity-animation 0)) - (if (when-not (and - @on-end-reached? - (not composer-active?) - (true? @big-name-visible?)) - (or - (and - (and composer-active? - (not @big-name-visible?)) - (< topbar-invisible-scroll-y-value (reanimated/get-shared-value scroll-y))) - (<= scroll-y-sending-eight-messages-threshold (reanimated/get-shared-value scroll-y)) - (and (not composer-active?) - more-than-eight-messages? - (= :initial-render @big-name-visible?)) - ;; Keyboard height increasing is different between iOS and Android, That's why we have - ;; two values. - (and (if platform/ios? more-than-two-messages? more-than-four-messages?) - (< title-opacity-interpolation-start (reanimated/get-shared-value scroll-y)) - composer-active?) - (and (if platform/ios? more-than-two-messages? more-than-four-messages?) - composer-active?) - @animate-topbar-name?)) - (do - (reanimated/animate title-opacity-animation 1) - (reanimated/animate translate-animation 0)) - (do - (reanimated/animate title-opacity-animation 0) - (reanimated/animate translate-animation title-opacity-interpolation-start)))) - [@animate-topbar-name? @big-name-visible? @animate-topbar-opacity? composer-active? - @on-end-reached?]) - [rn/view {:style (style/navigation-view chat-screen-loaded?)} - [reanimated/view - {:style (style/animated-background-view all-loaded? opacity-animation nil)}] - [reanimated/view {:style (style/animated-blur-view all-loaded? opacity-animation)} - [blur/view - {:blur-amount 20 - :blur-type :transparent - :overlay-color (colors/theme-colors colors/white-70-blur colors/neutral-95-opa-70-blur theme) - :blur-radius (if platform/ios? 20 10) - :style {:flex 1}}]] - - [rn/view {:style style/header-container} + [{:keys [distance-from-list-top calculations-complete?]}] + (let [{:keys [chat-id chat-type] :as chat} (rf/sub [:chats/current-chat-chat-view]) + all-loaded? (reanimated/use-shared-value false) + all-loaded-sub (rf/sub [:chats/all-loaded? chat-id]) + top-insets (safe-area/get-top) + top-bar-height messages.constants/top-bar-height + navigation-view-height (+ top-bar-height top-insets)] + (reanimated/set-shared-value all-loaded? all-loaded-sub) + [rn/view + {:style (style/navigation-view navigation-view-height messages.constants/pinned-banner-height)} + [:f> f-animated-background-and-pinned-banner + {:chat-id chat-id + :navigation-view-height navigation-view-height + :distance-from-list-top distance-from-list-top + :all-loaded? all-loaded?}] + [rn/view {:style (style/header-container top-insets top-bar-height)} [quo/button {:icon-only? true :type :grey @@ -115,33 +123,12 @@ :size 32 :accessibility-label :back-button :on-press #(rf/dispatch [:navigate-back])} - back-icon] - [reanimated/view - {:style (style/animated-header all-loaded? translate-animation title-opacity-animation)} - [rn/view {:style style/header-content-container} - (when-not group-chat - [rn/view {:style style/header-avatar-container} - [quo/user-avatar - {:full-name display-name - :online? online? - :profile-picture photo-path - :size :small}]]) - [rn/view {:style style/header-text-container} - [rn/view {:style {:flex-direction :row}} - [quo/text - {:weight :semi-bold - :size :paragraph-1 - :number-of-lines 1 - :style (style/header-display-name)} - display-name]] - (when-not group-chat - [quo/text - {:number-of-lines 1 - :weight :medium - :size :paragraph-2 - :style (style/header-status)} - (i18n/label - (if online? :t/online :t/offline))])]]] + (if (= chat-type constants/community-chat-type) :i/arrow-left :i/close)] + [:f> f-header-content-container + {:chat chat + :distance-from-list-top distance-from-list-top + :all-loaded? all-loaded? + :calculations-complete? calculations-complete?}] [quo/button {:icon-only? true :type :grey @@ -152,16 +139,4 @@ (rf/dispatch [:dismiss-keyboard]) (rf/dispatch [:show-bottom-sheet {:content (fn [] [actions/chat-actions chat true])}]))} - :i/options]] - [:f> - pin.banner/f-banner - {:chat-id chat-id - :opacity-animation banner-opacity-animation - :all-loaded? all-loaded? - :top-offset style/navigation-bar-height}]])) - -(defn- internal-navigation-view - [params] - [:f> f-view params]) - -(def navigation-view (quo.theme/with-theme internal-navigation-view)) + :i/options]]])) diff --git a/src/status_im2/contexts/chat/messages/pin/banner/style.cljs b/src/status_im2/contexts/chat/messages/pin/banner/style.cljs index 65d3f49c61..6f964ed78a 100644 --- a/src/status_im2/contexts/chat/messages/pin/banner/style.cljs +++ b/src/status_im2/contexts/chat/messages/pin/banner/style.cljs @@ -1,41 +1,17 @@ (ns status-im2.contexts.chat.messages.pin.banner.style (:require - [quo.foundations.colors :as colors] - [react-native.platform :as platform] - [react-native.reanimated :as reanimated])) + [react-native.reanimated :as reanimated] + [status-im2.contexts.chat.messages.constants :as messages.constants])) -(defonce ^:const pinned-banner-height 40) - -(defn blur-container-style - [top-offset opacity-animation enabled?] - (reanimated/apply-animations-to-style - {:opacity opacity-animation} - {:position :absolute - :display (if enabled? :flex :none) - :top top-offset - :left 0 - :right 0 - :bottom 0 - :height pinned-banner-height - :overflow :hidden})) - -(defn blur-view-style - [] - {:style {:flex 1} - :blur-radius (if platform/ios? 20 10) - :blur-type (colors/theme-colors :light :dark) - :blur-amount 20}) - -(defn pinned-banner - [top-offset] +(def container {:position :absolute + :overflow :hidden :left 0 :right 0 - :top top-offset}) + :height messages.constants/pinned-banner-height}) -(defn animated-pinned-banner - [top-offset enabled? animation] +(defn container-animated-style + [top-offset banner-opacity] (reanimated/apply-animations-to-style - (when enabled? - {:opacity animation}) - (pinned-banner top-offset))) + {:opacity banner-opacity} + (assoc container :top top-offset))) diff --git a/src/status_im2/contexts/chat/messages/pin/banner/view.cljs b/src/status_im2/contexts/chat/messages/pin/banner/view.cljs index 2280bf0a99..873999886a 100644 --- a/src/status_im2/contexts/chat/messages/pin/banner/view.cljs +++ b/src/status_im2/contexts/chat/messages/pin/banner/view.cljs @@ -1,27 +1,31 @@ (ns status-im2.contexts.chat.messages.pin.banner.view (:require [quo.core :as quo] + [quo.theme :as theme] [react-native.blur :as blur] - [react-native.core :as rn] + [react-native.platform :as platform] [react-native.reanimated :as reanimated] [status-im2.contexts.chat.messages.pin.banner.style :as style] [utils.re-frame :as rf])) -(defn f-blur-view - [top-offset opacity-animation enabled?] - [reanimated/view {:style (style/blur-container-style top-offset opacity-animation enabled?)} - [blur/view (style/blur-view-style)]]) - (defn f-banner - [{:keys [chat-id opacity-animation all-loaded? top-offset]}] + [{:keys [chat-id banner-opacity top-offset]} latest-pin-text pins-count] + [reanimated/view {:style (style/container-animated-style top-offset banner-opacity)} + [blur/view + {:style style/container + :blur-radius (if platform/ios? 20 10) + :blur-type (if (theme/dark?) :dark :light) + :blur-amount 20}] + [quo/banner + {:latest-pin-text latest-pin-text + :pins-count pins-count + :on-press (fn [] + (rf/dispatch [:dismiss-keyboard]) + (rf/dispatch [:pin-message/show-pins-bottom-sheet chat-id]))}]]) + +(defn banner + [{:keys [chat-id] :as props}] (let [latest-pin-text (rf/sub [:chats/last-pinned-message-text chat-id]) pins-count (rf/sub [:chats/pin-messages-count chat-id])] - [rn/view - [:f> f-blur-view top-offset opacity-animation (> pins-count 0)] - [reanimated/view {:style (style/animated-pinned-banner top-offset all-loaded? opacity-animation)} - [quo/banner - {:latest-pin-text latest-pin-text - :pins-count pins-count - :on-press (fn [] - (rf/dispatch [:dismiss-keyboard]) - (rf/dispatch [:pin-message/show-pins-bottom-sheet chat-id]))}]]])) + (when (> pins-count 0) + [:f> f-banner props latest-pin-text pins-count]))) diff --git a/src/status_im2/contexts/chat/messages/view.cljs b/src/status_im2/contexts/chat/messages/view.cljs index 879417fbee..af16367f0f 100644 --- a/src/status_im2/contexts/chat/messages/view.cljs +++ b/src/status_im2/contexts/chat/messages/view.cljs @@ -1,111 +1,62 @@ (ns status-im2.contexts.chat.messages.view (:require [react-native.core :as rn] - [react-native.hooks :as hooks] [react-native.reanimated :as reanimated] [react-native.safe-area :as safe-area] [reagent.core :as reagent] - [status-im2.constants :as constants] [status-im2.contexts.chat.composer.view :as composer.view] - [status-im2.contexts.chat.messages.contact-requests.bottom-drawer :as contact-requests.bottom-drawer] [status-im2.contexts.chat.messages.list.style :as style] [status-im2.contexts.chat.messages.list.view :as list.view] [status-im2.contexts.chat.messages.navigation.view :as messages.navigation] + [status-im2.contexts.chat.placeholder.view :as placeholder.view] [utils.re-frame :as rf])) ;; NOTE(parvesh) - I am working on refactoring/optimization of the chat screen for performance ;; improvement. Please avoid refactoring these files. Also if you are not already working on bug ;; fixes related to the chat navigation bar, please skip them. ;; And ping me, so I can address them while refactoring -(defn f-chat - [{:keys [show-floating-scroll-down-button? animate-topbar-name? - big-name-visible? animate-topbar-opacity? on-end-reached? messages-list-on-layout-finished?] - :as inner-state-atoms}] - (let [insets (safe-area/get-insets) - scroll-y (reanimated/use-shared-value 0) - content-height (reanimated/use-shared-value 0) - {:keys [keyboard-shown]} (hooks/use-keyboard) - {:keys [chat-id - contact-request-state - group-chat - able-to-send-message? - chat-type - chat-name - emoji] - :as chat} (rf/sub [:chats/current-chat-chat-view]) - chat-screen-loaded? (rf/sub [:shell/chat-screen-loaded?]) - all-loaded? (when chat-screen-loaded? - (rf/sub [:chats/all-loaded? (:chat-id chat)])) - display-name (cond - (= chat-type constants/one-to-one-chat-type) - (first (rf/sub - [:contacts/contact-two-names-by-identity - chat-id])) - (= chat-type constants/community-chat-type) - (str (when emoji (str emoji " ")) "# " chat-name) - :else (str emoji chat-name)) - online? (rf/sub [:visibility-status-updates/online? chat-id]) - photo-path (rf/sub [:chats/photo-path chat-id]) - back-icon (if (= chat-type constants/one-to-one-chat-type) - :i/close - :i/arrow-left) - {:keys [focused?]} (rf/sub [:chats/current-chat-input])] - ;; Note - Don't pass `behavior :height` to keyboard avoiding view,. It breaks composer - - ;; https://github.com/status-im/status-mobile/issues/16595 +(defn- f-chat-screen + [calculations-complete?] + (let [insets (safe-area/get-insets) + keyboard-offset? (atom false) + content-height (atom 0) + show-floating-scroll-down-button? (reagent/atom false) + messages-list-on-layout-finished? (reagent/atom false) + distance-from-list-top (reanimated/use-shared-value 0)] [rn/keyboard-avoiding-view {:style style/keyboard-avoiding-container :keyboard-vertical-offset (- (:bottom insets))} - - [list.view/message-list-content-view - {:chat chat - :insets insets - :scroll-y scroll-y + [:f> messages.navigation/f-view + {:distance-from-list-top distance-from-list-top + :calculations-complete? calculations-complete?}] + [:f> list.view/f-messages-list-content + {:insets insets :content-height content-height + :keyboard-offset? keyboard-offset? + :calculations-complete? calculations-complete? + :distance-from-list-top distance-from-list-top + :messages-list-on-layout-finished? messages-list-on-layout-finished? :cover-bg-color :turquoise - :keyboard-shown? keyboard-shown - :inner-state-atoms inner-state-atoms - :animate-topbar-name? animate-topbar-name? - :big-name-visible? big-name-visible? - :animate-topbar-opacity? animate-topbar-opacity? - :composer-active? focused? - :on-end-reached? on-end-reached? - :messages-list-on-layout-finished? messages-list-on-layout-finished?}] + :show-floating-scroll-down-button? show-floating-scroll-down-button?}] + [composer.view/composer + {:insets insets + :scroll-to-bottom-fn list.view/scroll-to-bottom + :messages-list-on-layout-finished? messages-list-on-layout-finished? + :show-floating-scroll-down-button? show-floating-scroll-down-button?}]])) - [messages.navigation/navigation-view - {:scroll-y scroll-y - :animate-topbar-name? animate-topbar-name? - :back-icon back-icon - :chat chat - :chat-screen-loaded? chat-screen-loaded? - :all-loaded? all-loaded? - :display-name display-name - :online? online? - :photo-path photo-path - :big-name-visible? big-name-visible? - :animate-topbar-opacity? animate-topbar-opacity? - :composer-active? focused? - :on-end-reached? on-end-reached?}] +(defn lazy-chat-screen + [calculations-complete?] + (let [screen-loaded? (rf/sub [:shell/chat-screen-loaded?])] + (when screen-loaded? + [:f> f-chat-screen calculations-complete?]))) - (when (seq chat) - (if able-to-send-message? - [:f> composer.view/composer - {:insets insets - :scroll-to-bottom-fn list.view/scroll-to-bottom - :show-floating-scroll-down-button? show-floating-scroll-down-button? - :messages-list-on-layout-finished? messages-list-on-layout-finished?}] - [contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]))])) +(defn- f-chat + [] + (let [calculations-complete? (reanimated/use-shared-value false)] + [:<> + [lazy-chat-screen calculations-complete?] + [:f> placeholder.view/f-view calculations-complete?]])) (defn chat [] - (let [inner-state-atoms - {:extra-keyboard-height (reagent/atom 0) - :show-floating-scroll-down-button? (reagent/atom false) - :messages-scroll-y-value-initialized? (reagent/atom false) - :messages-view-height (reagent/atom 0) - :messages-view-header-height (reagent/atom 0) - :animate-topbar-name? (reagent/atom false) - :big-name-visible? (reagent/atom :initial-render) - :animate-topbar-opacity? (reagent/atom false) - :on-end-reached? (reagent/atom false) - :messages-list-on-layout-finished? (reagent/atom false)}] - [:f> f-chat inner-state-atoms])) + [:f> f-chat]) diff --git a/src/status_im2/contexts/chat/placeholder/style.cljs b/src/status_im2/contexts/chat/placeholder/style.cljs new file mode 100644 index 0000000000..9a0b243837 --- /dev/null +++ b/src/status_im2/contexts/chat/placeholder/style.cljs @@ -0,0 +1,16 @@ +(ns status-im2.contexts.chat.placeholder.style + (:require [quo.foundations.colors :as colors] + [react-native.reanimated :as reanimated])) + +(defn container + [top opacity z-index] + (reanimated/apply-animations-to-style + {:opacity opacity + :z-index z-index} + {:position :absolute + :padding-top top + :top 0 + :left 0 + :right 0 + :bottom 0 + :background-color (colors/theme-colors colors/white colors/neutral-95)})) diff --git a/src/status_im2/contexts/chat/placeholder/view.cljs b/src/status_im2/contexts/chat/placeholder/view.cljs new file mode 100644 index 0000000000..b15cb908aa --- /dev/null +++ b/src/status_im2/contexts/chat/placeholder/view.cljs @@ -0,0 +1,23 @@ +(ns status-im2.contexts.chat.placeholder.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im2.contexts.chat.placeholder.style :as style] + [utils.worklets.chat.messages :as worklets])) + +(defn- loading-skeleton + [] + [quo/skeleton-list + {:content :messages + :parent-height (:height (rn/get-window)) + :animated? false}]) + +(defn f-view + [calculations-complete?] + (let [top (safe-area/get-top) + opacity (worklets/placeholder-opacity calculations-complete?) + z-index (worklets/placeholder-z-index calculations-complete?)] + [reanimated/view {:style (style/container top opacity z-index)} + [loading-skeleton]])) diff --git a/src/utils/worklets/chat/lightbox.cljs b/src/utils/worklets/chat/lightbox.cljs new file mode 100644 index 0000000000..be20d0d59a --- /dev/null +++ b/src/utils/worklets/chat/lightbox.cljs @@ -0,0 +1,7 @@ +(ns utils.worklets.chat.lightbox) + +(def ^:private layout-worklets (js/require "../src/js/worklets/chat/lightbox.js")) + +(defn info-layout + [input top?] + (.infoLayout ^js layout-worklets input top?)) diff --git a/src/utils/worklets/chat/messages.cljs b/src/utils/worklets/chat/messages.cljs new file mode 100644 index 0000000000..6635acdc1b --- /dev/null +++ b/src/utils/worklets/chat/messages.cljs @@ -0,0 +1,37 @@ +(ns utils.worklets.chat.messages) + +(def ^:private messages-worklets (js/require "../src/js/worklets/chat/messages.js")) + +;;;; Navigtion +(defn navigation-header-opacity + [distance-from-list-top all-loaded? calculations-complete? start-position] + (.navigationHeaderOpacity ^js messages-worklets + distance-from-list-top + all-loaded? + calculations-complete? + start-position)) + +(defn navigation-header-position + [distance-from-list-top all-loaded? top-bar-height start-position] + (.navigationHeaderPosition ^js messages-worklets + distance-from-list-top + all-loaded? + top-bar-height + start-position)) + +(defn interpolate-navigation-view-opacity + [props] + (.interpolateNavigationViewOpacity ^js messages-worklets (clj->js props))) + +(defn messages-list-on-scroll + [distance-from-list-top callback] + (.messagesListOnScroll ^js messages-worklets distance-from-list-top callback)) + +;;;; Placeholder +(defn placeholder-opacity + [calculations-complete?] + (.placeholderOpacity ^js messages-worklets calculations-complete?)) + +(defn placeholder-z-index + [calculations-complete?] + (.placeholderZIndex ^js messages-worklets calculations-complete?)) diff --git a/src/utils/worklets/lightbox.cljs b/src/utils/worklets/lightbox.cljs deleted file mode 100644 index bec6e5ebec..0000000000 --- a/src/utils/worklets/lightbox.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns utils.worklets.lightbox) - -(def ^:private layout-worklets (js/require "../src/js/worklets/lightbox.js")) - -(defn info-layout - [input top?] - (.infoLayout ^js layout-worklets input top?))