Reimplement composer: add reply (#15736)

* feat: add composer reply
This commit is contained in:
Omar Basem 2023-04-27 20:20:00 +04:00 committed by GitHub
parent 2eed30be8f
commit 3e9d5dde54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 291 additions and 72 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 913 B

View File

@ -51,12 +51,6 @@
(let [current-chat-id (or chat-id (:current-chat-id db))]
{:db (assoc-in db [:chat/inputs current-chat-id :input-maximized?] maximized?)}))
(rf/defn set-input-refocus
{:events [:chat.ui/set-input-refocus]}
[{db :db} refocus? chat-id]
(let [current-chat-id (or chat-id (:current-chat-id db))]
{:db (assoc-in db [:chat/inputs current-chat-id :input-refocus?] refocus?)}))
(rf/defn select-mention
{:events [:chat.ui/select-mention]}
[{:keys [db] :as cofx} text-input-ref {:keys [primary-name searched-text match public-key] :as user}]

View File

@ -158,3 +158,5 @@
["enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@test.waku.nodes.status.im"]})
(def default-kdf-iterations 3200)
(def ^:const new-composer-enabled? false)

View File

@ -88,17 +88,13 @@
(defn open-photo-selector
[{:keys [input-ref]}
{:keys [focused?]}
{:keys [height]}
insets]
(permissions/request-permissions
{:permissions [:read-external-storage :write-external-storage]
:on-allowed (fn []
(when platform/android?
(when @focused?
(rf/dispatch [:chat.ui/set-input-refocus true]))
(when @input-ref
(.blur ^js @input-ref)))
(when (and platform/android? @input-ref)
(.blur ^js @input-ref))
(rf/dispatch [:chat.ui/set-input-content-height
(reanimated/get-shared-value height)])
(rf/dispatch [:open-modal :photo-selector {:insets insets}]))
@ -108,9 +104,9 @@
:t/external-storage-denied)))}))
(defn image-button
[props state animations insets]
[props animations insets]
[quo/button
{:on-press #(open-photo-selector props state animations insets)
{:on-press #(open-photo-selector props animations insets)
:icon true
:type :outline
:size 32
@ -141,7 +137,7 @@
[rn/view {:style style/actions-container}
[rn/view {:style {:flex-direction :row}}
[camera-button]
[image-button props state animations insets]
[image-button props animations insets]
[reaction-button]
[format-button]]
[send-button props state animations window-height images?]

View File

@ -17,6 +17,8 @@
(def ^:const images-container-height 76)
(def ^:const reply-container-height 32)
(def ^:const extra-content-offset (if platform/ios? 6 0))
(def ^:const content-change-threshold 10)

View File

@ -5,7 +5,6 @@
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
[status-im2.contexts.chat.bottom-sheet-composer.keyboard :as kb]
[utils.re-frame :as rf]
[utils.number :as utils.number]))
(defn reenter-screen-effect
@ -29,13 +28,6 @@
(reanimated/set-shared-value saved-height max-height)
(reanimated/set-shared-value last-height max-height)))
(defn refocus-effect
[{:keys [input-ref]}
{:keys [input-refocus?]}]
(when (and input-refocus? @input-ref)
(.focus ^js @input-ref)
(rf/dispatch [:chat.ui/set-input-refocus false])))
(defn layout-effect
[{:keys [lock-layout?]}]
(when-not @lock-layout?
@ -56,17 +48,29 @@
(reanimated/set-shared-value background-y 0)
(reanimated/animate opacity 1)))
(defn images-effect
(defn images-or-reply-effect
[{:keys [container-opacity]}
images?]
(when images?
(reanimated/animate container-opacity 1)))
{:keys [replying? sending-images? input-ref]}
images? reply?]
(when (or images? reply?)
(reanimated/animate container-opacity 1))
(when (and (not @sending-images?) images? @input-ref)
(.focus ^js @input-ref)
(reset! sending-images? true))
(when (and (not @replying?) reply? @input-ref)
(.focus ^js @input-ref)
(reset! replying? true))
(when-not images?
(reset! sending-images? false))
(when-not reply?
(reset! replying? false)))
(defn empty-effect
[{:keys [text-value maximized? focused?]}
{:keys [container-opacity]}
images?]
(when (and (empty? @text-value) (not images?) (not @maximized?) (not @focused?))
images?
reply?]
(when (and (empty? @text-value) (not images?) (not reply?) (not @maximized?) (not @focused?))
(reanimated/animate-delay container-opacity constants/empty-opacity 200)))
(defn component-will-unmount
@ -76,17 +80,16 @@
(.remove ^js @keyboard-frame-listener))
(defn initialize
[props state animations {:keys [max-height] :as dimensions} chat-input keyboard-height images?]
[props state animations {:keys [max-height] :as dimensions} chat-input keyboard-height images? reply?]
(rn/use-effect
(fn []
(maximized-effect state animations dimensions chat-input)
(refocus-effect props chat-input)
(reenter-screen-effect state dimensions chat-input)
(layout-effect state)
(kb-default-height-effect state)
(background-effect state animations dimensions chat-input)
(images-effect animations images?)
(empty-effect state animations images?)
(images-or-reply-effect animations props images? reply?)
(empty-effect state animations images? reply?)
(kb/add-kb-listeners props state animations dimensions keyboard-height)
#(component-will-unmount props))
[max-height]))

View File

@ -34,7 +34,8 @@
maximized?]}
{:keys [height saved-height last-height gradient-opacity container-opacity opacity background-y]}
{:keys [lines content-height max-height window-height]}
images]
images
reply]
(let [min-height (utils/get-min-height lines)
reopen-height (utils/calc-reopen-height text-value min-height content-height saved-height)]
(reset! focused? false)
@ -43,7 +44,7 @@
(reanimated/set-shared-value saved-height min-height)
(reanimated/animate opacity 0)
(js/setTimeout #(reanimated/set-shared-value background-y (- window-height)) 300)
(when (and (empty? @text-value) (empty? images))
(when (utils/empty-input? @text-value images reply)
(reanimated/animate container-opacity constants/empty-opacity))
(reanimated/animate gradient-opacity 0)
(reset! lock-selection? true)

View File

@ -0,0 +1,41 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.reply.style)
(defn reply-content
[pin?]
{:padding-right (when-not pin? 10)
:flex 1
:flex-direction :row})
(defn quoted-message
[pin?]
(merge {:flex-direction :row
:flex 1
:align-items :center}
(when-not pin?
{:left 22
:margin-right 22})))
(def reply-from
{:flex-direction :row
:align-items :center})
(def message-author-text
{:margin-left 4})
(def message-text
{:text-transform :none
:margin-left 4
:margin-top 2
:flex 1})
(def gradient
{:position :absolute
:right 0
:top 0
:bottom 0
:width "50%"})
(def reply-deleted-message
{:text-transform :none
:margin-left 4
:margin-top 2})

View File

@ -0,0 +1,152 @@
(ns status-im2.contexts.chat.bottom-sheet-composer.reply.view
(:require [clojure.string :as string]
[react-native.core :as rn]
[react-native.reanimated :as reanimated]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
[utils.i18n :as i18n]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[status-im2.constants :as constant]
[status-im.ethereum.stateofus :as stateofus]
[utils.re-frame :as rf]
[status-im2.contexts.chat.bottom-sheet-composer.reply.style :as style]
[react-native.linear-gradient :as linear-gradient]))
(defn get-quoted-text-with-mentions
[parsed-text]
(string/join
(mapv (fn [{:keys [type literal children]}]
(cond
(= type "paragraph")
(get-quoted-text-with-mentions children)
(= type "mention")
(rf/sub [:messages/resolve-mention literal])
(seq children)
(get-quoted-text-with-mentions children)
:else
literal))
parsed-text)))
(defn format-author
[contact-name]
(let [author (if (or (= (first contact-name) "@")
;; in case of replies
(= (second contact-name) "@"))
(or (stateofus/username contact-name)
(subs contact-name 0 81))
contact-name)]
author))
(defn format-reply-author
[from username current-public-key]
(or (and (= from current-public-key)
(i18n/label :t/You))
(when username (format-author username))))
(defn reply-deleted-message
[]
[rn/view
{:style {:flex-direction :row
:align-items :center}}
[quo/icon :i/sad-face {:size 16}]
[quo/text
{:number-of-lines 1
:size :label
:weight :regular
:accessibility-label :quoted-message
:style style/reply-deleted-message}
(i18n/label :t/message-deleted)]])
(defn reply-from
[{:keys [from contact-name current-public-key]}]
(let [display-name (first (rf/sub [:contacts/contact-two-names-by-identity from]))
contact (rf/sub [:contacts/contact-by-address from])
photo-path (when-not (empty? (:images contact)) (rf/sub [:chats/photo-path from]))]
[rn/view {:style style/reply-from}
[quo/user-avatar
{:full-name display-name
:profile-picture photo-path
:status-indicator? false
:size :xxxs}]
[quo/text
{:weight :semi-bold
:size :paragraph-2
:number-of-lines 1
:style style/message-author-text}
(format-reply-author from contact-name current-public-key)]]))
(defn reply-message
[{:keys [from identicon content-type contentType parsed-text content deleted? deleted-for-me?
album-images-count]}
in-chat-input? pin? recording-audio?]
(let [contact-name (rf/sub [:contacts/contact-name-by-identity from])
current-public-key (rf/sub [:multiaccount/public-key])
content-type (or content-type contentType)]
[rn/view
{:style {:flex-direction :row
:height (when-not pin? 24)
:accessibility-label :reply-message}}
[rn/view {:style (style/reply-content pin?)}
(when-not pin?
[quo/icon :i/connector
{:size 16
:color (colors/theme-colors colors/neutral-40 colors/neutral-60)
:container-style {:position :absolute :left 0 :bottom -4 :width 16 :height 16}}])
(if (or deleted? deleted-for-me?)
[rn/view {:style (style/quoted-message pin?)}
[reply-deleted-message]]
[rn/view {:style (style/quoted-message pin?)}
[reply-from
{:from from
:identicon identicon
:contact-name contact-name
:current-public-key current-public-key}]
[quo/text
{:number-of-lines 1
:size :label
:weight :regular
:accessibility-label :quoted-message
:ellipsize-mode :tail
:style (merge
style/message-text
(when (or (= constant/content-type-image content-type)
(= constant/content-type-sticker content-type)
(= constant/content-type-audio content-type))
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40)}))}
(case (or content-type contentType)
constant/content-type-image (if album-images-count
(i18n/label :t/images-albums-count
{:album-images-count album-images-count})
(i18n/label :t/image))
constant/content-type-sticker (i18n/label :t/sticker)
constant/content-type-audio (i18n/label :t/audio)
(get-quoted-text-with-mentions (or parsed-text (:parsed-text content))))]])]
(when (and in-chat-input? (not recording-audio?))
[quo/button
{:icon true
:type :outline
:size 24
:on-press #(rf/dispatch [:chat.ui/cancel-message-reply])}
:i/close])
(when (and in-chat-input? recording-audio?)
[linear-gradient/linear-gradient
{:colors [(colors/theme-colors colors/white-opa-0 colors/neutral-90-opa-0)
(colors/theme-colors colors/white colors/neutral-90)]
:start {:x 0 :y 0}
:end {:x 0.7 :y 0}
:style style/gradient}])]))
(defn- f-view
[]
(let [reply (rf/sub [:chats/reply-message])
height (reanimated/use-shared-value (if reply constants/reply-container-height 0))]
(rn/use-effect #(reanimated/animate height (if reply constants/reply-container-height 0)) [reply])
[reanimated/view {:style (reanimated/apply-animations-to-style {:height height} {})}
(when reply [reply-message reply true false false])]))
(defn view
[]
[:f> f-view])

View File

@ -6,16 +6,16 @@
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]))
(defn shadow
[lines]
[elevation?]
(if platform/ios?
{:shadow-radius 20
:shadow-opacity (colors/theme-colors 0.1 0.7)
:shadow-color colors/neutral-100
:shadow-offset {:width 0 :height (colors/theme-colors -4 -8)}}
{:elevation (if (> lines 1) 10 0)}))
{:elevation (if elevation? 10 0)}))
(defn sheet-container
[insets opacity lines]
[insets opacity elevation?]
(reanimated/apply-animations-to-style
{:opacity opacity}
(merge
@ -29,7 +29,7 @@
:background-color (colors/theme-colors colors/white colors/neutral-95)
:z-index 3
:padding-bottom (:bottom insets)}
(shadow lines))))
(shadow elevation?))))
(def bar-container
{:height constants/bar-container-height

View File

@ -60,17 +60,21 @@
(if platform/ios? lines (dec lines))))
(defn calc-max-height
[window-height kb-height insets images]
[window-height kb-height insets images? reply?]
(let [margin-top (if platform/ios? (:top insets) (+ 10 (:top insets)))
max-height (- window-height
margin-top
kb-height
constants/bar-container-height
constants/actions-container-height)]
(if (seq images)
(- max-height constants/images-container-height)
max-height)))
constants/actions-container-height)
max-height (if images? (- max-height constants/images-container-height) max-height)
max-height (if reply? (- max-height constants/reply-container-height) max-height)]
max-height))
(defn empty-input?
[input-text images]
(and (nil? input-text) (empty? images)))
[text images reply?]
(and (empty? text) (empty? images) (not reply?)))
(defn android-elevation?
[lines images reply?]
(or (> lines 1) (seq images) reply?))

View File

@ -9,6 +9,7 @@
[utils.i18n :as i18n]
[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.utils :as utils]
[status-im2.contexts.chat.bottom-sheet-composer.constants :as constants]
@ -29,7 +30,9 @@
:keyboard-frame-listener (atom nil)
:keyboard-hide-listener (atom nil)
:emoji-kb-extra-height (atom nil)
:saved-emoji-kb-extra-height (atom nil)}
:saved-emoji-kb-extra-height (atom nil)
:replying? (atom nil)
:sending-images? (atom nil)}
state {:text-value (reagent/atom "")
:cursor-position (reagent/atom 0)
:saved-cursor-position (reagent/atom 0)
@ -43,6 +46,7 @@
[:f>
(fn []
(let [images (rf/sub [:chats/sending-image])
reply (rf/sub [:chats/reply-message])
{:keys [input-text input-content-height]
:as chat-input} (rf/sub [:chats/current-chat-input])
content-height (reagent/atom (or input-content-height
@ -53,7 +57,8 @@
max-height (utils/calc-max-height window-height
kb-height
insets
images)
(seq images)
reply)
lines (utils/calc-lines @content-height)
max-lines (utils/calc-lines max-height)
initial-height (if (> lines 1)
@ -64,7 +69,8 @@
:container-opacity (reanimated/use-shared-value
(if (utils/empty-input?
input-text
images)
images
reply)
0.7
1))
:height (reanimated/use-shared-value
@ -83,20 +89,25 @@
: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)
android-elevation? (utils/android-elevation? lines images reply)]
(effects/initialize props
state
animations
dimensions
chat-input
keyboard-height
(seq images))
(seq images)
reply)
[gesture/gesture-detector
{:gesture (drag-gesture/drag-gesture props state animations dimensions keyboard-shown)}
[reanimated/view
{:style (style/sheet-container insets (:container-opacity animations) lines)
{:style (style/sheet-container insets
(:container-opacity animations)
android-elevation?)
:on-layout #(handler/layout % state blur-height)}
[sub-view/bar]
[reply/view]
[reanimated/touchable-opacity
{:active-opacity 1
:on-press (when @(:input-ref props) #(.focus ^js @(:input-ref props)))
@ -106,7 +117,7 @@
{: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)
:on-blur #(handler/blur state animations dimensions images reply)
:on-content-size-change #(handler/content-size-change %
state
animations

View File

@ -150,22 +150,27 @@
{:position :absolute
:bottom (+ (:bottom insets) composer.constants/composer-default-height 6)}]])))
(defn use-keyboard-visibility
[]
(let [show-listener (atom nil)
hide-listener (atom nil)
shown? (atom nil)]
(rn/use-effect
(fn []
(reset! show-listener
(.addListener rn/keyboard "keyboardWillShow" #(reset! shown? true)))
(reset! hide-listener
(.addListener rn/keyboard "keyboardWillHide" #(reset! shown? false)))
(fn []
(.remove ^js @show-listener)
(.remove ^js @hide-listener))))
{:shown? shown?}))
(defn- f-messages-list
[chat insets]
(let [{keyboard-shown? :shown?} (use-keyboard-visibility)]
[messages-list-content chat insets keyboard-shown?]))
(defn messages-list
[chat insets]
[:f>
(fn []
(let [keyboard-show-listener (atom nil)
keyboard-hide-listener (atom nil)
keyboard-shown (atom false)]
(rn/use-effect
(fn []
(reset! keyboard-show-listener (.addListener rn/keyboard
"keyboardWillShow"
#(reset! keyboard-shown true)))
(reset! keyboard-hide-listener (.addListener rn/keyboard
"keyboardWillHide"
#(reset! keyboard-shown false)))
(fn []
(.remove ^js @keyboard-show-listener)
(.remove ^js @keyboard-hide-listener))))
[messages-list-content chat insets keyboard-shown]))])
[:f> f-messages-list chat insets])

View File

@ -4,11 +4,14 @@
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[reagent.core :as reagent]
[status-im2.config :as config]
[status-im2.constants :as constants]
[status-im2.contexts.chat.bottom-sheet-composer.view :as bottom-sheet-composer]
[status-im2.contexts.chat.messages.composer.view :as composer]
[status-im2.contexts.chat.messages.contact-requests.bottom-drawer :as
contact-requests.bottom-drawer]
[status-im2.contexts.chat.messages.list.view :as messages.list]
[status-im2.contexts.chat.messages.list.new-temp-view :as messages.list.new]
[status-im2.contexts.chat.messages.pin.banner.view :as pin.banner]
[status-im2.navigation.state :as navigation.state]
[utils.debounce :as debounce]
@ -75,10 +78,15 @@
:keyboardVerticalOffset (- (:bottom insets))}
[page-nav]
[pin.banner/banner chat-id]
[messages.list/messages-list chat insets]
(if config/new-composer-enabled?
[messages.list.new/messages-list chat insets]
[messages.list/messages-list chat insets])
(if-not able-to-send-message?
[contact-requests.bottom-drawer/view chat-id contact-request-state group-chat]
[:f> composer/f-composer chat-id insets])]))
(if config/new-composer-enabled?
[bottom-sheet-composer/bottom-sheet-composer insets]
[:f> composer/f-composer chat-id insets]))]))
(defn chat
[]