diff --git a/resources/images/ui/hand-wave.png b/resources/images/ui/hand-wave.png new file mode 100644 index 0000000000..d761bd07d6 Binary files /dev/null and b/resources/images/ui/hand-wave.png differ diff --git a/resources/images/ui/hand-wave@2x.png b/resources/images/ui/hand-wave@2x.png new file mode 100644 index 0000000000..80fd494cd9 Binary files /dev/null and b/resources/images/ui/hand-wave@2x.png differ diff --git a/resources/images/ui/hand-wave@3x.png b/resources/images/ui/hand-wave@3x.png new file mode 100644 index 0000000000..966468d61e Binary files /dev/null and b/resources/images/ui/hand-wave@3x.png differ diff --git a/src/quo/components/text.cljs b/src/quo/components/text.cljs index fa26429271..1434a0b7eb 100644 --- a/src/quo/components/text.cljs +++ b/src/quo/components/text.cljs @@ -28,6 +28,7 @@ :inherit nil) (case (or size :base) :tiny typography/tiny + :x-small typography/x-small :small typography/small :base typography/base :large typography/large diff --git a/src/quo/design_system/typography.cljs b/src/quo/design_system/typography.cljs index e37f71eaff..dddf0bd86f 100644 --- a/src/quo/design_system/typography.cljs +++ b/src/quo/design_system/typography.cljs @@ -3,6 +3,9 @@ (def tiny {:font-size 10 :line-height 14}) +(def x-small {:font-size 12 + :line-height 16}) + (def small {:font-size 13 :line-height 18}) diff --git a/src/status_im/chat/models/input.cljs b/src/status_im/chat/models/input.cljs index 023dc47445..4587bffd49 100644 --- a/src/status_im/chat/models/input.cljs +++ b/src/status_im/chat/models/input.cljs @@ -143,6 +143,20 @@ :on-error #(log/error "failed to delete message message " %) :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]}) +(fx/defn show-contact-request-input + "Sets reference to previous chat message and focuses on input" + {:events [:chat.ui/send-contact-request]} + [{:keys [db] :as cofx}] + (let [current-chat-id (:current-chat-id db)] + {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :sending-contact-request] + current-chat-id) + (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] + nil) + (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] nil) + (update-in [:chat/inputs current-chat-id :metadata] + dissoc :sending-image))})) + (fx/defn cancel-message-reply "Cancels stage message reply" {:events [:chat.ui/cancel-message-reply]} @@ -179,6 +193,7 @@ (fx/defn clean-input [{:keys [db] :as cofx} current-chat-id] (fx/merge cofx {:db (-> db + (assoc-in [:chat/inputs current-chat-id :metadata :sending-contact-request] nil) (assoc-in [:chat/inputs current-chat-id :metadata :sending-image] nil) (assoc-in [:chat/inputs current-chat-id :metadata :editing-message] nil) (assoc-in [:chat/inputs current-chat-id :metadata :responding-to-message] nil))} @@ -262,6 +277,34 @@ (mentions/clear-mentions) (mentions/clear-cursor)))) +(fx/defn send-contact-request + {:events [:contacts/send-contact-request]} + [{:keys [db] :as cofx} public-key message] + (fx/merge cofx + {::json-rpc/call [{:method "wakuext_sendContactRequest" + :js-response true + :params [{:id public-key :message message}] + :on-error #(log/warn "failed to send a contact request" %) + + :on-success #(re-frame/dispatch [:transport/message-sent %])}]} + + (mentions/clear-mentions) + (mentions/clear-cursor) + (clean-input (:current-chat-id db)) + (process-cooldown))) + +(fx/defn cancel-contact-request + "Cancels contact request" + {:events [:chat.ui/cancel-contact-request]} + [{:keys [db] :as cofx}] + (let [current-chat-id (:current-chat-id db)] + (fx/merge cofx + {:db (assoc-in db [:chat/inputs current-chat-id :metadata :sending-contact-request] nil)} + (mentions/clear-mentions) + (mentions/clear-cursor) + (clean-input (:current-chat-id db)) + (process-cooldown)))) + (fx/defn chat-send-sticker {:events [:chat/send-sticker]} [{{:keys [current-chat-id] :as db} :db :as cofx} {:keys [hash packID] :as sticker}] diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 2b719e6606..59e98ac5ff 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -13,6 +13,13 @@ (def ^:const content-type-audio 8) (def ^:const content-type-community 9) (def ^:const content-type-gap 10) +(def ^:const content-type-contact-request 11) ;; TODO: temp, will be removed + +(def ^:const contact-request-state-none 0) +(def ^:const contact-request-state-mutual 1) +(def ^:const contact-request-state-sent 2) +(def ^:const contact-request-state-received 3) +(def ^:const contact-request-state-dismissed 4) (def ^:const emoji-reaction-love 1) (def ^:const emoji-reaction-thumbs-up 2) @@ -28,6 +35,10 @@ (def ^:const timeline-chat-type 5) (def ^:const community-chat-type 6) +(def ^:const contact-request-message-state-pending 1) +(def ^:const contact-request-message-state-accepted 2) +(def ^:const contact-request-message-state-declined 3) + (def request-to-join-pending-state 1) (def reactions {emoji-reaction-love (:love resources/reactions) @@ -161,6 +172,8 @@ (def ^:const activity-center-notification-type-private-group-chat 2) (def ^:const activity-center-notification-type-mention 3) (def ^:const activity-center-notification-type-reply 4) +(def ^:const activity-center-notification-type-contact-request 5) +(def ^:const activity-center-notification-type-contact-request-retracted 6) (def ^:const visibility-status-unknown 0) (def ^:const visibility-status-automatic 1) @@ -173,4 +186,4 @@ (def ^:const sticker-pack-status-installed 1) (def ^:const sticker-pack-status-pending 2) -(def ^:const sticker-pack-status-owned 3) \ No newline at end of file +(def ^:const sticker-pack-status-owned 3) diff --git a/src/status_im/contact/core.cljs b/src/status_im/contact/core.cljs index 24b6f48047..2dfe01d071 100644 --- a/src/status_im/contact/core.cljs +++ b/src/status_im/contact/core.cljs @@ -5,6 +5,7 @@ [status-im.ethereum.json-rpc :as json-rpc] [status-im.navigation :as navigation] [status-im.utils.fx :as fx] + [status-im.async-storage.core :as async-storage] [taoensso.timbre :as log] [status-im.constants :as constants] [status-im.contact.block :as contact.block])) @@ -28,13 +29,6 @@ (= public-key (:public-key multiaccount)) (assoc :name (:name multiaccount)))) -(defn- own-info - [db] - (let [{:keys [name preferred-name identicon address]} (:multiaccount db)] - {:name (or preferred-name name) - :profile-image identicon - :address address})) - (fx/defn ensure-contacts [{:keys [db]} contacts chats] (let [events @@ -66,6 +60,13 @@ (when (> (count events) 1) {:dispatch-n events})))) +(defn- own-info + [db] + (let [{:keys [name preferred-name identicon address]} (:multiaccount db)] + {:name (or preferred-name name) + :profile-image identicon + :address address})) + (fx/defn send-contact-request {:events [::send-contact-request]} [{:keys [db] :as cofx} public-key] @@ -92,12 +93,33 @@ "Remove a contact from current account's contact list" {:events [:contact.ui/remove-contact-pressed]} [{:keys [db]} {:keys [public-key]}] - {:db (assoc-in db [:contacts/contacts public-key :added] false) + {:db (-> db + (assoc-in [:contacts/contacts public-key :added] false) + (assoc-in [:contacts/contacts public-key :contact-request-state] constants/contact-request-state-none)) ::json-rpc/call [{:method "wakuext_removeContact" :params [public-key] + :on-success #(log/debug "contact removed successfully")} + {:method "wakuext_retractContactRequest" + :params [{:contactId public-key}] :on-success #(log/debug "contact removed successfully")}] :dispatch [:offload-messages constants/timeline-chat-id]}) +(fx/defn accept-contact-request + {:events [:contact-requests.ui/accept-request]} + [{:keys [db]} id] + {::json-rpc/call [{:method "wakuext_acceptContactRequest" + :params [{:id id}] + :js-response true + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]}) + +(fx/defn decline-contact-request + {:events [:contact-requests.ui/decline-request]} + [{:keys [db]} id] + {::json-rpc/call [{:method "wakuext_dismissContactRequest" + :params [{:id id}] + :js-response true + :on-success #(re-frame/dispatch [:sanitize-messages-and-process-response %])}]}) + (fx/defn initialize-contacts [cofx] (contacts-store/fetch-contacts-rpc cofx #(re-frame/dispatch [::contacts-loaded %]))) @@ -119,3 +141,9 @@ nickname #(re-frame/dispatch [:sanitize-messages-and-process-response %])) (navigation/navigate-back))) + +(fx/defn switch-mutual-contact-requests-enabled + {:events [:multiaccounts.ui/switch-mutual-contact-requests-enabled]} + [{:keys [db]} enabled?] + {::async-storage/set! {:mutual-contact-requests-enabled? enabled?} + :db (assoc db :mutual-contact-requests/enabled? enabled?)}) diff --git a/src/status_im/data_store/contacts.cljs b/src/status_im/data_store/contacts.cljs index 09a0fdf707..8f06d9d310 100644 --- a/src/status_im/data_store/contacts.cljs +++ b/src/status_im/data_store/contacts.cljs @@ -5,13 +5,18 @@ [taoensso.timbre :as log])) (defn <-rpc [contact] - (clojure.set/rename-keys contact {:id :public-key - :ensVerifiedAt :ens-verified-at - :ensVerified :ens-verified - :ensVerificationRetries :ens-verification-retries - :lastENSClockValue :last-ens-clock-value - :lastUpdated :last-updated - :localNickname :nickname})) + (-> contact + (clojure.set/rename-keys {:id :public-key + :ensVerifiedAt :ens-verified-at + :ensVerified :ens-verified + :ensVerificationRetries :ens-verification-retries + :hasAddedUs :has-added-us + :contactRequestState :contact-request-state + :lastENSClockValue :last-ens-clock-value + :lastUpdated :last-updated + :localNickname :nickname}) + (assoc :mutual? (and (:added contact) + (:hasAddedUs contact))))) (fx/defn fetch-contacts-rpc [_ on-success] diff --git a/src/status_im/data_store/contacts_test.cljs b/src/status_im/data_store/contacts_test.cljs index 294d1317b6..03cb559829 100644 --- a/src/status_im/data_store/contacts_test.cljs +++ b/src/status_im/data_store/contacts_test.cljs @@ -10,6 +10,7 @@ :lastUpdated 1} expected-contact {:public-key "pk" :address "address" + :mutual? nil :name "name" :identicon "identicon" :last-updated 1}] diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 65ba03006b..0b37e434b2 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -20,6 +20,7 @@ (clojure.set/rename-keys {:id :message-id :whisperTimestamp :whisper-timestamp :editedAt :edited-at + :contactRequestState :contact-request-state :commandParameters :command-parameters :gapParameters :gap-parameters :messageType :message-type diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs index 84c65131bb..2891d1e65a 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -61,4 +61,4 @@ ::call (fn [params] (doseq [param params] - (call param)))) \ No newline at end of file + (call param)))) diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs index d27d1ffdd3..0a8c587ce1 100644 --- a/src/status_im/multiaccounts/login/core.cljs +++ b/src/status_im/multiaccounts/login/core.cljs @@ -94,6 +94,13 @@ :wallet-connect-enabled? #(re-frame/dispatch [:multiaccounts.ui/switch-wallet-connect-enabled %])))) +(re-frame/reg-fx + ::initialize-mutual-contact-requests + (fn [] + (async-storage/get-item + :mutual-contact-requests-enabled? + #(re-frame/dispatch [:multiaccounts.ui/switch-mutual-contact-requests-enabled %])))) + (defn rpc->accounts [accounts] (reduce (fn [acc {:keys [chat type wallet] :as account}] (if chat @@ -359,6 +366,10 @@ [cofx] {::initialize-wallet-connect nil}) +(fx/defn initialize-mutual-contact-requests + [cofx] + {::initialize-mutual-contact-requests nil}) + (fx/defn get-node-config-callback {:events [::get-node-config-callback]} [{:keys [db] :as cofx} node-config-json] @@ -393,6 +404,7 @@ (initialize-appearance) (initialize-communities-enabled) (initialize-wallet-connect) + (initialize-mutual-contact-requests) (get-node-config) (communities/fetch) (logging/set-log-level (:log-level multiaccount)) diff --git a/src/status_im/notifications_center/core.cljs b/src/status_im/notifications_center/core.cljs index 41c53677d6..81ab197167 100644 --- a/src/status_im/notifications_center/core.cljs +++ b/src/status_im/notifications_center/core.cljs @@ -1,23 +1,38 @@ (ns status-im.notifications-center.core (:require [status-im.utils.fx :as fx] [status-im.ethereum.json-rpc :as json-rpc] + [status-im.constants :as constants] [taoensso.timbre :as log] [re-frame.core :as re-frame] [status-im.data-store.activities :as data-store.activities])) +(def non-dismissable-notifications + #{constants/activity-center-notification-type-contact-request + constants/activity-center-notification-type-contact-request-retracted}) + (fx/defn handle-activities [{:keys [db]} activities] (let [{:keys [unread-count notifications]} (reduce (fn [acc {:keys [read dismissed accepted] :as notification}] - (as-> acc a - (if read - (update a :unread-count dec) - (update a :unread-count inc)) + (let [index-existing (->> (map-indexed vector (:notifications acc)) + (filter (fn [[idx {:keys [id]}]] (= id (:id notification)))) + first + first)] + (as-> acc a + (if read + (update a :unread-count dec) + (update a :unread-count inc)) - (if (or dismissed accepted) - (update a :notifications (fn [items] (remove #(= (:id notification) (:id %)) items))) - (update a :notifications conj notification)))) + (if index-existing + (if (or dismissed accepted) + ;; Remove at specific location + (assoc a :notifications + (into (subvec (:notifications a) 0 index-existing) (subvec (:notifications a) (inc index-existing)))) + ;; Replace element + (do + (assoc-in a [:notifications index-existing] notification))) + (update a :notifications conj notification))))) {:unread-count (get db :activity.center/notifications-count 0) - :notifications (get-in db [:activity.center/notifications :notifications])} + :notifications (into [] (get-in db [:activity.center/notifications :notifications]))} activities)] (merge {:db (-> db @@ -69,14 +84,21 @@ {:events [:accept-all-activity-center-notifications-from-chat]} [{:keys [db]} chat-id] (let [notifications (get-in db [:activity.center/notifications :notifications]) - notifications-from-chat (filter #(= chat-id (:chat-id %)) notifications) + notifications-from-chat (filter #(and + (= chat-id (:chat-id %)) + (not (contains? non-dismissable-notifications (:type %)))) + notifications) notifications-from-chat-not-read (filter #(and (= chat-id (:chat-id %)) - (not (:read %))) notifications) - ids (map :id notifications-from-chat)] + (not (:read %))) + notifications) + ids (into #{} (map :id notifications-from-chat))] (when (seq ids) {:db (-> db (update-in [:activity.center/notifications :notifications] - (fn [items] (filter #(not (= chat-id (:chat-id %))) items))) + (fn [items] + (filter + #(not (contains? ids (:id %))) + items))) (update :activity.center/notifications-count - (min (db :activity.center/notifications-count) (count notifications-from-chat-not-read)))) ::json-rpc/call [{:method "wakuext_acceptActivityCenterNotifications" :params [ids] @@ -170,4 +192,3 @@ (update-in [:activity.center/notifications :notifications] concat (map data-store.activities/<-rpc notifications)))}) - diff --git a/src/status_im/react_native/resources.cljs b/src/status_im/react_native/resources.cljs index 2ff16e14b1..375c8c0ca7 100644 --- a/src/status_im/react_native/resources.cljs +++ b/src/status_im/react_native/resources.cljs @@ -45,6 +45,7 @@ :notifications (js/require "../resources/images/ui/notifications.png") :collectible (js/require "../resources/images/ui/collectible.png") :collectible-dark (js/require "../resources/images/ui/collectible-dark.png") + :hand-wave (js/require "../resources/images/ui/hand-wave.png") :graph (js/require "../resources/images/ui/graph.png")}) (defn get-theme-image [k] diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index f4c5843b0e..d9c3217ae2 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -264,6 +264,9 @@ (reg-root-key-sub :wallet-connect/sessions :wallet-connect/sessions) (reg-root-key-sub :wallet-connect-legacy/sessions :wallet-connect-legacy/sessions) (reg-root-key-sub :wallet-connect/session-managed :wallet-connect/session-managed) +(reg-root-key-sub :contact-requests/pending :contact-requests/pending) + +(reg-root-key-sub :mutual-contact-requests/enabled? :mutual-contact-requests/enabled?) (re-frame/reg-sub :communities @@ -941,6 +944,13 @@ (fn [ui-props [_ prop]] (get ui-props prop))) +(re-frame/reg-sub + :chats/current-chat-contact + :<- [:contacts/contacts] + :<- [:chats/current-chat-id] + (fn [[contacts current-chat-id]] + (get contacts current-chat-id))) + (re-frame/reg-sub :chats/home-list-chats :<- [::chats] @@ -1033,7 +1043,10 @@ :<- [:multiaccount/public-key] :<- [:communities/current-community] :<- [:contacts/blocked-set] - (fn [[{:keys [group-chat chat-id] :as current-chat} my-public-key community blocked-users-set]] + :<- [:contacts/contacts] + :<- [:chat/inputs] + :<- [:mutual-contact-requests/enabled?] + (fn [[{:keys [group-chat chat-id] :as current-chat} my-public-key community blocked-users-set contacts inputs mutual-contact-requests-enabled?]] (when current-chat (cond-> current-chat (chat.models/public-chat? current-chat) @@ -1049,7 +1062,15 @@ (assoc :show-input? true) (not group-chat) - (assoc :show-input? (not (contains? blocked-users-set chat-id))))))) + (assoc :show-input? + (and + (or + (not mutual-contact-requests-enabled?) + (get-in inputs [chat-id :metadata :sending-contact-request]) + (and mutual-contact-requests-enabled? + (= constants/contact-request-state-mutual + (get-in contacts [chat-id :contact-request-state])))) + (not (contains? blocked-users-set chat-id)))))))) (re-frame/reg-sub :chats/current-chat-chat-view @@ -1300,6 +1321,12 @@ (fn [{:keys [metadata]}] (:editing-message metadata))) +(re-frame/reg-sub + :chats/sending-contact-request + :<- [:chats/current-chat-inputs] + (fn [{:keys [metadata]}] + (:sending-contact-request metadata))) + (re-frame/reg-sub :chats/sending-image :<- [:chats/current-chat-inputs] @@ -1321,22 +1348,27 @@ :<- [:current-chat/metadata] :<- [:chats/reply-message] :<- [:chats/edit-message] - (fn [[{:keys [processing]} sending-image mainnet? one-to-one-chat? {:keys [public?]} reply edit]] + :<- [:chats/sending-contact-request] + (fn [[{:keys [processing]} sending-image mainnet? one-to-one-chat? {:keys [public?]} reply edit sending-contact-request]] (let [sending-image (seq sending-image)] {:send (not processing) :stickers (and (or config/stickers-test-enabled? mainnet?) (not sending-image) + (not sending-contact-request) (not reply)) :image (and (not reply) (not edit) + (not sending-contact-request) (not public?)) :extensions (and one-to-one-chat? (or config/commands-enabled? mainnet?) (not edit) + (not sending-contact-request) (not reply)) :audio (and (not sending-image) (not reply) (not edit) + (not sending-contact-request) (not public?)) :sending-image sending-image}))) @@ -1896,6 +1928,8 @@ (filter (fn [{:keys [type last-message]}] (or (and (= constants/activity-center-notification-type-one-to-one-chat type) (not (nil? last-message))) + (= constants/activity-center-notification-type-contact-request type) + (= constants/activity-center-notification-type-contact-request-retracted type) (= constants/activity-center-notification-type-private-group-chat type) (= constants/activity-center-notification-type-reply type) (= constants/activity-center-notification-type-mention type))) diff --git a/src/status_im/ui/screens/advanced_settings/views.cljs b/src/status_im/ui/screens/advanced_settings/views.cljs index de937173da..aee913f6e2 100644 --- a/src/status_im/ui/screens/advanced_settings/views.cljs +++ b/src/status_im/ui/screens/advanced_settings/views.cljs @@ -14,8 +14,9 @@ wakuv2-flag current-fleet webview-debug - wallet-connect-enabled? - new-ui-enabled?]}] + new-ui-enabled? + mutual-contact-requests-enabled? + wallet-connect-enabled?]}] (keep identity [{:size :small @@ -108,6 +109,15 @@ [:multiaccounts.ui/waku-bloom-filter-mode-switched (not waku-bloom-filter-mode)]) :accessory :switch :active waku-bloom-filter-mode} + {:size :small + :title (i18n/label :t/mutual-contact-requests) + :accessibility-label :wallet-connect-settings-switch + :container-margin-bottom 8 + :on-press + #(re-frame/dispatch + [:multiaccounts.ui/switch-mutual-contact-requests-enabled (not mutual-contact-requests-enabled?)]) + :accessory :switch + :active mutual-contact-requests-enabled?} {:size :small :title (i18n/label :t/wallet-connect) :accessibility-label :wallet-connect-settings-switch @@ -143,6 +153,7 @@ transactions-management-enabled? [:wallet/transactions-management-enabled?] current-log-level [:log-level/current-log-level] current-fleet [:fleets/current-fleet] + mutual-contact-requests-enabled? [:mutual-contact-requests/enabled?] wallet-connect-enabled? [:wallet-connect/enabled?]] [list/flat-list {:data (flat-list-data @@ -155,7 +166,8 @@ :wakuv2-flag wakuv2-flag :waku-bloom-filter-mode waku-bloom-filter-mode :webview-debug webview-debug - :wallet-connect-enabled? wallet-connect-enabled? - :new-ui-enabled? @config/new-ui-enabled?}) + :new-ui-enabled? @config/new-ui-enabled? + :mutual-contact-requests-enabled? mutual-contact-requests-enabled? + :wallet-connect-enabled? wallet-connect-enabled?}) :key-fn (fn [_ i] (str i)) :render-fn render-item}])) diff --git a/src/status_im/ui/screens/chat/audio_message/views.cljs b/src/status_im/ui/screens/chat/audio_message/views.cljs index ed389fbd3d..a97b3016b0 100644 --- a/src/status_im/ui/screens/chat/audio_message/views.cljs +++ b/src/status_im/ui/screens/chat/audio_message/views.cljs @@ -227,13 +227,13 @@ [react/animated-view {:style (styles/rec-outer-circle outer-scale)}] [react/animated-view {:style (styles/rec-inner-circle inner-scale inner-border-radius)}]]])) -(defn- cancel-button [disabled? on-press] +(defn- cancel-button [disabled? on-press contact-request] [pressable/pressable {:type :scale :disabled disabled? :on-press on-press} [react/view {:style (input.style/send-message-button)} [icons/icon :main-icons/close - {:container-style (merge (input.style/send-message-container) {:background-color colors/gray}) + {:container-style (merge (input.style/send-message-container contact-request) {:background-color colors/gray}) :accessibility-label :cancel-message-button :color colors/white-persist}]]]) @@ -272,13 +272,14 @@ (reset! on-background-cb nil))} (let [base-params {:rec-button-anim-value rec-button-anim-value :ctrl-buttons-anim-value ctrl-buttons-anim-value - :timer timer}] + :timer timer} + contact-request @(re-frame/subscribe [:chats/sending-contact-request])] [react/view {:style styles/container} [react/text {:style styles/timer :accessibility-label :audio-message-recorded-time} @timer] [react/view {:style styles/buttons-container} [react/animated-view {:style {:opacity ctrl-buttons-anim-value}} - [cancel-button (:cancel-disabled? @state) #(stop-recording base-params)]] + [cancel-button (:cancel-disabled? @state) #(stop-recording base-params) contact-request]] [rec-button-view (merge base-params {:state state})] [react/animated-view {:style {:opacity ctrl-buttons-anim-value}} [input/send-button (fn [] (cond diff --git a/src/status_im/ui/screens/chat/components/contact_request.cljs b/src/status_im/ui/screens/chat/components/contact_request.cljs new file mode 100644 index 0000000000..f6f0813863 --- /dev/null +++ b/src/status_im/ui/screens/chat/components/contact_request.cljs @@ -0,0 +1,86 @@ +(ns status-im.ui.screens.chat.components.contact-request + (:require [quo.core :as quo] + [quo.react :as quo.react] + [quo.react-native :as rn] + [quo.design-system.colors :as quo.colors] + [status-im.i18n.i18n :as i18n] + [status-im.ethereum.stateofus :as stateofus] + [status-im.ui.screens.chat.components.style :as styles] + [re-frame.core :as re-frame] + [clojure.string :as string]) + (:require-macros [status-im.utils.views :refer [defview letsubs]])) + +(def ^:private contact-request-symbol "↪ ") + +(defn input-focus [text-input-ref] + (some-> ^js (quo.react/current-ref text-input-ref) .focus)) + +(defn format-author [contact-name] + (let [author (if (or (= (aget contact-name 0) "@") + ;; in case of replies + (= (aget contact-name 1) "@")) + (or (stateofus/username contact-name) + (subs contact-name 0 81)) + contact-name)] + (i18n/label :contact-requesting-to {:author author}))) + +(defn format-contact-request-author [from username current-public-key] + (or (and (= from current-public-key) + (str contact-request-symbol (i18n/label :t/You))) + (str contact-request-symbol (format-author username)))) + +(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") + @(re-frame/subscribe [:contacts/contact-name-by-identity literal]) + + (seq children) + (get-quoted-text-with-mentions children) + + :else + literal)) + parsed-text))) + +(defn contact-request-message [their-public-key] + (let [{:keys [input-text]} @(re-frame/subscribe [:chats/current-chat-inputs])] + [rn/view {:style {:flex-direction :row}} + [rn/view {:style (styles/contact-request-content)} + [quo/button {:type :secondary + :weight :medium + :number-of-lines 1 + :style {:line-height 18} + :on-press #(re-frame/dispatch [:chat.ui/cancel-contact-request])} + (i18n/label :t/cancel)] + [quo/button {:type :secondary + :disabled (string/blank? input-text) + :weight :medium + :after :main-icons/send + :on-press #(re-frame/dispatch [:contacts/send-contact-request their-public-key input-text]) + :style {:line-height 18}} + (i18n/label :t/send-request)]]])) + +(defn focus-input-on-contact-request [contact-request had-contact-request text-input-ref] + ;;when we show contact-request we focus input + (when-not (= contact-request @had-contact-request) + (reset! had-contact-request contact-request) + (when contact-request + (js/setTimeout #(input-focus text-input-ref) 250)))) + +(defn contact-request-message-wrapper [contact-request] + [rn/view {:style {:padding-horizontal 15 + :border-top-width 1 + :border-top-color (:ui-01 @quo.colors/theme) + :padding-vertical 8}} + [contact-request-message contact-request]]) + +(defview contact-request-message-auto-focus-wrapper [text-input-ref] + (letsubs [had-reply (atom nil) + contact-request @(re-frame/subscribe [:chats/sending-contact-request])] + {:component-did-mount #(focus-input-on-contact-request contact-request had-reply text-input-ref)} + (when contact-request + [contact-request-message-wrapper contact-request]))) diff --git a/src/status_im/ui/screens/chat/components/input.cljs b/src/status_im/ui/screens/chat/components/input.cljs index 7644a58021..853e7fd7b5 100644 --- a/src/status_im/ui/screens/chat/components/input.cljs +++ b/src/status_im/ui/screens/chat/components/input.cljs @@ -73,13 +73,14 @@ [icons/icon :main-icons/keyboard (styles/icon false)] [icons/icon :main-icons/speech (styles/icon false)])]]) -(defn send-button [on-send] +(defn send-button [on-send contact-request] [rn/touchable-opacity {:on-press-in on-send} [rn/view {:style (styles/send-message-button)} - [icons/icon :main-icons/arrow-up - {:container-style (styles/send-message-container) - :accessibility-label :send-message-button - :color (styles/send-icon-color)}]]]) + (when-not contact-request + [icons/icon :main-icons/arrow-up + {:container-style (styles/send-message-container contact-request) + :accessibility-label :send-message-button + :color (styles/send-icon-color)}])]]) (defn on-selection-change [timeout-id last-text-change mentionable-users args] (let [selection (.-selection ^js (.-nativeEvent ^js args)) @@ -232,10 +233,11 @@ mentionable-users @(re-frame/subscribe [:chats/mentionable-users]) timeout-id (atom nil) last-text-change (atom nil) - mentions-enabled (get @mentions-enabled chat-id)] + mentions-enabled (get @mentions-enabled chat-id) + contact-request @(re-frame/subscribe [:chats/sending-contact-request])] [rn/text-input - {:style (styles/text-input) + {:style (styles/text-input contact-request) :ref (:text-input-ref refs) :max-font-size-multiplier 1 :accessibility-label :chat-message-input @@ -327,8 +329,8 @@ (when (seq sending-image) [reply/send-image sending-image]))) -(defn actions [extensions image show-send actions-ref active-panel set-active-panel] - [rn/view {:style (styles/actions-wrapper show-send) +(defn actions [extensions image show-send actions-ref active-panel set-active-panel contact-request] + [rn/view {:style (styles/actions-wrapper (and (not contact-request) show-send)) :ref actions-ref} (when extensions [touchable-icon {:panel :extensions @@ -354,12 +356,13 @@ :sticker-ref sticker-ref :text-input-ref text-input-ref} {:keys [send stickers image extensions audio sending-image]} @toolbar-options - show-send (or sending-image (seq (get @input-texts chat-id)))] + show-send (or sending-image (seq (get @input-texts chat-id))) + contact-request @(re-frame/subscribe [:chats/sending-contact-request])] [rn/view {:style (styles/toolbar) :on-layout on-chat-toolbar-layout} ;;EXTENSIONS and IMAGE buttons - [actions extensions image show-send actions-ref active-panel set-active-panel] - [rn/view {:style (styles/input-container)} + [actions extensions image show-send actions-ref active-panel set-active-panel contact-request] + [rn/view {:style (styles/input-container contact-request)} [send-image] [rn/view {:style styles/input-row} [text-input {:chat-id chat-id @@ -370,7 +373,8 @@ [rn/view {:ref send-ref :style (when-not show-send {:width 0 :right -100})} (when send [send-button #(do (clear-input chat-id refs) - (re-frame/dispatch [:chat.ui/send-current-message]))])] + (re-frame/dispatch [:chat.ui/send-current-message])) + contact-request])] ;;STICKERS and AUDIO buttons (when-not @(re-frame/subscribe [:chats/edit-message]) diff --git a/src/status_im/ui/screens/chat/components/style.cljs b/src/status_im/ui/screens/chat/components/style.cljs index 148f871294..aee891ec76 100644 --- a/src/status_im/ui/screens/chat/components/style.cljs +++ b/src/status_im/ui/screens/chat/components/style.cljs @@ -11,14 +11,15 @@ :align-items :flex-end :flex-direction :row}) -(defn input-container [] +(defn input-container [contact-request] {:background-color (:ui-01 @colors/theme) :flex 1 - :border-top-left-radius 16 - :border-top-right-radius 16 - :border-bottom-right-radius 4 - :border-bottom-left-radius 16 - :margin-horizontal 8}) + :height (when contact-request 44) + :border-top-left-radius (if contact-request 8 16) + :border-top-right-radius (if contact-request 8 16) + :border-bottom-right-radius (if contact-request 8 4) + :border-bottom-left-radius (if contact-request 8 16) + :margin-horizontal (when contact-request 8)}) (def input-row {:flex-direction :row @@ -34,7 +35,7 @@ (when platform/ios? {:padding-top 2}))) -(defn text-input [] +(defn text-input [contact-request] (merge typography/font-regular typography/base {:flex 1 @@ -46,8 +47,8 @@ :padding-horizontal 12} (if platform/android? {:padding-vertical 2} - {:padding-top 2 - :padding-bottom 6}))) + {:padding-top (if contact-request 10 2) + :padding-bottom (if contact-request 5 6)}))) (defn actions-wrapper [show-send] (merge @@ -91,6 +92,11 @@ :padding-horizontal 10 :flex 1}) +(defn contact-request-content [] + {:flex 1 + :flex-direction :row + :justify-content :space-between}) + (defn close-button [] {:margin-top 3}) @@ -98,11 +104,11 @@ {:margin-vertical 4 :margin-horizontal 5}) -(defn send-message-container [] +(defn send-message-container [contact-request] {:background-color (:interactive-01 @colors/theme) :width 26 - :height 26 - :border-radius 13 + :height (if contact-request 44 26) + :border-radius (if contact-request 22 13) :justify-content :center :align-items :center}) diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index d31ce19d26..5aab7e7fbb 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -621,6 +621,49 @@ [message.audio/message-content message] [message-status message]]]]] reaction-picker]))) +(defn contact-request-status-pending [] + [react/view {:style {:flex-direction :row}} + [quo/text {:style {:margin-right 5.27} + :weight :medium + :color :secondary} + (i18n/label :t/contact-request-pending)] + [react/activity-indicator {:animating true + :size :small + :color colors/gray}]]) + +(defn contact-request-status-accepted [] + [quo/text {:style {:color colors/green} + :weight :medium} + (i18n/label :t/contact-request-accepted)]) + +(defn contact-request-status-declined [] + [quo/text {:style {:color colors/red} + :weight :medium} + (i18n/label :t/contact-request-declined)]) + +(defn contact-request-status-label [state] + [react/view {:style (style/contact-request-status-label state)} + (case state + constants/contact-request-message-state-pending [contact-request-status-pending] + constants/contact-request-message-state-accepted [contact-request-status-accepted] + constants/contact-request-message-state-declined [contact-request-status-declined])]) + +(defmethod ->message constants/content-type-contact-request + [{:keys [outgoing] :as message} _] + [react/view {:style (style/content-type-contact-request outgoing)} + [react/image {:source (resources/get-image :hand-wave) + :style {:width 112 + :height 97}}] + [quo/text {:style {:margin-top 6} + :weight :bold + :size :large} + (i18n/label :t/contact-request)] + [react/view {:style {:padding-horizontal 16}} + [quo/text {:style {:margin-top 2 + :margin-bottom 14}} + (get-in message [:content :text])]] + [contact-request-status-label (:contact-request-state message)]]) + (defmethod ->message :default [message] [message-content-wrapper message [unknown-content-type message]]) diff --git a/src/status_im/ui/screens/chat/pinned_messages.cljs b/src/status_im/ui/screens/chat/pinned_messages.cljs index 41dc2a0d30..2e7cc0c848 100644 --- a/src/status_im/ui/screens/chat/pinned_messages.cljs +++ b/src/status_im/ui/screens/chat/pinned_messages.cljs @@ -116,4 +116,4 @@ [connectivity/loading-indicator] [pinned-messages-view {:chat chat :pan-responder pan-responder - :space-keeper space-keeper}]])))) \ No newline at end of file + :space-keeper space-keeper}]])))) diff --git a/src/status_im/ui/screens/chat/styles/main.cljs b/src/status_im/ui/screens/chat/styles/main.cljs index c6f0256996..f037a9d1d8 100644 --- a/src/status_im/ui/screens/chat/styles/main.cljs +++ b/src/status_im/ui/screens/chat/styles/main.cljs @@ -53,8 +53,7 @@ {:flex 1 :flex-direction :column :justify-content :center - :align-items :center - :height 324} + :align-items :center} {:flex 1 :flex-direction :column :justify-content :center @@ -148,3 +147,10 @@ {:font-size 13 :line-height 18 :text-align :center}) + +(def contact-request + {:width "100%" + :justify-content :center + :align-items :center + :border-top-width 1 + :border-color colors/gray-transparent-10}) diff --git a/src/status_im/ui/screens/chat/styles/message/message.cljs b/src/status_im/ui/screens/chat/styles/message/message.cljs index 80dcdf2849..841434e593 100644 --- a/src/status_im/ui/screens/chat/styles/message/message.cljs +++ b/src/status_im/ui/screens/chat/styles/message/message.cljs @@ -435,3 +435,32 @@ :border-bottom-left-radius 10 :border-bottom-right-radius 10 :border-color colors/gray-lighter}) + +(defn contact-request-status-label [state] + {:width 136 + :border-radius 8 + :flex 1 + :justify-content :center + :align-items :center + :background-color (when (= :retry state) + colors/blue-light) + :border-width 1 + :border-color (case state + constants/contact-request-message-state-accepted colors/green-transparent-10 + constants/contact-request-message-state-declined colors/red-light + constants/contact-request-message-state-pending colors/gray-lighter) + :padding-vertical 10 + :padding-horizontal 16}) + +(defn content-type-contact-request [outgoing] + {:width 168 + :min-height 224.71 + :border-radius 8 + :border-width 1 + :border-color colors/gray-lighter + :align-items :center + :padding-bottom 10 + :margin-vertical 4 + :align-self (if outgoing :flex-end :flex-start) + :margin-right (if outgoing 8 0) + :margin-left (if outgoing 0 8)}) diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index 2698d66098..420f6d1708 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -10,6 +10,7 @@ [status-im.ui.components.list.views :as list] [status-im.ui.screens.chat.components.reply :as reply] [status-im.ui.screens.chat.components.edit :as edit] + [status-im.ui.screens.chat.components.contact-request :as contact-request] [status-im.ui.components.react :as react] [quo.animated :as animated] [quo.react-native :as rn] @@ -35,7 +36,8 @@ [status-im.utils.utils :as utils] [status-im.ui.screens.chat.sheets :as sheets] [status-im.utils.debounce :as debounce] - [status-im.navigation.state :as navigation.state])) + [status-im.navigation.state :as navigation.state] + [status-im.react-native.resources :as resources])) (defn invitation-requests [chat-id admins] (let [current-pk @(re-frame/subscribe [:multiaccount/public-key]) @@ -62,20 +64,45 @@ {:color colors/blue}] [react/i18n-text {:style style/add-contact-text :key :add-to-contacts}]]])) +(defn contact-request [] + (let [contact-request @(re-frame/subscribe [:chats/sending-contact-request])] + [react/view {:style style/contact-request} + [react/image {:source (resources/get-image :hand-wave) + :style {:width 112 + :height 96.71 + :margin-top 17}}] + [quo/text {:style {:margin-top 14} + :weight :bold + :size :large} + (i18n/label :t/say-hi)] + [quo/text {:style {:margin-top 2 + :margin-bottom 14}} + (i18n/label :t/send-contact-request-message)] + (when-not contact-request + [react/view {:style {:padding-horizontal 16 + :padding-bottom 8}} + [quo/button + {:style {:width "100%"} + :accessibility-label :contact-request--button + :on-press #(re-frame/dispatch [:chat.ui/send-contact-request])} + (i18n/label :t/contact-request)]])])) + (defn chat-intro [{:keys [chat-id chat-name chat-type group-chat invitation-admin + mutual-contact-requests-enabled? contact-name color loading-messages? no-messages? + contact-request-state emoji]}] [react/view {:style (style/intro-header-container loading-messages? no-messages?) :accessibility-label :history-chat} ;; Icon section - [react/view {:style {:margin-top 42 + [react/view {:style {:margin-top 52 :margin-bottom 24}} [chat-icon.screen/emoji-chat-intro-icon-view chat-name chat-id group-chat emoji @@ -100,12 +127,28 @@ (str (i18n/label :t/empty-chat-description-one-to-one) - contact-name)])]) + contact-name)]) + (when + (and + mutual-contact-requests-enabled? + (= chat-type constants/one-to-one-chat-type) + (or + (= contact-request-state constants/contact-request-state-none) + (= contact-request-state constants/contact-request-state-received) + (= contact-request-state constants/contact-request-state-dismissed))) + [contact-request])]) (defn chat-intro-one-to-one [{:keys [chat-id] :as opts}] - (let [contact-names @(re-frame/subscribe + (let [contact @(re-frame/subscribe + [:contacts/contact-by-identity chat-id]) + mutual-contact-requests-enabled? @(re-frame/subscribe [:mutual-contact-requests/enabled?]) + contact-names @(re-frame/subscribe [:contacts/contact-two-names-by-identity chat-id])] - [chat-intro (assoc opts :contact-name (first contact-names))])) + [chat-intro (assoc opts + :mutual-contact-requests-enabled? mutual-contact-requests-enabled? + :contact-name (first contact-names) + :contact-request-state (or (:contact-request-state contact) + constants/contact-request-state-none))])) (defn chat-intro-header-container [{:keys [group-chat invitation-admin @@ -299,9 +342,23 @@ :edit-enabled edit-enabled :in-pinned-view? in-pinned-view?})) -(defn messages-view [{:keys [chat bottom-space pan-responder space-keeper show-input?]}] - (let [{:keys [group-chat chat-id public? community-id admins]} chat - messages @(re-frame/subscribe [:chats/raw-chat-messages-stream chat-id])] +(defn messages-view [{:keys [chat + bottom-space + pan-responder + mutual-contact-requests-enabled? + space-keeper + show-input?]}] + (let [{:keys [group-chat chat-type chat-id public? community-id admins]} chat + + messages @(re-frame/subscribe [:chats/raw-chat-messages-stream chat-id]) + one-to-one? (= chat-type constants/one-to-one-chat-type) + contact-added? (when one-to-one? @(re-frame/subscribe [:contacts/contact-added? chat-id])) + should-send-contact-request? + (and + mutual-contact-requests-enabled? + one-to-one? + (not contact-added?))] + ;;do not use anonymous functions for handlers [list/flat-list (merge @@ -310,7 +367,8 @@ :ref list-ref :header [list-header chat] :footer [list-footer chat] - :data messages + :data (when-not should-send-contact-request? + messages) :render-data (get-render-data {:group-chat group-chat :chat-id chat-id :public? public? @@ -385,6 +443,7 @@ (let [{:keys [chat-id show-input? group-chat admins invitation-admin] :as chat} ;;we want to react only on these fields, do not use full chat map here @(re-frame/subscribe [:chats/current-chat-chat-view]) + mutual-contact-requests-enabled? @(re-frame/subscribe [:mutual-contact-requests/enabled?]) max-bottom-space (max @bottom-space @panel-space)] [:<> [topbar] @@ -392,11 +451,12 @@ (when chat-id (if group-chat [invitation-requests chat-id admins] - [add-contact-bar chat-id])) + (when-not mutual-contact-requests-enabled? [add-contact-bar chat-id]))) ;;MESSAGES LIST [messages-view {:chat chat :bottom-space max-bottom-space :pan-responder pan-responder + :mutual-contact-requests-enabled? mutual-contact-requests-enabled? :space-keeper space-keeper :show-input? show-input?}] (when (and group-chat invitation-admin) @@ -421,7 +481,8 @@ {:chat-id chat-id :active-panel @active-panel :set-active-panel set-active-panel - :text-input-ref text-input-ref}]] + :text-input-ref text-input-ref}] + [contact-request/contact-request-message-auto-focus-wrapper text-input-ref]] [bottom-sheet @active-panel]])])))) (defn chat [] diff --git a/src/status_im/ui/screens/home/views/inner_item.cljs b/src/status_im/ui/screens/home/views/inner_item.cljs index c816e79b26..a5eb011d21 100644 --- a/src/status_im/ui/screens/home/views/inner_item.cljs +++ b/src/status_im/ui/screens/home/views/inner_item.cljs @@ -82,6 +82,7 @@ [preview-label :t/no-messages] (and (or (= constants/content-type-text content-type) + (= constants/content-type-contact-request content-type) (= constants/content-type-emoji content-type) (= constants/content-type-command content-type)) (not (string/blank? (:text content)))) diff --git a/src/status_im/ui/screens/notifications_center/styles.cljs b/src/status_im/ui/screens/notifications_center/styles.cljs index 295ba77722..5b36b65a25 100644 --- a/src/status_im/ui/screens/notifications_center/styles.cljs +++ b/src/status_im/ui/screens/notifications_center/styles.cljs @@ -62,9 +62,9 @@ (def notification-content-container {:flex 1}) -(def photo-container +(defn photo-container [has-header?] {:position :absolute - :top 12 + :top (if has-header? 37 12) :left 16}) (defn title-text [title-text-width] diff --git a/src/status_im/ui/screens/notifications_center/views.cljs b/src/status_im/ui/screens/notifications_center/views.cljs index 3bd8d4f46e..55323b9c28 100644 --- a/src/status_im/ui/screens/notifications_center/views.cljs +++ b/src/status_im/ui/screens/notifications_center/views.cljs @@ -33,7 +33,11 @@ {:on-press (fn [] (if @selecting (on-change) - (re-frame/dispatch [:accept-activity-center-notification-and-open-chat id]))) + ;; We don't dispatch on contact requests unless + ;; accepted + (when (or (not= type constants/activity-center-notification-type-contact-request) + (= constants/contact-request-message-state-accepted (get-in home-item [:message :contact-request-state]))) + (re-frame/dispatch [:accept-activity-center-notification-and-open-chat id])))) :on-long-press #(do (reset! selecting true) (when-not (= type constants/activity-center-notification-type-mention) (swap! selected-items conj id)))}]]]))) (defn filter-item [] diff --git a/src/status_im/ui/screens/notifications_center/views/notification.cljs b/src/status_im/ui/screens/notifications_center/views/notification.cljs index b66d4c96f5..8fad25cd56 100644 --- a/src/status_im/ui/screens/notifications_center/views/notification.cljs +++ b/src/status_im/ui/screens/notifications_center/views/notification.cljs @@ -2,6 +2,8 @@ (:require [status-im.ui.components.react :as react] [re-frame.core :as re-frame] [quo.core :as quo] + [quo.components.animated.pressable :as animation] + [status-im.i18n.i18n :as i18n] [status-im.ui.screens.notifications-center.styles :as styles] [status-im.utils.handlers :refer [", "owner": "status-im", "repo": "status-go", - "version": "a244d776571cc73ea34d7a700cc5db36166ce832", - "commit-sha1": "a244d776571cc73ea34d7a700cc5db36166ce832", - "src-sha256": "1v057jy565y9178ja8j9j6sg04z83y7a0bnry6ziy9zf2zda7nkg" + "version": "v0.100.0", + "commit-sha1": "1bfde4c4cc69292875ae9fdc41a8b4bd29e95a7a", + "src-sha256": "1nkk7dsqz1x31n305p33jdwpqr9bm7ajkqs3khq5l4ag3njagbrh" } diff --git a/translations/en.json b/translations/en.json index 537a9de213..0962323dcb 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1738,5 +1738,17 @@ "wallet-manage-app-connections": "Manage app connections", "connection-request": "Connection Request", "disconnect": "Disconnect", - "new-ui": "New UI" + "new-ui": "New UI", + "send-contact-request-message": "To start a chat you need to become contacts", + "contact-request": "Contact request", + "say-hi": "Say hi", + "accepted": "Accepted", + "declined": "Declined", + "contact-request": "Contact request", + "contact-request-header": "👋 Contact requests", + "contact-request-declined": "Declined ⓧ", + "contact-request-accepted": "Accepted ✓", + "contact-request-pending": "Pending...", + "removed-from-contacts": "Removed from contacts", + "mutual-contact-requests": "Mutual contact requests" }