New mentions design (#15799)

* feat: mentions new design
This commit is contained in:
Omar Basem 2023-05-04 13:12:42 +04:00 committed by GitHub
parent 4774b0f5e5
commit 7e54aa0b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 229 additions and 61 deletions

View File

@ -3,6 +3,7 @@
[quo.react :as react]
[quo.react-native :as rn]
[re-frame.core :as re-frame]
[status-im2.config :as config]
[utils.re-frame :as rf]
[taoensso.timbre :as log]
[native-module.core :as native-module]))
@ -167,17 +168,20 @@
cursor (+ at-sign-idx (count primary-name) 2)]
(rf/merge
cofx
{:db (-> db
(assoc-in [:chats/mention-suggestions chat-id] nil))
:set-text-input-value [chat-id new-text text-input-ref]
:dispatch [:chat.ui/set-chat-input-text new-text chat-id]}
;; NOTE(rasom): Some keyboards do not react on selection property passed to
;; text input (specifically Samsung keyboard with predictive text set on).
;; In this case, if the user continues typing after the programmatic change,
;; the new text is added to the last known cursor position before
;; programmatic change. By calling `reset-text-input-cursor` we force the
;; keyboard's cursor position to be changed before the next input.
(reset-text-input-cursor text-input-ref cursor)
(let [common {:db (-> db
(assoc-in [:chats/mention-suggestions chat-id] nil))
:dispatch [:chat.ui/set-chat-input-text new-text chat-id]}
extra (if (not config/new-composer-enabled?)
;; NOTE(rasom): Some keyboards do not react on selection property passed to
;; text input (specifically Samsung keyboard with predictive text set on).
;; In this case, if the user continues typing after the programmatic change,
;; the new text is added to the last known cursor position before
;; programmatic change. By calling `reset-text-input-cursor` we force the
;; keyboard's cursor position to be changed before the next input.
{:set-text-input-value [chat-id new-text text-input-ref]
:reset-text-input-cursor (reset-text-input-cursor text-input-ref cursor)}
{})]
(merge common extra))
(recheck-at-idxs public-key))))
(rf/defn clear-suggestions

View File

@ -21,6 +21,8 @@
(def ^:const edit-container-height 32)
(def ^:const mentions-max-height 240)
(def ^:const extra-content-offset (if platform/ios? 6 0))
(def ^:const content-change-threshold 10)

View File

@ -1,11 +1,12 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.handlers
(:require [react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[oops.core :as oops]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
[utils.re-frame :as rf]))
(:require
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[oops.core :as oops]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
[utils.re-frame :as rf]))
(defn focus
[{:keys [input-ref] :as props}
@ -70,7 +71,8 @@
(reanimated/animate height new-height)
(reanimated/set-shared-value saved-height new-height))
(when (= new-height max-height)
(reset! maximized? true))
(reset! maximized? true)
(rf/dispatch [:chat.ui/set-input-maximized true]))
(if (utils/show-background? saved-height max-height new-height)
(do
(reanimated/set-shared-value background-y 0)
@ -82,10 +84,12 @@
(defn scroll
[event
{:keys [scroll-y]}
{:keys [gradient-z-index focused?]}
{:keys [gradient-opacity]}
{:keys [lines max-lines]}]
(let [y (oops/oget event "nativeEvent.contentOffset.y")]
(reset! scroll-y y)
(when (utils/show-top-gradient? y lines max-lines gradient-opacity focused?)
(reset! gradient-z-index 1)
(js/setTimeout #(reanimated/animate gradient-opacity 1) 0))
@ -105,7 +109,8 @@
(when @recording?
(@record-reset-fn)
(reset! recording? false))
(rf/dispatch [:chat.ui/set-chat-input-text text]))
(rf/dispatch [:chat.ui/set-chat-input-text text])
(rf/dispatch [:mention/on-change-text text]))
(defn selection-change
[event {:keys [lock-selection? cursor-position]}]

View File

@ -0,0 +1,30 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.mentions.style
(:require [quo2.foundations.colors :as colors]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
(defn shadow
[]
(if platform/ios?
{:shadow-radius (colors/theme-colors 30 50)
:shadow-opacity (colors/theme-colors 0.1 0.7)
:shadow-color colors/neutral-100
:shadow-offset {:width 0 :height (colors/theme-colors 8 12)}}
{:elevation 10}))
(defn container
[opacity bottom]
(reanimated/apply-animations-to-style
{:opacity opacity}
(merge
{:position :absolute
:bottom bottom
:left 8
:right 8
:border-radius 16
:z-index 4
:max-height constants/mentions-max-height
:background-color (colors/theme-colors colors/white colors/neutral-95)}
(shadow))))

View File

@ -0,0 +1,67 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.mentions.view
(:require
[react-native.hooks :as hooks]
[react-native.platform :as platform]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
[utils.re-frame :as rf]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[status-im2.common.contact-list-item.view :as contact-list-item]
[status-im2.contexts.chat.bottom-sheet-composer.mentions.style :as style]))
(defn update-cursor
[user {:keys [cursor-position input-ref]}]
(when platform/android?
(let [new-cursor-pos (+ (count (:primary-name user)) @cursor-position)]
(reset! cursor-position new-cursor-pos)
(reagent/next-tick #(when @input-ref
(.setNativeProps ^js @input-ref
(clj->js {:selection {:start new-cursor-pos
:end
new-cursor-pos}})))))))
(defn mention-item
[user _ _ render-data]
[contact-list-item/contact-list-item
{:on-press (fn []
(rf/dispatch [:chat.ui/select-mention nil user])
(update-cursor user render-data))}
user])
(defn- f-view
[suggestions-atom props state animations max-height cursor-pos]
(let [{:keys [keyboard-height]} (hooks/use-keyboard)
suggestions (rf/sub [:chat/mention-suggestions])
opacity (reanimated/use-shared-value (if (seq suggestions) 1 0))
size (count suggestions)
data {:keyboard-height keyboard-height
:insets (safe-area/get-insets)
:curr-height (reanimated/get-shared-value (:height animations))
:window-height (rf/sub [:dimensions/window-height])
:reply (rf/sub [:chats/reply-message])
:edit (rf/sub [:chats/edit-message])}
mentions-pos (utils/calc-suggestions-position cursor-pos max-height size state data)]
(rn/use-effect
(fn []
(if (seq suggestions)
(reset! suggestions-atom suggestions)
(js/setTimeout #(reset! suggestions-atom suggestions) 300))
(reanimated/animate opacity (if (seq suggestions) 1 0)))
[(seq suggestions)])
[reanimated/view
{:style (style/container opacity mentions-pos)}
[rn/flat-list
{:keyboard-should-persist-taps :always
:data (vals @suggestions-atom)
:key-fn :key
:render-fn mention-item
:render-data {:cursor-position (:cursor-position state)
:input-ref (:input-ref props)}
:accessibility-label :mentions-list}]]))
(defn view
[props state animations max-height cursor-pos]
(let [suggestions-atom (reagent/atom {})]
[:f> f-view suggestions-atom props state animations max-height cursor-pos]))

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.utils
(:require
[clojure.string :as string]
[oops.core :as oops]
[react-native.platform :as platform]
[react-native.reanimated :as reanimated]
@ -90,3 +91,53 @@
(reanimated/set-shared-value last-height constants/input-height))
(reset! text-value "")
(rf/dispatch [:chat.ui/set-input-content-height constants/input-height]))
(defn update-input
[{:keys [input-ref]}
{:keys [text-value]}
input-text]
(when (and input-text (not= @text-value input-text))
(reset! text-value input-text)
(when @input-ref
(.setNativeProps ^js @input-ref (clj->js {:text input-text})))))
(defn count-lines
[s]
(-> s
(string/split #"\n" -1)
(butlast)
count))
(defn cursor-y-position-relative-to-container
[{:keys [scroll-y]}
{:keys [cursor-position text-value]}]
(let [sub-text (subs @text-value 0 @cursor-position)
sub-text-lines (count-lines sub-text)
scrolled-lines (Math/round (/ @scroll-y constants/line-height))
sub-text-lines-in-view (- sub-text-lines scrolled-lines)]
(* sub-text-lines-in-view constants/line-height)))
(defn calc-suggestions-position
[cursor-pos max-height size
{:keys [maximized?]}
{:keys [insets curr-height window-height keyboard-height edit reply]}]
(let [base (+ constants/composer-default-height (:bottom insets) 8)
base (+ base (- curr-height constants/input-height))
base (if edit
(+ base constants/edit-container-height)
base)
base (if reply
(+ base constants/reply-container-height)
base)
view-height (- window-height keyboard-height (:top insets))
container-height (bounded-val
(* (/ constants/mentions-max-height 4) size)
(/ constants/mentions-max-height 4)
constants/mentions-max-height)]
(if @maximized?
(if (< (+ cursor-pos container-height) max-height)
(+ constants/actions-container-height (:bottom insets))
(+ constants/actions-container-height (:bottom insets) (- max-height cursor-pos) 18))
(if (< (+ base container-height) view-height)
base
(+ constants/actions-container-height (:bottom insets) (- curr-height cursor-pos) 18)))))

View File

@ -7,11 +7,12 @@
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[status-im2.contexts.chat.bottom-sheet-composer.style :as style]
[status-im2.contexts.chat.bottom-sheet-composer.images.view :as images]
[status-im2.contexts.chat.bottom-sheet-composer.reply.view :as reply]
[utils.re-frame :as rf]
[status-im2.contexts.chat.bottom-sheet-composer.edit.view :as edit]
[status-im2.contexts.chat.bottom-sheet-composer.mentions.view :as mentions]
[status-im2.contexts.chat.bottom-sheet-composer.utils :as utils]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
[status-im2.contexts.chat.bottom-sheet-composer.actions.view :as actions]
@ -36,7 +37,8 @@
:sending-images? (atom nil)
:editing? (atom nil)
:record-permission? (atom nil)
:record-reset-fn (atom nil)}
:record-reset-fn (atom nil)
:scroll-y (atom 0)}
state {:text-value (reagent/atom "")
:cursor-position (reagent/atom 0)
:saved-cursor-position (reagent/atom 0)
@ -64,7 +66,7 @@
max-height (utils/calc-max-height window-height
kb-height
insets
(seq images)
(boolean (seq images))
reply
edit)
lines (utils/calc-lines @content-height)
@ -98,53 +100,60 @@
:window-height window-height
:lines lines
:max-lines max-lines}
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)]
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)
cursor-pos (utils/cursor-y-position-relative-to-container
props
state)]
(effects/initialize props
state
animations
dimensions
chat-input
keyboard-height
(seq images)
(boolean (seq images))
reply
edit
audio)
[gesture/gesture-detector
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
[reanimated/view
{:style (style/sheet-container insets state animations)
:on-layout #(handler/layout % state blur-height)}
[sub-view/bar]
[reply/view state]
[edit/view edit #(utils/cancel-edit-message state animations)]
[reanimated/touchable-opacity
{:active-opacity 1
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
:style (style/input-container (:height animations) max-height)
:accessibility-label :message-input-container}
[rn/text-input
{:ref #(reset! (:input-ref props) %)
:default-value @(:text-value state)
:on-focus #(handler/focus props state animations dimensions)
:on-blur #(handler/blur state animations dimensions images reply)
:on-content-size-change #(handler/content-size-change %
state
animations
dimensions
(or keyboard-shown edit))
:on-scroll #(handler/scroll % state animations dimensions)
:on-change-text #(handler/change-text % props state)
:on-selection-change #(handler/selection-change % state)
:max-height max-height
:max-font-size-multiplier 1
:multiline true
:placeholder (i18n/label :t/type-something)
:placeholder-text-color (colors/theme-colors colors/neutral-40 colors/neutral-50)
:style (style/input props state)
:accessibility-label :chat-message-input}]
[gradients/view props state animations show-bottom-gradient?]]
[images/images-list]
[actions/view props state animations window-height insets (seq images)]]]))]))])
(utils/update-input props state input-text)
[:<>
[mentions/view props state animations max-height cursor-pos]
[gesture/gesture-detector
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
[reanimated/view
{:style (style/sheet-container insets state animations)
:on-layout #(handler/layout % state blur-height)}
[sub-view/bar]
[reply/view state]
[edit/view edit #(utils/cancel-edit-message state animations)]
[reanimated/touchable-opacity
{:active-opacity 1
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
:style (style/input-container (:height animations) max-height)
:accessibility-label :message-input-container}
[rn/text-input
{:ref #(reset! (:input-ref props) %)
:default-value @(:text-value state)
:on-focus #(handler/focus props state animations dimensions)
:on-blur #(handler/blur state animations dimensions images reply)
:on-content-size-change #(handler/content-size-change %
state
animations
dimensions
(or keyboard-shown edit))
:on-scroll #(handler/scroll % props state animations dimensions)
:on-change-text #(handler/change-text % props state)
:on-selection-change #(handler/selection-change % state)
:max-height max-height
:max-font-size-multiplier 1
:multiline true
:placeholder (i18n/label :t/type-something)
:placeholder-text-color (colors/theme-colors colors/neutral-40 colors/neutral-50)
:style (style/input props state)
:accessibility-label :chat-message-input}]
[gradients/view props state animations show-bottom-gradient?]]
[images/images-list]
[actions/view props state animations window-height insets
(boolean (seq images))]]]]))]))])
(defn f-bottom-sheet-composer
[insets]