Compare commits

...
Author SHA1 Message Date
Omar Basem d433285acb selectable-input 2023-05-04 10:26:51 +04:00
Omar Basem eb16f4b6c4 revert podfile.lock 2023-05-04 09:17:53 +04:00
Omar Basem 9268060aa9 review 2023-05-04 09:16:53 +04:00
Omar Basem 7ab9e7abd0 review 2023-05-04 09:16:52 +04:00
Omar Basem 2430bde3d5 review 2023-05-04 09:16:52 +04:00
Omar Basem 739556f2b4 review 2023-05-04 09:16:52 +04:00
Omar Basem f4ac7a4c51 lint 2023-05-04 09:16:51 +04:00
Omar Basem 36fac66eac feat: mentions new design
review

lint

review

udpates

updates

updates

udpates

updates

updates

lint

updates

composer shell button

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates

updates
2023-05-04 09:16:51 +04:00
9 changed files with 352 additions and 129 deletions
+15 -11
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
+1 -1
View File
@@ -159,4 +159,4 @@
(def default-kdf-iterations 3200)
(def ^:const new-composer-enabled? false)
(def ^:const new-composer-enabled? true)
@@ -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)
@@ -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,10 +109,12 @@
(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]}]
(println "selectionc ahnge")
(when-not @lock-selection?
(reset! cursor-position (oops/oget event "nativeEvent.selection.end"))))
@@ -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))))
@@ -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]))
@@ -0,0 +1,42 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.selectable-input.view
(:require [utils.i18n :as i18n]))
(declare first-level-menu-items second-level-menu-items)
(defn calculate-input-text
[{:keys [full-text selection-start selection-end]} content]
(let [head (subs full-text 0 selection-start)
tail (subs full-text selection-end)]
(str head content tail)))
(defn reset-to-first-level-menu
[first-level menu-items]
(reset! first-level true)
(reset! menu-items first-level-menu-items))
(defn append-markdown-char
[{:keys [first-level menu-items content selection-start selection-end text-input-handle
selection-event]
:as params} wrap-chars]
(let [content (str wrap-chars content wrap-chars)
new-text (calculate-input-text params content)
len-wrap-chars (count wrap-chars)
selection-start (+ selection-start len-wrap-chars)
selection-end (+ selection-end len-wrap-chars)]
;don't update selection directly here, process it within on-selection-change instead
;so that we can avoid java.lang.IndexOutOfBoundsException: setSpan..
(reset! selection-event {:start selection-start
:end selection-end
:text-input-handle text-input-handle})
;(update-input-text params new-text)
(reset-to-first-level-menu first-level menu-items)))
(def second-level-menus
{:bold #(append-markdown-char % "**")
:italic #(append-markdown-char % "*")
:strikethrough #(append-markdown-char % "~~")})
(def second-level-menu-items (map i18n/label (keys second-level-menus)))
@@ -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)))))
@@ -7,11 +7,13 @@
[react-native.reanimated :as reanimated]
[reagent.core :as reagent]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[oops.core :as oops]
[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]
@@ -20,131 +22,150 @@
[status-im2.contexts.chat.bottom-sheet-composer.effects :as effects]
[status-im2.contexts.chat.bottom-sheet-composer.gesture :as drag-gesture]
[status-im2.contexts.chat.bottom-sheet-composer.handlers :as handler]
[status-im2.contexts.chat.bottom-sheet-composer.gradients.view :as gradients]))
[status-im2.contexts.chat.bottom-sheet-composer.gradients.view :as gradients]
[status-im2.contexts.chat.bottom-sheet-composer.selectable-input.view :as selectable-input]))
(defn sheet
[insets window-height blur-height opacity background-y]
[:f>
(fn []
(let [props {:input-ref (atom nil)
:keyboard-show-listener (atom nil)
:keyboard-frame-listener (atom nil)
:keyboard-hide-listener (atom nil)
:emoji-kb-extra-height (atom nil)
:saved-emoji-kb-extra-height (atom nil)
:replying? (atom nil)
:sending-images? (atom nil)
:editing? (atom nil)
:record-permission? (atom nil)
:record-reset-fn (atom nil)}
state {:text-value (reagent/atom "")
:cursor-position (reagent/atom 0)
:saved-cursor-position (reagent/atom 0)
:gradient-z-index (reagent/atom 0)
:kb-default-height (reagent/atom nil)
:gesture-enabled? (reagent/atom true)
:lock-selection? (reagent/atom true)
:focused? (reagent/atom false)
:lock-layout? (reagent/atom false)
:maximized? (reagent/atom false)
:recording? (reagent/atom false)}]
(let [props {:input-ref (atom nil)
:keyboard-show-listener (atom nil)
:keyboard-frame-listener (atom nil)
:keyboard-hide-listener (atom nil)
:emoji-kb-extra-height (atom nil)
:saved-emoji-kb-extra-height (atom nil)
:replying? (atom nil)
:sending-images? (atom nil)
:editing? (atom nil)
:record-permission? (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)
:gradient-z-index (reagent/atom 0)
:kb-default-height (reagent/atom nil)
:gesture-enabled? (reagent/atom true)
:lock-selection? (reagent/atom true)
:focused? (reagent/atom false)
:lock-layout? (reagent/atom false)
:maximized? (reagent/atom false)
:recording? (reagent/atom false)}
selectable-input-ref (atom nil)
menu-items (reagent/atom selectable-input/first-level-menu-items)
manager (rn/selectable-text-input-manager)]
[:f>
(fn []
(let [images (rf/sub [:chats/sending-image])
audio (rf/sub [:chats/sending-audio])
reply (rf/sub [:chats/reply-message])
edit (rf/sub [:chats/edit-message])
(let [images (rf/sub [:chats/sending-image])
audio (rf/sub [:chats/sending-audio])
reply (rf/sub [:chats/reply-message])
edit (rf/sub [:chats/edit-message])
{:keys [input-text input-content-height]
:as chat-input} (rf/sub [:chats/current-chat-input])
content-height (reagent/atom (or input-content-height
constants/input-height))
:as chat-input} (rf/sub [:chats/current-chat-input])
content-height (reagent/atom (or input-content-height
constants/input-height))
{:keys [keyboard-shown keyboard-height]} (hooks/use-keyboard)
kb-height (kb/get-kb-height keyboard-height
@(:kb-default-height state))
max-height (utils/calc-max-height window-height
kb-height
insets
(seq images)
reply
edit)
lines (utils/calc-lines @content-height)
max-lines (utils/calc-lines max-height)
initial-height (if (> lines 1)
constants/multiline-minimized-height
constants/input-height)
animations {:gradient-opacity (reanimated/use-shared-value
0)
:container-opacity (reanimated/use-shared-value
(if (utils/empty-input?
input-text
images
reply
audio)
0.7
1))
:height (reanimated/use-shared-value
initial-height)
:saved-height (reanimated/use-shared-value
initial-height)
:last-height (reanimated/use-shared-value
(utils/bounded-val
@content-height
constants/input-height
max-height))
:opacity opacity
:background-y background-y}
dimensions {:content-height content-height
:max-height max-height
:window-height window-height
:lines lines
:max-lines max-lines}
show-bottom-gradient? (utils/show-bottom-gradient? state dimensions)]
kb-height (kb/get-kb-height keyboard-height
@(:kb-default-height state))
max-height (utils/calc-max-height window-height
kb-height
insets
(boolean (seq images))
reply
edit)
lines (utils/calc-lines @content-height)
max-lines (utils/calc-lines max-height)
initial-height (if (> lines 1)
constants/multiline-minimized-height
constants/input-height)
animations {:gradient-opacity (reanimated/use-shared-value
0)
:container-opacity (reanimated/use-shared-value
(if (utils/empty-input?
input-text
images
reply
audio)
0.7
1))
:height (reanimated/use-shared-value
initial-height)
:saved-height (reanimated/use-shared-value
initial-height)
:last-height (reanimated/use-shared-value
(utils/bounded-val
@content-height
constants/input-height
max-height))
:opacity opacity
:background-y background-y}
dimensions {:content-height content-height
:max-height max-height
:window-height window-height
:lines lines
:max-lines max-lines}
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)]]]))]))])
(rn/use-effect (fn []
(let [selectable-text-input-handle (rn/find-node-handle @selectable-input-ref)
text-input-handle (rn/find-node-handle @(:input-ref props))]
(oops/ocall manager :setupMenuItems selectable-text-input-handle text-input-handle))))
(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/selectable-text-input {:ref #(reset! selectable-input-ref %)
:menuItems @menu-items}
[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)
:on-selection (fn [e] (println "onselection"))
: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]